diff --git a/.DS_Store b/.DS_Store index 5c9bed05..9117d665 100644 Binary files a/.DS_Store and b/.DS_Store differ diff --git a/local-runner/docker-compose.yml b/.bin/docker-compose.yml similarity index 100% rename from local-runner/docker-compose.yml rename to .bin/docker-compose.yml diff --git a/local-runner/mkdocs.yml b/.bin/mkdocs.yml similarity index 100% rename from local-runner/mkdocs.yml rename to .bin/mkdocs.yml diff --git a/local-runner/runFor.sh b/.bin/runFor.sh similarity index 99% rename from local-runner/runFor.sh rename to .bin/runFor.sh index 823f3859..825b75d8 100755 --- a/local-runner/runFor.sh +++ b/.bin/runFor.sh @@ -1,6 +1,6 @@ #!/bin/bash -NAME="Dark Lord Toni" +NAME="Development" REMOVE=0 RED='\033[0;31m' diff --git a/local-runner/runForDoc.md b/.bin/runForDoc.md similarity index 94% rename from local-runner/runForDoc.md rename to .bin/runForDoc.md index ded2191f..097887c0 100644 --- a/local-runner/runForDoc.md +++ b/.bin/runForDoc.md @@ -4,7 +4,7 @@ The runFor.sh Script will start a Docker Postgis Container with [docker-compose. The Postgis data volume will be mounted to: -- __local-runner/postgis-volume__ +- __.bin/postgis-volume__ so that even if the container is deleted no data will be lost! diff --git a/.deployment/runtest.sh b/.bin/test-local.sh similarity index 100% rename from .deployment/runtest.sh rename to .bin/test-local.sh diff --git a/.bsp/sbt.json b/.bsp/sbt.json index a636095f..cb536e81 100644 --- a/.bsp/sbt.json +++ b/.bsp/sbt.json @@ -1 +1 @@ -{"name":"sbt","version":"1.4.4","bspVersion":"2.0.0-M5","languages":["scala"],"argv":["/Applications/IntelliJ IDEA.app/Contents/jbr/Contents/Home/bin/java","-Xms100m","-Xmx100m","-classpath","/Users/patrickstadler/Library/Application Support/JetBrains/IntelliJIdea2020.3/plugins/Scala/launcher/sbt-launch.jar","xsbt.boot.Boot","-bsp"]} \ No newline at end of file +{"name":"sbt","version":"1.5.0","bspVersion":"2.0.0-M5","languages":["scala"],"argv":["/Applications/IntelliJ IDEA.app/Contents/jbr/Contents/Home/bin/java","-Xms100m","-Xmx100m","-classpath","/Users/patrickstadler/Library/Application Support/JetBrains/IntelliJIdea2020.3/plugins/Scala/launcher/sbt-launch.jar","xsbt.boot.Boot","-bsp","--sbt-launch-jar=/Users/patrickstadler/Library/Application%20Support/JetBrains/IntelliJIdea2020.3/plugins/Scala/launcher/sbt-launch.jar"]} \ No newline at end of file diff --git a/.gitignore b/.gitignore index a66307ac..67b0d545 100644 --- a/.gitignore +++ b/.gitignore @@ -34,7 +34,6 @@ server.pid *.eml /dist/ - test.mv.db #Firebase @@ -49,10 +48,12 @@ pubSub-dev.json .circleci/k8sdeploy.yaml-e .metals/* +.bsp/* + #puml out/doc .DS_Store -local-runner/postgis-volume +.bin/postgis-volume .bloop/ \ No newline at end of file diff --git a/app/Module.scala b/app/Module.scala index d40070da..f76a06c1 100755 --- a/app/Module.scala +++ b/app/Module.scala @@ -1,5 +1,7 @@ -import java.util.Properties +import com.google.auth.Credentials +import com.google.auth.oauth2.GoogleCredentials +import java.util.Properties import javax.inject.{ Inject, Provider, Singleton } import com.typesafe.config.Config import play.api.inject.ApplicationLifecycle @@ -7,11 +9,19 @@ import play.api.{ Configuration, Environment, Logger, Mode } import slick.jdbc.JdbcBackend.Database import com.google.inject.AbstractModule import de.innfactory.auth.firebase.FirebaseBase +import de.innfactory.auth.firebase.FirebaseBase.getClass import de.innfactory.auth.firebase.validator.{ JWTValidatorMock, JwtValidator, JwtValidatorImpl } import de.innfactory.bootstrapplay2.db.{ CompaniesDAO, LocationsDAO } import de.innfactory.play.flyway.FlywayMigrator +import io.opencensus.exporter.trace.jaeger.{ JaegerExporterConfiguration, JaegerTraceExporter } +import io.opencensus.exporter.trace.logging.LoggingTraceExporter +import io.opencensus.exporter.trace.stackdriver.{ StackdriverTraceConfiguration, StackdriverTraceExporter } +import io.opencensus.trace.AttributeValue import play.api.libs.concurrent.AkkaGuiceSupport + +import java.io.InputStream import scala.concurrent.Future +import scala.jdk.CollectionConverters.MapHasAsJava /** * This module handles the bindings for the API to the Slick implementation. @@ -24,30 +34,97 @@ class Module(environment: Environment, configuration: Configuration) extends Abs override def configure(): Unit = { logger.info(s"Configuring ${environment.mode}") + bind(classOf[Database]).toProvider(classOf[DatabaseProvider]) - bind(classOf[firebaseCreationService]).asEagerSingleton() - bind(classOf[firebaseDeletionService]).asEagerSingleton() bind(classOf[FlywayMigratorImpl]).asEagerSingleton() - bind(classOf[LocationsDAOCloseHook]).asEagerSingleton() - bind(classOf[CompaniesDAOCloseHook]).asEagerSingleton() + bind(classOf[DAOCloseHook]).asEagerSingleton() /** * Inject Modules depended on environment (Test, Prod, Dev) */ if (environment.mode == Mode.Test) { + logger.info(s"- - - Binding Services for for Test Mode - - -") - bind(classOf[JwtValidator]) - .to(classOf[JWTValidatorMock]) // Bind Mock JWT Validator for Test Mode + + // Bind Mock JWT Validator for Test Mode + bind(classOf[JwtValidator]).to(classOf[JWTValidatorMock]) + + } else if (environment.mode == Mode.Dev) { + + logger.info(s"- - - Binding Services for for Dev Mode - - -") + + // Firebase + bind(classOf[firebaseCreationService]).asEagerSingleton() + bind(classOf[firebaseDeletionService]).asEagerSingleton() + + // Bind Prod JWT Validator for Prod/Dev Mode + bind(classOf[JwtValidator]).to(classOf[JwtValidatorImpl]) + + // Optional Jaeger Exporter bind(classOf[JaegerTracingCreator]).asEagerSingleton() + } else { - logger.info(s"- - - Binding Services for for Prod/Dev Mode - - -") - bind(classOf[JwtValidator]) - .to(classOf[JwtValidatorImpl]) // Bind Prod JWT Validator for Prod/Dev Mode + + logger.info(s"- - - Binding Services for for Prod Mode - - -") + + bind(classOf[firebaseCreationService]).asEagerSingleton() + bind(classOf[firebaseDeletionService]).asEagerSingleton() + + // Bind Prod JWT Validator for Prod/Dev Mode + bind(classOf[JwtValidator]).to(classOf[JwtValidatorImpl]) + + // Tracing + bind(classOf[StackdriverTracingCreator]).asEagerSingleton() + bind(classOf[LoggingTracingCreator]).asEagerSingleton() + } } } +@Singleton +class LoggingTracingCreator @Inject() (lifecycle: ApplicationLifecycle) { + LoggingTraceExporter.register() + lifecycle.addStopHook { () => + Future.successful(LoggingTraceExporter.unregister()) + } +} + +@Singleton +class JaegerTracingCreator @Inject() (lifecycle: ApplicationLifecycle) { + val jaegerExporterConfiguration: JaegerExporterConfiguration = JaegerExporterConfiguration + .builder() + .setServiceName("bootstrap-play2") + .setThriftEndpoint("http://127.0.0.1:14268/api/traces") + .build() + JaegerTraceExporter.createAndRegister(jaegerExporterConfiguration) + + lifecycle.addStopHook { () => + Future.successful(JaegerTraceExporter.unregister()) + } +} + +@Singleton +class StackdriverTracingCreator @Inject() (lifecycle: ApplicationLifecycle, config: Config) { + val serviceAccount: InputStream = getClass.getClassLoader.getResourceAsStream(config.getString("firebase.file")) + val credentials: GoogleCredentials = GoogleCredentials.fromStream(serviceAccount) + val stackDriverTraceExporterConfig: StackdriverTraceConfiguration = StackdriverTraceConfiguration + .builder() + .setProjectId(config.getString("project.id")) + .setCredentials(credentials) + .setFixedAttributes( + Map( + ("/component", AttributeValue.stringAttributeValue("PlayServer")) + ).asJava + ) + .build() + + StackdriverTraceExporter.createAndRegister(stackDriverTraceExporterConfig) + lifecycle.addStopHook { () => + Future.successful(StackdriverTraceExporter.unregister()) + } +} + /** Migrate Flyway on application start */ class FlywayMigratorImpl @Inject() (env: Environment, configuration: Configuration) extends FlywayMigrator(configuration, env, configIdentifier = "bootstrap-play2") @@ -72,15 +149,11 @@ class DatabaseProvider @Inject() (config: Config) extends Provider[Database] { } /** Closes DAO. Important on dev restart. */ -class CompaniesDAOCloseHook @Inject() (dao: CompaniesDAO, lifecycle: ApplicationLifecycle) { - lifecycle.addStopHook { () => - Future.successful(dao.close()) - } -} - -/** Closes DAO. Important on dev restart. */ -class LocationsDAOCloseHook @Inject() (dao: LocationsDAO, lifecycle: ApplicationLifecycle) { +class DAOCloseHook @Inject() (companiesDAO: CompaniesDAO, locationsDAO: LocationsDAO, lifecycle: ApplicationLifecycle) { lifecycle.addStopHook { () => - Future.successful(dao.close()) + Future.successful({ + companiesDAO.close() + locationsDAO.close() + }) } } diff --git a/app/de/innfactory/bootstrapplay2/actions/CompanyForUserExtractAction.scala b/app/de/innfactory/bootstrapplay2/actions/CompanyForUserExtractAction.scala index e6760573..dc914111 100644 --- a/app/de/innfactory/bootstrapplay2/actions/CompanyForUserExtractAction.scala +++ b/app/de/innfactory/bootstrapplay2/actions/CompanyForUserExtractAction.scala @@ -1,35 +1,61 @@ package de.innfactory.bootstrapplay2.actions +import cats.implicits.catsSyntaxEitherId import com.google.inject.Inject import de.innfactory.bootstrapplay2.common.authorization.FirebaseEmailExtractor +import de.innfactory.bootstrapplay2.common.request.TraceContext +import de.innfactory.bootstrapplay2.common.results.ErrorResponse import de.innfactory.bootstrapplay2.db.CompaniesDAO import de.innfactory.bootstrapplay2.models.api.Company -import play.api.mvc.{ ActionBuilder, ActionTransformer, AnyContent, BodyParsers, Request, WrappedRequest } +import de.innfactory.play.tracing.{ RequestWithTrace, TraceRequest, UserExtractionActionBase } +import io.opencensus.trace.Span +import play.api.Environment +import play.api.mvc.Results.Forbidden +import play.api.mvc.{ BodyParsers, Request, Result, WrappedRequest } import scala.concurrent.{ ExecutionContext, Future } -class RequestWithCompany[A](val company: Option[Company], val email: Option[String], request: Request[A]) - extends WrappedRequest[A](request) +class RequestWithCompany[A]( + val company: Company, + val email: Option[String], + val request: Request[A], + val traceSpan: Span +) extends WrappedRequest[A](request) + with TraceRequest[A] class CompanyForUserExtractAction @Inject() ( - val parser: BodyParsers.Default, companiesDAO: CompaniesDAO, firebaseEmailExtractor: FirebaseEmailExtractor[Any] -)(implicit val executionContext: ExecutionContext) - extends ActionBuilder[RequestWithCompany, AnyContent] - with ActionTransformer[Request, RequestWithCompany] { - def transform[A](request: Request[A]): Future[RequestWithCompany[A]] = +)(implicit executionContext: ExecutionContext, parser: BodyParsers.Default, environment: Environment) + extends UserExtractionActionBase[RequestWithTrace, RequestWithCompany] { + + override def extractUserAndCreateNewRequest[A](request: RequestWithTrace[A])(implicit + environment: Environment, + parser: BodyParsers.Default, + executionContext: ExecutionContext + ): Future[Either[Result, RequestWithCompany[A]]] = Future.successful { val result: Option[Future[Option[Company]]] = for { email <- firebaseEmailExtractor.extractEmail(request) } yield for { - user <- companiesDAO.internal_lookupByEmail(email) + user <- companiesDAO.internal_lookupByEmail(email)(new TraceContext(request.traceSpan)) } yield user - result match { case Some(v) => - v.map(new RequestWithCompany(_, firebaseEmailExtractor.extractEmail(request), request)) - case None => Future(new RequestWithCompany(None, firebaseEmailExtractor.extractEmail(request), request)) + v.map { + case Some(value) => + new RequestWithCompany( + value, + firebaseEmailExtractor.extractEmail(request), + request.request, + request.traceSpan + ).asRight[Result] + case None => Forbidden(ErrorResponse.fromMessage("Forbidden")).asLeft[RequestWithCompany[A]] + } + case None => + Future( + Forbidden(ErrorResponse.fromMessage("Forbidden")).asLeft[RequestWithCompany[A]] + ) } }.flatten } diff --git a/app/de/innfactory/bootstrapplay2/actions/JwtValidationAction.scala b/app/de/innfactory/bootstrapplay2/actions/JwtValidationAction.scala index 45dfa916..9fd5010f 100644 --- a/app/de/innfactory/bootstrapplay2/actions/JwtValidationAction.scala +++ b/app/de/innfactory/bootstrapplay2/actions/JwtValidationAction.scala @@ -1,56 +1,28 @@ package de.innfactory.bootstrapplay2.actions import com.google.inject.Inject -import com.nimbusds.jwt.proc.BadJWTException -import de.innfactory.auth.firebase.validator.{ JwtToken, JwtValidator } -import play.api.Environment -import play.api.mvc.Results.Forbidden -import play.api.mvc.Results.Unauthorized +import de.innfactory.auth.firebase.validator.JwtValidator +import de.innfactory.bootstrapplay2.common.implicits.JWT.JwtTokenGenerator +import de.innfactory.play.tracing.{ BaseAuthHeaderRefineAction, RequestWithTrace } +import play.api.mvc.BodyParsers -import scala.concurrent.{ ExecutionContext, Future } -import play.api.mvc._ +import scala.concurrent.ExecutionContext -class JwtValidationAction @Inject() (parser: BodyParsers.Default, jwtValidator: JwtValidator, environment: Environment)( - implicit ec: ExecutionContext -) extends ActionBuilderImpl(parser) { - override def invokeBlock[A](request: Request[A], block: (Request[A]) => Future[Result]) = - if (extractAndCheckAuthHeader(request.headers).getOrElse(false)) - block(request) - else if (request.headers.get("Authorization").isEmpty) - Future.successful(Unauthorized("Unauthorized")) - else - Future.successful(Forbidden("Forbidden")) +class JwtValidationAction @Inject() ( + parser: BodyParsers.Default, + jwtValidator: JwtValidator +)(implicit + ec: ExecutionContext +) extends BaseAuthHeaderRefineAction[RequestWithTrace](parser) { - /** - * Extract auth header from requestHeaders - * @param requestHeader - * @return - */ - def extractAndCheckAuthHeader(requestHeader: Headers) = - for { - header <- requestHeader.get("Authorization") - } yield checkAuthHeader(header) - - /** - * check and validate auth header - * @param authHeader - * @return - */ - def checkAuthHeader(authHeader: String): Boolean = - // In Test env, jwt will not be validated - if (environment.mode.toString != "Test") { - val jwtToken = authHeader match { - case token: String if token.startsWith("Bearer") => - JwtToken(token.splitAt(7)._2) - case token => JwtToken(token) - } - - jwtValidator.validate(jwtToken) match { - case Left(error: BadJWTException) => - false - case Right(_) => true - } - } else - true + override def checkAuthHeader(authHeader: String): Boolean = { + val jwtToken = authHeader.toJwtToken + val res = jwtValidator.validate(jwtToken) match { + case Left(_) => false + case Right(_) => true + } + println("Auth Header Check on " + authHeader + " " + res) + res + } } diff --git a/app/de/innfactory/bootstrapplay2/actions/TracingCompanyAction.scala b/app/de/innfactory/bootstrapplay2/actions/TracingCompanyAction.scala new file mode 100644 index 00000000..68653dfa --- /dev/null +++ b/app/de/innfactory/bootstrapplay2/actions/TracingCompanyAction.scala @@ -0,0 +1,19 @@ +package de.innfactory.bootstrapplay2.actions + +import com.google.inject.Inject +import de.innfactory.play.tracing.TracingAction +import play.api.Environment +import play.api.mvc._ + +import scala.concurrent.ExecutionContext + +class TracingCompanyAction @Inject() ( + val parser: BodyParsers.Default, + companyAction: CompanyForUserExtractAction, + jwtValidationAction: JwtValidationAction, + traceAction: TracingAction, + implicit val environment: Environment +)(implicit val executionContext: ExecutionContext) { + def apply(traceString: String): ActionBuilder[RequestWithCompany, AnyContent] = + traceAction(traceString).andThen(jwtValidationAction).andThen(companyAction) +} diff --git a/app/de/innfactory/bootstrapplay2/actorsystem/services/HelloWorldService.scala b/app/de/innfactory/bootstrapplay2/actorsystem/services/HelloWorldService.scala index acb14372..f40c6c8b 100644 --- a/app/de/innfactory/bootstrapplay2/actorsystem/services/HelloWorldService.scala +++ b/app/de/innfactory/bootstrapplay2/actorsystem/services/HelloWorldService.scala @@ -21,7 +21,7 @@ trait HelloWorldService { class HelloWorldServiceImpl @Inject() ( )(implicit ec: ExecutionContext, system: ActorSystem) extends HelloWorldService { - // asking someone requires a timeout if the timeout hits without response + // // asking someone requires a timeout if the timeout hits without response // the ask is failed with a TimeoutException private implicit val timeout: Timeout = 10.seconds diff --git a/app/de/innfactory/bootstrapplay2/common/authorization/CompanyAuthorizationMethods.scala b/app/de/innfactory/bootstrapplay2/common/authorization/CompanyAuthorizationMethods.scala index 61d528a0..a8425b5e 100644 --- a/app/de/innfactory/bootstrapplay2/common/authorization/CompanyAuthorizationMethods.scala +++ b/app/de/innfactory/bootstrapplay2/common/authorization/CompanyAuthorizationMethods.scala @@ -1,10 +1,10 @@ package de.innfactory.bootstrapplay2.common.authorization -import de.innfactory.bootstrapplay2.actions.RequestWithCompany import com.google.inject.Inject -import de.innfactory.bootstrapplay2.common.results.Results.{ ErrorStatus, Result } +import de.innfactory.bootstrapplay2.common.request.{ RequestContext, RequestContextWithCompany } +import de.innfactory.bootstrapplay2.common.results.Results.{ Result, ResultStatus } import de.innfactory.bootstrapplay2.common.results.errors.Errors.{ BadRequest, Forbidden } -import de.innfactory.bootstrapplay2.common.utils.{ CompanyIdEqualsId, OptionAndCompanyId } import de.innfactory.bootstrapplay2.models.api.Company +import de.innfactory.implicits.BooleanImplicits.EnhancedBoolean import play.api.mvc.BodyParsers import play.api.Configuration @@ -18,30 +18,17 @@ class CompanyAuthorizationMethods[A] @Inject() ( val parser: BodyParsers.Default )(implicit val executionContext: ExecutionContext, configuration: Configuration) { - def canGet(request: RequestWithCompany[A], company: Company): Either[ErrorStatus, Boolean] = - OptionAndCompanyId(request.company, company.id.get) match { - case CompanyIdEqualsId() => Right(true) - case _ => Left(Forbidden()) - } + def canGet(company: Company)(implicit rc: RequestContextWithCompany): Either[ResultStatus, Boolean] = + company.id.get.equals(rc.company.id.get).toResult(Forbidden()) // Everyone can create owners - def canCreate(request: RequestWithCompany[A], company: Company): Result[Boolean] = - request.company match { - case Some(_) => Left(BadRequest()) - case None if company.firebaseUser.getOrElse(List.empty).contains(request.email.getOrElse("empty")) => Right(true) - case _ => Left(Forbidden()) - } + def canCreate(company: Company)(implicit rc: RequestContext): Result[Boolean] = + Right(true) - def canDelete(request: RequestWithCompany[A], company: Company): Result[Boolean] = - OptionAndCompanyId(request.company, company.id.get) match { - case CompanyIdEqualsId() => Right(true) - case _ => Left(Forbidden()) - } + def canDelete(company: Company)(implicit rc: RequestContextWithCompany): Result[Boolean] = + company.id.get.equals(rc.company.id.get).toResult(Forbidden()) - def canUpdate(request: RequestWithCompany[A], company: Company): Result[Boolean] = - OptionAndCompanyId(request.company, company.id.get) match { - case CompanyIdEqualsId() => Right(true) - case _ => Left(Forbidden()) - } + def canUpdate(company: Company)(implicit rc: RequestContextWithCompany): Result[Boolean] = + company.id.get.equals(rc.company.id.get).toResult(Forbidden()) } diff --git a/app/de/innfactory/bootstrapplay2/common/authorization/LocationAuthorizationMethods.scala b/app/de/innfactory/bootstrapplay2/common/authorization/LocationAuthorizationMethods.scala index 279e342a..59f3e2af 100644 --- a/app/de/innfactory/bootstrapplay2/common/authorization/LocationAuthorizationMethods.scala +++ b/app/de/innfactory/bootstrapplay2/common/authorization/LocationAuthorizationMethods.scala @@ -1,9 +1,8 @@ package de.innfactory.bootstrapplay2.common.authorization import java.util.UUID - -import de.innfactory.bootstrapplay2.actions.RequestWithCompany import com.google.inject.Inject +import de.innfactory.bootstrapplay2.common.request.RequestContextWithCompany import de.innfactory.bootstrapplay2.common.results.Results.Result import de.innfactory.bootstrapplay2.common.results.errors.Errors.Forbidden import de.innfactory.bootstrapplay2.models.api.{ Company, Location } @@ -11,12 +10,13 @@ import play.api.mvc.{ BodyParsers, Request } import play.api.Configuration import de.innfactory.bootstrapplay2.common.utils.{ CompanyAndLocation, - CompanyCompanyIdAndOldCompanyId, + CompanyId, + CompanyIdAndOldCompanyId, CompanyIdEqualsId, CompanyIdsAreEqual, - IsCompanyOfLocation, - OptionAndCompanyId + IsCompanyOfLocation } +import de.innfactory.implicits.BooleanImplicits.EnhancedBoolean import scala.concurrent.{ ExecutionContext, Future } @@ -32,39 +32,25 @@ class LocationAuthorizationMethods[A] @Inject() ( firebaseEmailExtractor: FirebaseEmailExtractor[Request[Any]] ) { - def accessGet(request: RequestWithCompany[A], location: Location): Result[Boolean] = { - val companyOption: Option[Company] = request.company - CompanyAndLocation(companyOption, location) match { + def accessGet(location: Location)(implicit rc: RequestContextWithCompany): Result[Boolean] = + CompanyAndLocation(rc.company, location) match { case IsCompanyOfLocation() => Right(true) case _ => Left(Forbidden()) } - } - def accessGetAllByCompany(id: UUID, request: RequestWithCompany[A]): Result[Boolean] = { - val companyOption: Option[Company] = request.company - OptionAndCompanyId(companyOption, id) match { + def accessGetAllByCompany(id: UUID)(implicit rc: RequestContextWithCompany): Result[Boolean] = + CompanyId(rc.company, id) match { case CompanyIdEqualsId() => Right(true) case _ => Left(Forbidden()) } - } - def update(request: RequestWithCompany[A], ownerId: UUID, oldOwnerId: UUID): Result[Boolean] = { - val companyOption: Option[Company] = request.company - CompanyCompanyIdAndOldCompanyId(companyOption, ownerId, oldOwnerId) match { + def update(ownerId: UUID, oldOwnerId: UUID)(implicit rc: RequestContextWithCompany): Result[Boolean] = + CompanyIdAndOldCompanyId(rc.company, ownerId, oldOwnerId) match { case CompanyIdsAreEqual() => Right(true) case _ => Left(Forbidden()) } - } - def createDelete(request: RequestWithCompany[A], locationOwnerId: UUID): Future[Result[Boolean]] = { - val result = for { - company <- request.company - } yield - if (company.id.get.equals(locationOwnerId)) - Right(true) - else - Left(Forbidden()) - Future(result.getOrElse(Left(Forbidden()))) - } + def createDelete(locationOwnerId: UUID)(implicit rc: RequestContextWithCompany): Result[Boolean] = + rc.company.id.get.equals(locationOwnerId).toResult(Forbidden()) } diff --git a/app/de/innfactory/bootstrapplay2/common/daos/BaseSlickDAO.scala b/app/de/innfactory/bootstrapplay2/common/daos/BaseSlickDAO.scala deleted file mode 100644 index 9226adc2..00000000 --- a/app/de/innfactory/bootstrapplay2/common/daos/BaseSlickDAO.scala +++ /dev/null @@ -1,138 +0,0 @@ -package de.innfactory.bootstrapplay2.common.daos - -import java.util.UUID - -import de.innfactory.bootstrapplay2.common.utils.OptionUtils._ -import cats.data.EitherT -import cats.implicits._ -import com.vividsolutions.jts.geom.Geometry -import de.innfactory.common.geo.GeoPointFactory -import de.innfactory.play.db.codegen.XPostgresProfile -import de.innfactory.bootstrapplay2.common.results.Results.{ ErrorStatus, Result } -import de.innfactory.bootstrapplay2.common.results.errors.Errors.{ BadRequest, DatabaseError, NotFound } -import javax.inject.{ Inject, Singleton } -import slick.jdbc.JdbcBackend.Database -import play.api.libs.json.Json -import de.innfactory.bootstrapplay2.models.api.{ ApiBaseModel, Location => LocationObject } -import org.joda.time.DateTime -import slick.basic.BasicStreamingAction -import slick.lifted.{ CompiledFunction, Query, Rep, TableQuery } -import dbdata.Tables -import scala.reflect.runtime.{ universe => ru } -import ru._ -import scala.concurrent.{ ExecutionContext, Future } -import scala.language.implicitConversions -import scala.concurrent.{ ExecutionContext, Future } - -class BaseSlickDAO(db: Database)(implicit ec: ExecutionContext) extends Tables { - - val currentClassForDatabaseError = "BaseSlickDAO" - - override val profile = XPostgresProfile - - import profile.api._ - - def lookupGeneric[R, T]( - queryHeadOption: DBIOAction[Option[R], NoStream, Nothing] - )(implicit rowToObject: R => T): Future[Result[T]] = { - val queryResult: Future[Option[R]] = db.run(queryHeadOption) - queryResult.map { res: Option[R] => - if (res.isDefined) - Right(rowToObject(res.get)) - else - Left( - NotFound() - ) - } - } - - def lookupSequenceGenericRawSequence[R, T]( - querySeq: DBIOAction[Seq[R], NoStream, Nothing] - )(implicit rowToObject: R => T): Future[Seq[T]] = { - val queryResult: Future[Seq[R]] = db.run(querySeq) - queryResult.map { res: Seq[R] => - res.map(rowToObject) - } - } - - def lookupSequenceGeneric[R, T]( - querySeq: DBIOAction[Seq[R], NoStream, Nothing] - )(implicit rowToObject: R => T): Future[Result[Seq[T]]] = { - val queryResult: Future[Seq[R]] = db.run(querySeq) - queryResult.map { res: Seq[R] => - Right(res.map(rowToObject)) - } - } - - def updateGeneric[R, T]( - queryById: DBIOAction[Option[R], NoStream, Nothing], - update: T => DBIOAction[Int, NoStream, Effect.Write], - patch: T => T - )(implicit rowToObject: R => T): Future[Result[T]] = { - val result = for { - lookup <- EitherT(db.run(queryById).map(_.toEither(BadRequest()))) - patchedObject <- EitherT(Future(Option(patch(rowToObject(lookup))).toEither(BadRequest()))) - patchResult <- - EitherT[Future, ErrorStatus, T]( - db.run(update(patchedObject)).map { x => - if (x != 0) Right(patchedObject) - else - Left( - DatabaseError("Could not replace entity", currentClassForDatabaseError, "update", "row not updated") - ) - } - ) - } yield patchResult - result.value - } - - def createGeneric[R, T]( - entity: T, - queryById: DBIOAction[Option[R], NoStream, Nothing], - create: R => DBIOAction[R, NoStream, Effect.Write] - )(implicit rowToObject: R => T, objectToRow: T => R): Future[Result[T]] = { - val entityToSave = objectToRow(entity) - val result = for { - _ <- db.run(queryById).map(_.toInverseEither(BadRequest())) - createdObject <- db.run(create(entityToSave)) - res <- Future( - Option(rowToObject(createdObject)) - .toEither( - DatabaseError("Could not create entity", currentClassForDatabaseError, "create", "row not created") - ) - ) - } yield res - result - } - - def deleteGeneric[R, T]( - queryById: DBIOAction[Option[R], NoStream, Nothing], - delete: DBIOAction[Int, NoStream, Effect.Write] - ): Future[Result[Boolean]] = { - val result = for { - _ <- db.run(queryById).map(_.toEither(BadRequest())) - dbDeleteResult <- db.run(delete).map { x => - if (x != 0) - Right(true) - else - Left( - DatabaseError( - "could not delete entity", - currentClassForDatabaseError, - "delete", - "entity was deleted" - ) - ) - } - } yield dbDeleteResult - result - } - - /** - * Close db - * @return - */ - def close(): Future[Unit] = - Future.successful(db.close()) - -} diff --git a/app/de/innfactory/bootstrapplay2/common/filteroptions/FilterOptionUtils.scala b/app/de/innfactory/bootstrapplay2/common/filteroptions/FilterOptionUtils.scala new file mode 100644 index 00000000..717ecf28 --- /dev/null +++ b/app/de/innfactory/bootstrapplay2/common/filteroptions/FilterOptionUtils.scala @@ -0,0 +1,46 @@ +package de.innfactory.bootstrapplay2.common.filteroptions + +import dbdata.Tables +import de.innfactory.play.slick.enhanced.utils.filteroptions.FilterOptions + +object FilterOptionUtils { + + private def queryStringToOptionsSequence(implicit + queryString: Map[String, Seq[String]] + ): Seq[FilterOptions[Tables.Company, _]] = { + val filterOptionsConfig = new FilterOptionsConfig + filterOptionsConfig.companiesFilterOptions + .map(_.getFromQueryString(queryString)) + .filter(_.isDefined) + .map(_.get) + .filter(_.atLeasOneFilterOptionApplicable) + } + + /** + * Map Query String to Filter Options + * @param queryString + */ + def queryStringToFilterOptions(implicit + queryString: Map[String, Seq[String]] + ): Seq[FilterOptions[Tables.Company, _]] = queryStringToOptionsSequence(queryString) + + def optionStringToFilterOptions(implicit + optionString: Option[String] + ): Seq[FilterOptions[Tables.Company, _]] = + optionString match { + case Some(value) if !value.isBlank => + val query: Map[String, Seq[String]] = value + .split('&') + .map(_.split('=')) + .map(array => array.head -> array.tail.head) + .groupBy(_._1) + .map(e => + e._1 -> e._2 + .map(_._2) + .toSeq + ) + queryStringToFilterOptions(query) + case _ => Seq.empty + } + +} diff --git a/app/de/innfactory/bootstrapplay2/common/filteroptions/FilterOptionsConfig.scala b/app/de/innfactory/bootstrapplay2/common/filteroptions/FilterOptionsConfig.scala new file mode 100644 index 00000000..eba7b065 --- /dev/null +++ b/app/de/innfactory/bootstrapplay2/common/filteroptions/FilterOptionsConfig.scala @@ -0,0 +1,21 @@ +package de.innfactory.bootstrapplay2.common.filteroptions + +import dbdata.Tables +import de.innfactory.play.slick.enhanced.utils.filteroptions.{ + BooleanOption, + FilterOptions, + LongOption, + OptionStringOption +} + +class FilterOptionsConfig { + + val companiesFilterOptions: Seq[FilterOptions[Tables.Company, _]] = Seq( + OptionStringOption(v => v.stringAttribute1, "stringAttribute1"), + OptionStringOption(v => v.stringAttribute2, "stringAttribute2"), + LongOption(v => v.longAttribute1, "longAttribute1"), + BooleanOption(v => v.booleanAttribute, "booleanAttribute"), + LongOption(v => v.longAttribute1, "testAttribute1") + ) + +} diff --git a/app/de/innfactory/bootstrapplay2/common/implicits/EitherTTracingImplicits.scala b/app/de/innfactory/bootstrapplay2/common/implicits/EitherTTracingImplicits.scala new file mode 100644 index 00000000..57bb5b19 --- /dev/null +++ b/app/de/innfactory/bootstrapplay2/common/implicits/EitherTTracingImplicits.scala @@ -0,0 +1,30 @@ +package de.innfactory.bootstrapplay2.common.implicits + +import cats.data.EitherT +import cats.implicits.catsSyntaxEitherId +import de.innfactory.bootstrapplay2.common.request.RequestContext +import de.innfactory.bootstrapplay2.common.results.Results.ResultStatus +import io.opencensus.scala.Tracing.traceWithParent +import io.opencensus.trace.Span + +import scala.concurrent.{ ExecutionContext, Future } + +object EitherTTracingImplicits { + + implicit class EnhancedTracingEitherT[T](eitherT: EitherT[Future, ResultStatus, T]) { + def trace[A]( + string: String + )(implicit rc: RequestContext, ec: ExecutionContext): EitherT[Future, ResultStatus, T] = + EitherT(traceWithParent(string, rc.span) { span => + eitherT.value + }) + } + + def TracedT[A]( + string: String + )(implicit rc: RequestContext, ec: ExecutionContext): EitherT[Future, ResultStatus, Span] = + EitherT(traceWithParent(string, rc.span) { span => + Future(span.asRight[ResultStatus]) + }) + +} diff --git a/app/de/innfactory/bootstrapplay2/common/implicits/FutureTracingImplicits.scala b/app/de/innfactory/bootstrapplay2/common/implicits/FutureTracingImplicits.scala new file mode 100644 index 00000000..86b6d23a --- /dev/null +++ b/app/de/innfactory/bootstrapplay2/common/implicits/FutureTracingImplicits.scala @@ -0,0 +1,30 @@ +package de.innfactory.bootstrapplay2.common.implicits + +import cats.data.EitherT +import cats.implicits.catsSyntaxEitherId +import de.innfactory.bootstrapplay2.common.request.{ RequestContext, TraceContext } +import de.innfactory.bootstrapplay2.common.results.Results.ResultStatus +import io.opencensus.scala.Tracing.traceWithParent +import io.opencensus.trace.Span + +import scala.concurrent.{ ExecutionContext, Future } + +object FutureTracingImplicits { + + implicit class EnhancedFuture[T](future: Future[T]) { + def trace( + string: String + )(implicit tc: TraceContext, ec: ExecutionContext): Future[T] = + traceWithParent(string, tc.span) { _ => + future + } + } + + def TracedT[A]( + string: String + )(implicit tc: TraceContext, ec: ExecutionContext): EitherT[Future, ResultStatus, Span] = + EitherT(traceWithParent(string, tc.span) { span => + Future(span.asRight[ResultStatus]) + }) + +} diff --git a/app/de/innfactory/bootstrapplay2/common/implicits/JWT.scala b/app/de/innfactory/bootstrapplay2/common/implicits/JWT.scala new file mode 100644 index 00000000..b884c6d1 --- /dev/null +++ b/app/de/innfactory/bootstrapplay2/common/implicits/JWT.scala @@ -0,0 +1,16 @@ +package de.innfactory.bootstrapplay2.common.implicits + +import de.innfactory.auth.firebase.validator.JwtToken + +object JWT { + + implicit class JwtTokenGenerator(authHeader: String) { + def toJwtToken: JwtToken = + authHeader match { + case token: String if token.startsWith("Bearer") => + JwtToken(token.splitAt(7)._2) + case token => JwtToken(token) + } + } + +} diff --git a/app/de/innfactory/bootstrapplay2/common/implicits/RequestToRequestContextImplicit.scala b/app/de/innfactory/bootstrapplay2/common/implicits/RequestToRequestContextImplicit.scala new file mode 100644 index 00000000..08a28760 --- /dev/null +++ b/app/de/innfactory/bootstrapplay2/common/implicits/RequestToRequestContextImplicit.scala @@ -0,0 +1,43 @@ +package de.innfactory.bootstrapplay2.common.implicits + +import de.innfactory.bootstrapplay2.common.request.RequestContext +import io.opencensus.scala.Tracing.{ startSpanWithRemoteParent, traceWithParent } +import io.opencensus.trace.{ SpanContext, SpanId, TraceId, TraceOptions, Tracestate } +import play.api.mvc.{ AnyContent, Request } + +import scala.concurrent.{ ExecutionContext, Future } + +object RequestToRequestContextImplicit { + + implicit class EnhancedRequest(request: Request[AnyContent]) { + def toRequestContextAndExecute[T](spanString: String, f: RequestContext => Future[T])(implicit + ec: ExecutionContext + ): Future[T] = { + val headerTracingId = request.headers.get("X-Tracing-ID").get + val spanId = request.headers.get("X-Internal-SpanId").get + val traceId = request.headers.get("X-Internal-TraceId").get + val traceOptions = request.headers.get("X-Internal-TraceOption").get + + val span = startSpanWithRemoteParent( + headerTracingId, + SpanContext.create( + TraceId.fromLowerBase16(traceId), + SpanId.fromLowerBase16(spanId), + TraceOptions.fromLowerBase16(traceOptions, 0), + Tracestate.builder().build() + ) + ) + + traceWithParent(spanString, span) { spanChild => + val rc = new RequestContext(spanChild, request) + val result = f(rc) + result.map { r => + spanChild.end() + span.end() + r + } + } + } + } + +} diff --git a/app/de/innfactory/bootstrapplay2/common/logging/ImplicitLogContext.scala b/app/de/innfactory/bootstrapplay2/common/logging/ImplicitLogContext.scala new file mode 100644 index 00000000..a2fcb661 --- /dev/null +++ b/app/de/innfactory/bootstrapplay2/common/logging/ImplicitLogContext.scala @@ -0,0 +1,12 @@ +package de.innfactory.bootstrapplay2.common.logging + +import de.innfactory.play.logging.logback.LogbackContext + +trait ImplicitLogContext { + implicit val logContext = LogContext(this.getClass.getName) +} + +case class LogContext(className: String) { + def toLogbackContext(traceId: String): LogbackContext = + LogbackContext(className = Some(className), trace = Some(traceId)) +} diff --git a/app/de/innfactory/bootstrapplay2/common/logging/LoggingEnhancer.scala b/app/de/innfactory/bootstrapplay2/common/logging/LoggingEnhancer.scala new file mode 100644 index 00000000..c542a91b --- /dev/null +++ b/app/de/innfactory/bootstrapplay2/common/logging/LoggingEnhancer.scala @@ -0,0 +1,29 @@ +package de.innfactory.bootstrapplay2.common.logging + +import io.opencensus.trace.Span +import org.slf4j.{ Marker, MarkerFactory } +import play.api.Logger + +object LoggingEnhancer { + + private def spanToMarker(span: Span): String = + "tracer=" + span.getContext.getTraceId.toLowerBase16 + + private def getMarker(span: Span): Marker = + MarkerFactory.getMarker(spanToMarker(span)) + + implicit class LoggingEnhancer(logger: Logger) { + def tracedWarn(message: String)(implicit span: Span) = + logger.logger.warn(getMarker(span), message) + + def tracedError(message: String)(implicit span: Span) = + logger.logger.error(getMarker(span), message) + + def tracedInfo(message: String)(implicit span: Span) = + logger.logger.info(getMarker(span), message) + + def tracedDebug(message: String)(implicit span: Span) = + logger.logger.info(getMarker(span), message) + } + +} diff --git a/app/de/innfactory/bootstrapplay2/common/repositories/All.scala b/app/de/innfactory/bootstrapplay2/common/repositories/All.scala new file mode 100644 index 00000000..6c329fa6 --- /dev/null +++ b/app/de/innfactory/bootstrapplay2/common/repositories/All.scala @@ -0,0 +1,10 @@ +package de.innfactory.bootstrapplay2.common.repositories + +import de.innfactory.bootstrapplay2.common.request.RequestContext +import de.innfactory.bootstrapplay2.common.results.Results.Result + +import scala.concurrent.Future + +trait All[RC <: RequestContext, T] { + def all(implicit rc: RC): Future[Result[Seq[T]]] +} diff --git a/app/de/innfactory/bootstrapplay2/common/repositories/Delete.scala b/app/de/innfactory/bootstrapplay2/common/repositories/Delete.scala new file mode 100644 index 00000000..4c340401 --- /dev/null +++ b/app/de/innfactory/bootstrapplay2/common/repositories/Delete.scala @@ -0,0 +1,9 @@ +package de.innfactory.bootstrapplay2.common.repositories + +import de.innfactory.bootstrapplay2.common.request.RequestContext +import de.innfactory.bootstrapplay2.common.results.Results.Result +import scala.concurrent.Future + +trait Delete[ID, RC <: RequestContext, T] { + def delete(id: ID)(implicit rc: RC): Future[Result[T]] +} diff --git a/app/de/innfactory/bootstrapplay2/common/repositories/Lookup.scala b/app/de/innfactory/bootstrapplay2/common/repositories/Lookup.scala new file mode 100644 index 00000000..6bb096f0 --- /dev/null +++ b/app/de/innfactory/bootstrapplay2/common/repositories/Lookup.scala @@ -0,0 +1,9 @@ +package de.innfactory.bootstrapplay2.common.repositories + +import de.innfactory.bootstrapplay2.common.request.RequestContext +import de.innfactory.bootstrapplay2.common.results.Results.Result +import scala.concurrent.Future + +trait Lookup[ID, RC <: RequestContext, T] { + def lookup(id: ID)(implicit rc: RC): Future[Result[T]] +} diff --git a/app/de/innfactory/bootstrapplay2/common/repositories/Patch.scala b/app/de/innfactory/bootstrapplay2/common/repositories/Patch.scala new file mode 100644 index 00000000..93e5fbb8 --- /dev/null +++ b/app/de/innfactory/bootstrapplay2/common/repositories/Patch.scala @@ -0,0 +1,9 @@ +package de.innfactory.bootstrapplay2.common.repositories + +import de.innfactory.bootstrapplay2.common.request.RequestContext +import de.innfactory.bootstrapplay2.common.results.Results.Result +import scala.concurrent.Future + +trait Patch[RC <: RequestContext, T] { + def patch(entity: T)(implicit rc: RC): Future[Result[T]] +} diff --git a/app/de/innfactory/bootstrapplay2/common/repositories/Post.scala b/app/de/innfactory/bootstrapplay2/common/repositories/Post.scala new file mode 100644 index 00000000..a2067b4e --- /dev/null +++ b/app/de/innfactory/bootstrapplay2/common/repositories/Post.scala @@ -0,0 +1,9 @@ +package de.innfactory.bootstrapplay2.common.repositories + +import de.innfactory.bootstrapplay2.common.request.RequestContext +import de.innfactory.bootstrapplay2.common.results.Results.Result +import scala.concurrent.Future + +trait Post[RC <: RequestContext, T] { + def post(entity: T)(implicit rc: RC): Future[Result[T]] +} diff --git a/app/de/innfactory/bootstrapplay2/common/request/RequestContext.scala b/app/de/innfactory/bootstrapplay2/common/request/RequestContext.scala new file mode 100644 index 00000000..94336c69 --- /dev/null +++ b/app/de/innfactory/bootstrapplay2/common/request/RequestContext.scala @@ -0,0 +1,54 @@ +package de.innfactory.bootstrapplay2.common.request + +import de.innfactory.bootstrapplay2.actions.RequestWithCompany +import de.innfactory.bootstrapplay2.common.request.logger.TraceLogger +import de.innfactory.bootstrapplay2.models.api.Company +import de.innfactory.play.tracing.TraceRequest +import io.opencensus.trace.Span +import play.api.mvc.{ AnyContent, Request } + +class TraceContext(traceSpan: Span) { + def span: Span = traceSpan + + private val traceLogger = new TraceLogger(span) + + final def log: TraceLogger = traceLogger +} + +trait BaseRequestContext { + + def request: Request[AnyContent] + +} + +trait RequestContextCompany[COMPANY] { + def company: COMPANY +} + +class RequestContext(rcSpan: Span, rcRequest: Request[AnyContent]) + extends TraceContext(rcSpan) + with BaseRequestContext { + override def request: Request[AnyContent] = rcRequest +} + +case class RequestContextWithCompany( + override val span: Span, + override val request: Request[AnyContent], + company: Company +) extends RequestContext(span, request) + with RequestContextCompany[Company] + +object RequestContextWithCompany { + implicit def toRequestContext(requestContextWithUser: RequestContextWithCompany): RequestContext = + new RequestContext(requestContextWithUser.span, requestContextWithUser.request) +} + +object ReqConverterHelper { + + def requestContext[R[A] <: TraceRequest[AnyContent]](implicit req: R[_]): RequestContext = + new RequestContext(req.traceSpan, req.request) + + def requestContextWithCompany[R[A] <: RequestWithCompany[AnyContent]](implicit req: R[_]): RequestContextWithCompany = + RequestContextWithCompany(req.traceSpan, req.request, req.company) + +} diff --git a/app/de/innfactory/bootstrapplay2/common/request/logger/TraceLogger.scala b/app/de/innfactory/bootstrapplay2/common/request/logger/TraceLogger.scala new file mode 100644 index 00000000..e3f20a12 --- /dev/null +++ b/app/de/innfactory/bootstrapplay2/common/request/logger/TraceLogger.scala @@ -0,0 +1,29 @@ +package de.innfactory.bootstrapplay2.common.request.logger + +import de.innfactory.bootstrapplay2.common.logging.LogContext +import io.opencensus.trace.Span +import org.slf4j.{ Marker, MarkerFactory } +import play.api.Logger +import play.api.libs.json.Json + +class TraceLogger(span: Span) { + private val logger: org.slf4j.Logger = Logger.apply("request-context").logger + + private def getMarker(span: Span)(implicit logContext: LogContext): Marker = + MarkerFactory.getMarker(spanToMarker(span)) + + private def spanToMarker(span: Span)(implicit logContext: LogContext): String = + Json.prettyPrint(Json.toJson(logContext.toLogbackContext(span.getContext.getTraceId.toLowerBase16))) + + def warn(message: String)(implicit logContext: LogContext): Unit = + logger.warn(getMarker(span), message) + + def error(message: String)(implicit logContext: LogContext): Unit = + logger.error(getMarker(span), message) + + def info(message: String)(implicit logContext: LogContext): Unit = + logger.info(getMarker(span), message) + + def debug(message: String)(implicit logContext: LogContext): Unit = + logger.info(getMarker(span), message) +} diff --git a/app/de/innfactory/bootstrapplay2/common/results/ErrorResponse.scala b/app/de/innfactory/bootstrapplay2/common/results/ErrorResponse.scala new file mode 100644 index 00000000..e5756b6f --- /dev/null +++ b/app/de/innfactory/bootstrapplay2/common/results/ErrorResponse.scala @@ -0,0 +1,18 @@ +package de.innfactory.bootstrapplay2.common.results + +import play.api.libs.json.Json +import play.api.mvc.{ AnyContent, Request } + +case class ErrorResponse(message: String) + +object ErrorResponse { + implicit val reads = Json.reads[ErrorResponse] + implicit val writes = Json.writes[ErrorResponse] + + def fromRequest(message: String)(implicit request: Request[AnyContent]) = + Json.toJson(ErrorResponse(message)) + + def fromMessage(message: String) = + Json.toJson(ErrorResponse(message)) + +} diff --git a/app/de/innfactory/bootstrapplay2/common/results/Results.scala b/app/de/innfactory/bootstrapplay2/common/results/Results.scala index 92cce531..97f8c2b3 100644 --- a/app/de/innfactory/bootstrapplay2/common/results/Results.scala +++ b/app/de/innfactory/bootstrapplay2/common/results/Results.scala @@ -1,78 +1,71 @@ package de.innfactory.bootstrapplay2.common.results +import akka.stream.scaladsl.Source import de.innfactory.bootstrapplay2.common.results.errors.Errors._ -import de.innfactory.bootstrapplay2.models.api.ApiBaseModel -import play.api.Logger -import play.api.libs.json.{ JsValue, Json } -import play.api.mvc.{ Results => MvcResults } +import play.api.libs.json.{ Json, Writes } +import play.api.mvc.{ AnyContent, Request, Results => MvcResults } import scala.concurrent.{ ExecutionContext, Future } object Results { - private val logger = Logger("application") + trait ResultStatus - trait ErrorStatus - - /** - * Extend from this error class to have the error logging itself - * @param message - * @param statusCode - * @param errorClass - * @param errorMethod - * @param internalErrorMessage - */ - abstract class SelfLoggingError( - message: String, - statusCode: Int, - errorClass: String, - errorMethod: String, - internalErrorMessage: String - ) extends ErrorStatus { - var currentStackTrace = new Throwable() - logger.error( - s"DatabaseError | message=$message statusCode=$statusCode | Error in class $errorClass in method $errorMethod $internalErrorMessage!", - currentStackTrace - ) + abstract class NotLoggingResult() extends ResultStatus { + def message: String + def additionalInfoToLog: Option[String] + def additionalInfoErrorCode: Option[String] } - abstract class NotLoggingError() extends ErrorStatus - - type Result[T] = Either[ErrorStatus, T] + type Result[T] = Either[ResultStatus, T] - implicit class RichError(value: ErrorStatus)(implicit ec: ExecutionContext) { + implicit class RichError(value: ResultStatus)(implicit ec: ExecutionContext) { def mapToResult: play.api.mvc.Result = value match { - case _: DatabaseError => MvcResults.Status(500)("") - case _: Forbidden => MvcResults.Status(403)("") - case _: BadRequest => MvcResults.Status(400)("") - case _: NotFound => MvcResults.Status(404)("") - case _ => MvcResults.Status(400)("") + case e: DatabaseResult => MvcResults.Status(500)(ErrorResponse.fromMessage(e.message)) + case e: Forbidden => MvcResults.Status(403)(ErrorResponse.fromMessage(e.message)) + case e: BadRequest => MvcResults.Status(400)(ErrorResponse.fromMessage(e.message)) + case e: NotFound => MvcResults.Status(404)(ErrorResponse.fromMessage(e.message)) + case _ => MvcResults.Status(400)("") } } - implicit class SeqApiBaseModel(value: Seq[ApiBaseModel]) { - def toJson: JsValue = Json.toJson(value.map(_.toJson)) - } - - implicit class RichResult(value: Future[Either[ErrorStatus, ApiBaseModel]])(implicit ec: ExecutionContext) { - def completeResult(statusCode: Int = 200): Future[play.api.mvc.Result] = + implicit class RichResult[T](value: Future[Either[ResultStatus, T]])(implicit ec: ExecutionContext) { + def completeResult(statusCode: Int = 200)(implicit writes: Writes[T]): Future[play.api.mvc.Result] = value.map { - case Left(error: ErrorStatus) => error.mapToResult - case Right(value: ApiBaseModel) => MvcResults.Status(statusCode)(value.toJson) + case Left(error: ResultStatus) => error.mapToResult + case Right(value: T) => MvcResults.Status(statusCode)(Json.toJson(value)) } def completeResultWithoutBody(statusCode: Int = 200): Future[play.api.mvc.Result] = value.map { - case Left(error: ErrorStatus) => error.mapToResult - case Right(value: ApiBaseModel) => MvcResults.Status(statusCode)("") + case Left(error: ResultStatus) => error.mapToResult + case Right(_: T) => MvcResults.Status(statusCode)("") + } + } + + implicit class RichSeqResult[T](value: Future[Either[ResultStatus, Seq[T]]])(implicit ec: ExecutionContext) { + def completeResult(implicit writes: Writes[T]): Future[play.api.mvc.Result] = + value.map { + case Left(error: ResultStatus) => error.mapToResult + case Right(value: Seq[T]) => MvcResults.Status(200)(Json.toJson(value)) } } - implicit class RichSeqResult(value: Future[Either[ErrorStatus, Seq[ApiBaseModel]]])(implicit ec: ExecutionContext) { - def completeResult: Future[play.api.mvc.Result] = + implicit class RichSourceResult[T](value: Future[Either[ResultStatus, Source[T, _]]])(implicit + ec: ExecutionContext, + request: Request[AnyContent] + ) { + def completeSourceChunked()(implicit writes: Writes[T]): Future[play.api.mvc.Result] = value.map { - case Left(error: ErrorStatus) => error.mapToResult - case Right(value: Seq[ApiBaseModel]) => MvcResults.Status(200)(value.toJson) + case Left(error: ResultStatus) => error.mapToResult + case Right(value: Source[T, _]) => + MvcResults + .Status(200) + .chunked( + value.map(Json.toJson(_).toString).intersperse("[", ",", "]"), + Some("application/json") + ) + case _ => MvcResults.Status(500)("") } } diff --git a/app/de/innfactory/bootstrapplay2/common/results/errors/Errors.scala b/app/de/innfactory/bootstrapplay2/common/results/errors/Errors.scala index bb4273e9..cde1cd56 100644 --- a/app/de/innfactory/bootstrapplay2/common/results/errors/Errors.scala +++ b/app/de/innfactory/bootstrapplay2/common/results/errors/Errors.scala @@ -1,15 +1,31 @@ package de.innfactory.bootstrapplay2.common.results.errors -import de.innfactory.bootstrapplay2.common.results.Results.{ NotLoggingError, SelfLoggingError } +import de.innfactory.bootstrapplay2.common.results.Results.NotLoggingResult object Errors { - case class DatabaseError(message: String, errorClass: String, errorMethod: String, internalErrorMessage: String) - extends SelfLoggingError(message, 400, errorClass, errorMethod, internalErrorMessage) - case class BadRequest() extends NotLoggingError() + case class DatabaseResult( + message: String = "Entity or request malformed", + additionalInfoToLog: Option[String] = None, + additionalInfoErrorCode: Option[String] = None + ) extends NotLoggingResult() - case class NotFound() extends NotLoggingError() + case class BadRequest( + message: String = "Entity or request malformed", + additionalInfoToLog: Option[String] = None, + additionalInfoErrorCode: Option[String] = None + ) extends NotLoggingResult() - case class Forbidden() extends NotLoggingError() + case class NotFound( + message: String = "Entity not found", + additionalInfoToLog: Option[String] = None, + additionalInfoErrorCode: Option[String] = None + ) extends NotLoggingResult() + + case class Forbidden( + message: String = "Forbidden", + additionalInfoToLog: Option[String] = None, + additionalInfoErrorCode: Option[String] = None + ) extends NotLoggingResult() } diff --git a/app/de/innfactory/bootstrapplay2/common/tracing/Common.scala b/app/de/innfactory/bootstrapplay2/common/tracing/Common.scala new file mode 100644 index 00000000..a0e86a1b --- /dev/null +++ b/app/de/innfactory/bootstrapplay2/common/tracing/Common.scala @@ -0,0 +1,17 @@ +package de.innfactory.bootstrapplay2.common.tracing + +object Common { + val XTRACINGID = "X-Tracing-ID" + val X_INTERNAL_TRACEID = "X-Internal-TraceId" + val X_INTERNAL_SPANID = "X-Internal-SpanId" + val X_INTERNAL_TRACEOPTIONS = "X-Internal-TraceOption" + + object GoogleAttributes { + val HTTP_STATUS_CODE = "http/status_code" + val STATUS = "status" + val HTTP_RESPONSE_SIZE = "/http/response/size" + val HTTP_URL = "/http/url" + val HTTP_HOST = "/http/host" + val HTTP_METHOD = "/http/method" + } +} diff --git a/app/de/innfactory/bootstrapplay2/common/utils/CompanyUtils.scala b/app/de/innfactory/bootstrapplay2/common/utils/CompanyUtils.scala index da97bd74..b3f3ea71 100644 --- a/app/de/innfactory/bootstrapplay2/common/utils/CompanyUtils.scala +++ b/app/de/innfactory/bootstrapplay2/common/utils/CompanyUtils.scala @@ -4,20 +4,20 @@ import java.util.UUID import de.innfactory.bootstrapplay2.models.api.{ Company, Location } -case class CompanyAndLocation(company: Option[Company], location: Location) +case class CompanyAndLocation(company: Company, location: Location) object IsCompanyOfLocation { def unapply(o: CompanyAndLocation): Boolean = - if (o.company.isDefined && o.company.get.id.isDefined && o.company.get.id.get.equals(o.location.company)) true + if (o.company.id.isDefined && o.company.id.get.equals(o.location.company)) true else false } -case class CompanyCompanyIdAndOldCompanyId(company: Option[Company], companyId: UUID, companyIdOld: UUID) +case class CompanyIdAndOldCompanyId(company: Company, companyId: UUID, companyIdOld: UUID) object CompanyIdsAreEqual { - def unapply(o: CompanyCompanyIdAndOldCompanyId): Boolean = + def unapply(o: CompanyIdAndOldCompanyId): Boolean = if ( - o.company.isDefined && o.company.get.id.isDefined && o.company.get.id.get.equals(o.companyId) && o.companyId + o.company.id.isDefined && o.company.id.get.equals(o.companyId) && o.companyId .equals( o.companyIdOld ) @@ -25,12 +25,12 @@ object CompanyIdsAreEqual { else false } -case class OptionAndCompanyId(optionalCompany: Option[Company], companyId: UUID) +case class CompanyId(optionalCompany: Company, companyId: UUID) object CompanyIdEqualsId { - def unapply(o: OptionAndCompanyId): Boolean = + def unapply(o: CompanyId): Boolean = if ( - o.optionalCompany.isDefined && o.optionalCompany.get.id.isDefined && o.optionalCompany.get.id.get + o.optionalCompany.id.isDefined && o.optionalCompany.id.get .equals(o.companyId) ) true else false diff --git a/app/de/innfactory/bootstrapplay2/common/utils/NilUtils.scala b/app/de/innfactory/bootstrapplay2/common/utils/NilUtils.scala deleted file mode 100644 index 6307e94f..00000000 --- a/app/de/innfactory/bootstrapplay2/common/utils/NilUtils.scala +++ /dev/null @@ -1,8 +0,0 @@ -package de.innfactory.bootstrapplay2.common.utils - -import play.api.libs.json.{ Json, Reads, Writes } - -object NilUtils { - implicit val nilReader: Reads[Nil.type] = Json.reads[scala.collection.immutable.Nil.type] - implicit val nilWriter: Writes[Nil.type] = Json.writes[scala.collection.immutable.Nil.type] -} diff --git a/app/de/innfactory/bootstrapplay2/common/utils/OptionUtils.scala b/app/de/innfactory/bootstrapplay2/common/utils/OptionUtils.scala index 1de47b44..2b237d8b 100644 --- a/app/de/innfactory/bootstrapplay2/common/utils/OptionUtils.scala +++ b/app/de/innfactory/bootstrapplay2/common/utils/OptionUtils.scala @@ -1,6 +1,6 @@ package de.innfactory.bootstrapplay2.common.utils -import de.innfactory.bootstrapplay2.common.results.Results.{ ErrorStatus, Result } +import de.innfactory.bootstrapplay2.common.results.Results.{ Result, ResultStatus } object OptionUtils { implicit class EnhancedOption[T](value: Option[T]) { @@ -10,7 +10,7 @@ object OptionUtils { case None => oldOption } - def toEither(leftResult: ErrorStatus): Result[T] = + def toEither(leftResult: ResultStatus): Result[T] = value match { case Some(v) => Right(v) case None => Left(leftResult) diff --git a/app/de/innfactory/bootstrapplay2/common/utils/PagedData.scala b/app/de/innfactory/bootstrapplay2/common/utils/PagedData.scala deleted file mode 100644 index 39447561..00000000 --- a/app/de/innfactory/bootstrapplay2/common/utils/PagedData.scala +++ /dev/null @@ -1,71 +0,0 @@ -package de.innfactory.bootstrapplay2.common.utils - -import play.api.libs.json.{ JsValue, Json } - -case class PagedData[T]( - data: T, - prev: String, - next: String, - count: Long -) - -/** - * Generator for Paging links - * prevGen Takes from, to, count and the api endpoint end generates previous link - * nextGen Takes from, to, count and the api endpoint end generates next link - */ -object PagedGen { - implicit val pagedDataWriter = Json.writes[PagedData[JsValue]] - implicit val pagedDataReader = Json.reads[PagedData[JsValue]] - def prevGen(to: Int, from: Int, count: Int, apiString: String, query: Option[String]): String = - if (count == 0) - "" - else { - val lowerFrom = from - (to - from) - 1 - val lowerTo = to - (to - from) - 1 - if (from > 0) - if (lowerFrom >= 0) { - var api = apiString.concat( - "?startIndex=".concat(lowerFrom.toString.concat("&endIndex=".concat(lowerTo.toString))) - ) - if (query.isDefined) - api = api.concat(query.get) - api - } else { - var api = - apiString.concat("?startIndex=0&endIndex=".concat(lowerTo.toString)) - if (query.isDefined) - api = api.concat(query.get) - api - } - else - "" - } - - def nextGen(to: Int, from: Int, count: Int, apiString: String, query: Option[String]) = - if (count == 0) - "" - else { - val upperTo = to + (to - from) + 1 - val upperFrom = from + (to - from) + 1 - val limit = count - 1 - if (upperTo <= limit) { - var api = apiString.concat( - "?startIndex=".concat(upperFrom.toString.concat("&endIndex=".concat(upperTo.toString))) - ) - if (query.isDefined) - api = api.concat(query.get) - api - } else if (upperFrom > limit) - "" - else { - var api = apiString.concat( - "?startIndex=".concat(upperFrom.toString.concat("&endIndex=".concat(limit.toString))) - ) - if (query.isDefined) - api = api.concat(query.get) - api - } - } - -} diff --git a/app/de/innfactory/bootstrapplay2/controllers/CompaniesController.scala b/app/de/innfactory/bootstrapplay2/controllers/CompaniesController.scala index 383a74db..cb13570e 100644 --- a/app/de/innfactory/bootstrapplay2/controllers/CompaniesController.scala +++ b/app/de/innfactory/bootstrapplay2/controllers/CompaniesController.scala @@ -1,70 +1,75 @@ package de.innfactory.bootstrapplay2.controllers +import akka.stream.scaladsl.Source import java.util.UUID - -import de.innfactory.bootstrapplay2.actions.{ CompanyForUserExtractAction, JwtValidationAction } +import de.innfactory.bootstrapplay2.actions.TracingCompanyAction import cats.data.EitherT import cats.implicits._ -import de.innfactory.bootstrapplay2.common.results.Results.ErrorStatus +import de.innfactory.bootstrapplay2.common.request.ReqConverterHelper.{ requestContext, requestContextWithCompany } +import de.innfactory.bootstrapplay2.common.results.Results.ResultStatus +import de.innfactory.play.tracing.TracingAction import javax.inject.{ Inject, Singleton } import de.innfactory.bootstrapplay2.models.api.Company import play.api.mvc._ +import de.innfactory.bootstrapplay2.models.api.Company._ import de.innfactory.bootstrapplay2.repositories.CompaniesRepository import de.innfactory.bootstrapplay2.common.validators.JsonValidator._ -import de.innfactory.bootstrapplay2.common.utils.NilUtils._ import scala.concurrent.{ ExecutionContext, Future } @Singleton class CompaniesController @Inject() ( cc: ControllerComponents, - jwtValidationAction: JwtValidationAction, - companyForUserExtractAction: CompanyForUserExtractAction, + tracingAction: TracingAction, + tracingCompanyAction: TracingCompanyAction, companiesRepository: CompaniesRepository )(implicit ec: ExecutionContext) extends AbstractController(cc) { def getMe: Action[AnyContent] = - jwtValidationAction.andThen(companyForUserExtractAction).async { implicit request => - val result = request.company match { - case Some(company) => Right(company) - case None => Left(de.innfactory.bootstrapplay2.common.results.errors.Errors.NotFound()) - } - Future(result).completeResult() + tracingCompanyAction("getMe Company").async { implicit request => + Future(request.company.asRight[ResultStatus]).completeResult() } def getSingle( id: String ): Action[AnyContent] = - jwtValidationAction.andThen(companyForUserExtractAction).async { implicit request => - companiesRepository.lookup(UUID.fromString(id), request).completeResult() + tracingCompanyAction("get Company").async { implicit request => + companiesRepository.lookup(UUID.fromString(id))(requestContextWithCompany).completeResult() + } + + def getStreamed = + tracingAction("get companies streamed").async { implicit request => + val result = + EitherT.right[ResultStatus](companiesRepository.streamedAll(requestContext).map(Source.fromPublisher(_))) + result.value.completeSourceChunked() } def patch: Action[AnyContent] = - jwtValidationAction.andThen(companyForUserExtractAction).async { implicit request => - val json = request.body.asJson.get - val stock = json.as[Company] - val result: EitherT[Future, ErrorStatus, Company] = for { + tracingCompanyAction("patch Company").async { implicit request => + val json = request.body.asJson.get + val entity = json.as[Company] + val result: EitherT[Future, ResultStatus, Company] = for { _ <- EitherT(Future(json.validateFor)) - created <- EitherT(companiesRepository.patch(stock, request)) + created <- EitherT(companiesRepository.patch(entity)(requestContextWithCompany)) } yield created result.value.completeResult() } def post: Action[AnyContent] = - jwtValidationAction.andThen(companyForUserExtractAction).async { implicit request => - val json = request.body.asJson.get - val stock = json.as[Company] - val result: EitherT[Future, ErrorStatus, Company] = for { + tracingAction("post Company").async { implicit request => + val json = request.body.asJson.get + val entity = json.as[Company] + val result: EitherT[Future, ResultStatus, Company] = for { _ <- EitherT(Future(json.validateFor[Company])) - created <- EitherT(companiesRepository.post(stock, request)) + created <- EitherT(companiesRepository.post(entity)(requestContext)) } yield created result.value.completeResult() } def delete(id: String): Action[AnyContent] = - jwtValidationAction.andThen(companyForUserExtractAction).async { implicit request => - companiesRepository.delete(UUID.fromString(id), request).completeResultWithoutBody(204) + tracingCompanyAction("delete Company").async { implicit request => + companiesRepository.delete(UUID.fromString(id))(requestContextWithCompany).completeResultWithoutBody(204) } } diff --git a/app/de/innfactory/bootstrapplay2/controllers/HealthController.scala b/app/de/innfactory/bootstrapplay2/controllers/HealthController.scala index 88847ae8..a0bdf271 100644 --- a/app/de/innfactory/bootstrapplay2/controllers/HealthController.scala +++ b/app/de/innfactory/bootstrapplay2/controllers/HealthController.scala @@ -1,16 +1,24 @@ package de.innfactory.bootstrapplay2.controllers +import de.innfactory.bootstrapplay2.db.DatabaseHealthSocket + import javax.inject.{ Inject, Singleton } import play.api.mvc._ + import scala.concurrent.ExecutionContext @Singleton class HealthController @Inject() ( - cc: ControllerComponents + cc: ControllerComponents, + databaseHealthSocket: DatabaseHealthSocket )(implicit ec: ExecutionContext) extends AbstractController(cc) { def ping: Action[AnyContent] = Action { - Ok("Ok") + if (databaseHealthSocket.isConnectionOpen) + Ok("Ok") + else + InternalServerError("Database Connection Lost") + } } diff --git a/app/de/innfactory/bootstrapplay2/controllers/LocationsController.scala b/app/de/innfactory/bootstrapplay2/controllers/LocationsController.scala index cbf0005a..2cb35ad1 100644 --- a/app/de/innfactory/bootstrapplay2/controllers/LocationsController.scala +++ b/app/de/innfactory/bootstrapplay2/controllers/LocationsController.scala @@ -5,29 +5,29 @@ import javax.inject.{ Inject, Singleton } import de.innfactory.bootstrapplay2.models.api.Location import play.api.libs.json._ import play.api.mvc._ -import de.innfactory.bootstrapplay2.actions._ -import de.innfactory.bootstrapplay2.repositories.LocationRepository import cats.data.EitherT -import de.innfactory.bootstrapplay2.common.results.Results.ErrorStatus +import de.innfactory.bootstrapplay2.common.results.Results.ResultStatus import de.innfactory.bootstrapplay2.common.validators.JsonValidator._ -import de.innfactory.bootstrapplay2.models.api.Location.reads import cats.implicits._ +import de.innfactory.bootstrapplay2.actions.TracingCompanyAction +import de.innfactory.bootstrapplay2.common.request.ReqConverterHelper.requestContextWithCompany +import de.innfactory.bootstrapplay2.repositories.LocationRepository + import scala.concurrent.{ ExecutionContext, Future } @Singleton class LocationsController @Inject() ( cc: ControllerComponents, locationRepository: LocationRepository, - jwtValidationAction: JwtValidationAction, - companyForUserExtractAction: CompanyForUserExtractAction + tracingCompanyAction: TracingCompanyAction )(implicit ec: ExecutionContext) extends AbstractController(cc) { def getSingle( id: Long ): Action[AnyContent] = - jwtValidationAction.andThen(companyForUserExtractAction).async { implicit request => - locationRepository.lookup(id, request).completeResult() + tracingCompanyAction("get Single Location").async { implicit request => + locationRepository.lookup(id)(requestContextWithCompany).completeResult() } def getByDistance( @@ -35,43 +35,45 @@ class LocationsController @Inject() ( lat: Double, lon: Double ): Action[AnyContent] = - jwtValidationAction.andThen(companyForUserExtractAction).async { implicit request => - locationRepository.getByDistance(distance, lat, lon, request).completeResult + tracingCompanyAction("Get Locations By Distance").async { implicit request => + locationRepository.getByDistance(distance, lat, lon)(requestContextWithCompany).completeResult } def getByCompany( companyId: String ): Action[AnyContent] = - jwtValidationAction.andThen(companyForUserExtractAction).async { implicit request => - locationRepository.lookupByCompany(UUID.fromString(companyId), request).completeResult + tracingCompanyAction("Get Locations By Company").async { implicit request => + locationRepository.lookupByCompany(UUID.fromString(companyId))(requestContextWithCompany).completeResult } def patch: Action[AnyContent] = - jwtValidationAction.andThen(companyForUserExtractAction).async { implicit request => - val json: JsValue = request.body.asJson.get // Get the request body as json - val stock = json.as[Location] // Json to Location Object - val result: EitherT[Future, ErrorStatus, Location] = for { - _ <- EitherT(Future(json.validateFor)) // Validate Json - updated <- EitherT(locationRepository.patch(stock, request)) // call locationRepository to patch the object + tracingCompanyAction("Patch Location").async { implicit request => + val json: JsValue = request.body.asJson.get // Get the request body as json + val entity = json.as[Location] // Json to Location Object + val result: EitherT[Future, ResultStatus, Location] = for { + _ <- EitherT(Future(json.validateFor[Location])) // Validate Json + updated <- EitherT( + locationRepository.patch(entity)(requestContextWithCompany) + ) // call locationRepository to patch the object } yield updated result.value .completeResult() // get .value of EitherT and then .completeResult (implicit on Future[Either[ErrorStatus, ApiBaseModel]]) } def post: Action[AnyContent] = - jwtValidationAction.andThen(companyForUserExtractAction).async { implicit request => - val json = request.body.asJson.get - val stock = json.as[Location] - val result: EitherT[Future, ErrorStatus, Location] = for { - _ <- EitherT(Future(json.validateFor)) - created <- EitherT(locationRepository.post(stock, request)) + tracingCompanyAction("Post Location").async { implicit request => + val json = request.body.asJson.get + val entity = json.as[Location] + val result: EitherT[Future, ResultStatus, Location] = for { + _ <- EitherT(Future(json.validateFor[Location])) + created <- EitherT(locationRepository.post(entity)(requestContextWithCompany)) } yield created result.value.completeResult() } def delete(id: Long): Action[AnyContent] = - jwtValidationAction.andThen(companyForUserExtractAction).async { implicit request => - locationRepository.delete(id, request).completeResultWithoutBody(204) + tracingCompanyAction("Delete Location").async { implicit request => + locationRepository.delete(id)(requestContextWithCompany).completeResultWithoutBody(204) } } diff --git a/app/de/innfactory/bootstrapplay2/db/BaseSlickDAO.scala b/app/de/innfactory/bootstrapplay2/db/BaseSlickDAO.scala new file mode 100644 index 00000000..7762df50 --- /dev/null +++ b/app/de/innfactory/bootstrapplay2/db/BaseSlickDAO.scala @@ -0,0 +1,204 @@ +package de.innfactory.bootstrapplay2.db + +import de.innfactory.bootstrapplay2.common.utils.OptionUtils._ +import cats.data.EitherT +import de.innfactory.play.db.codegen.XPostgresProfile +import de.innfactory.bootstrapplay2.common.results.Results.{ Result, ResultStatus } +import de.innfactory.bootstrapplay2.common.results.errors.Errors.{ BadRequest, DatabaseResult, NotFound } +import slick.jdbc.JdbcBackend.Database +import dbdata.Tables +import de.innfactory.bootstrapplay2.common.implicits.FutureTracingImplicits.EnhancedFuture +import de.innfactory.bootstrapplay2.common.logging.ImplicitLogContext +import de.innfactory.bootstrapplay2.common.request.TraceContext +import slick.dbio.{ DBIOAction, Effect, NoStream } + +import scala.language.implicitConversions +import scala.concurrent.{ ExecutionContext, Future } + +class BaseSlickDAO(db: Database)(implicit ec: ExecutionContext) extends Tables with ImplicitLogContext { + + override val profile = XPostgresProfile + + def lookupGeneric[R, T]( + queryHeadOption: DBIOAction[Option[R], NoStream, Nothing] + )(implicit rowToObject: R => T, tc: TraceContext): Future[Result[T]] = { + val queryResult: Future[Option[R]] = db.run(queryHeadOption).trace("lookupGeneric") + queryResult.map { res: Option[R] => + if (res.isDefined) + Right(rowToObject(res.get)) + else + Left( + NotFound() + ) + } + } + + def lookupGenericOption[R, T]( + queryHeadOption: DBIOAction[Option[R], NoStream, Nothing] + )(implicit rowToObject: R => T, tc: TraceContext): Future[Option[T]] = { + val queryResult: Future[Option[R]] = db.run(queryHeadOption).trace("lookupGenericOption") + queryResult.map { res: Option[R] => + if (res.isDefined) + Some(rowToObject(res.get)) + else + None + } + } + + def countGeneric[R, T]( + querySeq: DBIOAction[Seq[R], NoStream, Nothing] + )(implicit tc: TraceContext): Future[Result[Int]] = { + val queryResult: Future[Seq[R]] = db.run(querySeq).trace("countGeneric") + queryResult.map(seq => Right(seq.length)) + } + + def lookupSequenceGeneric[R, T]( + querySeq: DBIOAction[Seq[R], NoStream, Nothing] + )(implicit rowToObject: R => T, tc: TraceContext): Future[Result[Seq[T]]] = { + val queryResult: Future[Seq[R]] = db.run(querySeq).trace("lookupSequenceGeneric") + queryResult.map { res: Seq[R] => + Right(res.map(rowToObject)) + } + } + + def lookupSequenceGenericRawSequence[R, T]( + querySeq: DBIOAction[Seq[R], NoStream, Nothing] + )(implicit rowToObject: R => T, tc: TraceContext): Future[Seq[T]] = { + val queryResult: Future[Seq[R]] = db.run(querySeq).trace("lookupSequenceGenericRawSequence") + queryResult.map { res: Seq[R] => + res.map(rowToObject) + } + } + + def lookupSequenceGeneric[R, T]( + querySeq: DBIOAction[Seq[R], NoStream, Nothing], + count: Int + )(implicit rowToObject: R => T, tc: TraceContext): Future[Result[Seq[T]]] = { + val queryResult: Future[Seq[R]] = db.run(querySeq).trace("lookupSequenceGeneric") + queryResult.map { res: Seq[R] => + Right(res.takeRight(count).map(rowToObject)) + } + } + + def lookupSequenceGeneric[R, T]( + querySeq: DBIOAction[Seq[R], NoStream, Nothing], + from: Int, + to: Int + )(implicit rowToObject: R => T, tc: TraceContext): Future[Result[Seq[T]]] = { + val queryResult: Future[Seq[R]] = db.run(querySeq).trace("lookupSequenceGeneric") + queryResult.map { res: Seq[R] => + Right(res.slice(from, to + 1).map(rowToObject)) + } + } + + def lookupSequenceGeneric[R, T, X, Z]( + querySeq: DBIOAction[Seq[R], NoStream, Nothing], + mapping: T => X, + filter: X => Boolean, + afterFilterMapping: X => Z + )(implicit rowToObject: R => T, tc: TraceContext): Future[Result[Seq[Z]]] = { + val queryResult: Future[Seq[R]] = db.run(querySeq).trace("lookupSequenceGeneric") + queryResult.map { res: Seq[R] => + Right(res.map(rowToObject).map(mapping).filter(filter).map(afterFilterMapping)) + } + } + + def lookupSequenceGeneric[R, T, Z]( + querySeq: DBIOAction[Seq[R], NoStream, Nothing], + sequenceMapping: Seq[T] => Z + )(implicit rowToObject: R => T, tc: TraceContext): Future[Result[Z]] = { + val queryResult: Future[Seq[R]] = db.run(querySeq).trace("lookupSequenceGeneric") + queryResult.map { res: Seq[R] => + val sequence = res.map(rowToObject) + Right(sequenceMapping(sequence)) + } + } + + def updateGeneric[R, T]( + queryById: DBIOAction[Option[R], NoStream, Nothing], + update: T => DBIOAction[Int, NoStream, Effect.Write], + patch: T => T + )(implicit rowToObject: R => T, tc: TraceContext): Future[Result[T]] = { + val result = for { + lookup <- EitherT(db.run(queryById).map(_.toEither(BadRequest())).trace("updateGeneric lookup")) + patchedObject <- EitherT(Future(Option(patch(rowToObject(lookup))).toEither(BadRequest()))) + patchResult <- + EitherT[Future, ResultStatus, T]( + db.run(update(patchedObject)) + .map { x => + if (x != 0) Right(patchedObject) + else { + tc.log.error("Database Result Updating entity") + Left( + DatabaseResult("Could not update entity") + ) + } + } + .trace("updateGeneric update") + ) + } yield patchResult + result.value + } + + def createGeneric[R, T]( + entity: T, + queryById: DBIOAction[Option[R], NoStream, Nothing], + create: R => DBIOAction[R, NoStream, Effect.Write] + )(implicit rowToObject: R => T, objectToRow: T => R, tc: TraceContext): Future[Result[T]] = { + val entityToSave = objectToRow(entity) + val result = for { + _ <- db.run(queryById).map(_.toInverseEither(BadRequest())).trace("createGeneric lookup") + createdObject <- db.run(create(entityToSave)).trace("createGeneric create") + res <- Future( + Right(rowToObject(createdObject)) + ) + } yield res + result + } + + def createGeneric[R, T]( + entity: T, + create: R => DBIOAction[R, NoStream, Effect.Write] + )(implicit rowToObject: R => T, objectToRow: T => R, tc: TraceContext): Future[Result[T]] = { + val entityToSave = objectToRow(entity) + val result = for { + createdObject <- db.run(create(entityToSave)).trace("createGeneric create") + res <- Future( + Right(rowToObject(createdObject)) + ) + } yield res + result + } + + def deleteGeneric[R, T]( + queryById: DBIOAction[Option[R], NoStream, Nothing], + delete: DBIOAction[Int, NoStream, Effect.Write] + )(implicit tc: TraceContext): Future[Result[Boolean]] = { + val result = for { + _ <- db.run(queryById).map(_.toEither(BadRequest())).trace("deleteGeneric lookup") + dbDeleteResult <- db.run(delete) + .map { x => + if (x != 0) + Right(true) + else { + tc.log.error("Database Error deleting entity") + Left( + DatabaseResult( + "could not delete entity" + ) + ) + } + } + .trace("deleteGeneric delete") + } yield dbDeleteResult + result + } + + /** + * Close dao + * @return + */ + def close(): Future[Unit] = + Future.successful(db.close()) + +} diff --git a/app/de/innfactory/bootstrapplay2/db/CompaniesDAO.scala b/app/de/innfactory/bootstrapplay2/db/CompaniesDAO.scala index f37a25df..a3233017 100644 --- a/app/de/innfactory/bootstrapplay2/db/CompaniesDAO.scala +++ b/app/de/innfactory/bootstrapplay2/db/CompaniesDAO.scala @@ -1,12 +1,13 @@ package de.innfactory.bootstrapplay2.db import java.util.UUID - import com.google.inject.ImplementedBy -import de.innfactory.bootstrapplay2.common.daos.BaseSlickDAO +import de.innfactory.bootstrapplay2.common.request.{ RequestContext, TraceContext } +import de.innfactory.bootstrapplay2.db.BaseSlickDAO import de.innfactory.bootstrapplay2.common.results.Results.Result -import de.innfactory.bootstrapplay2.common.results.errors.Errors.{ DatabaseError, NotFound } +import de.innfactory.bootstrapplay2.common.results.errors.Errors.{ DatabaseResult, NotFound } import de.innfactory.play.db.codegen.XPostgresProfile + import javax.inject.{ Inject, Singleton } import slick.jdbc.JdbcBackend.Database import play.api.libs.json.Json @@ -14,84 +15,96 @@ import de.innfactory.bootstrapplay2.models.api.{ Company => CompanyObject } import org.joda.time.DateTime import de.innfactory.bootstrapplay2.models.api.Company.patch import de.innfactory.bootstrapplay2.repositories.LocationRepositoryImpl +import de.innfactory.play.slick.enhanced.utils.filteroptions.FilterOptions +import de.innfactory.play.slick.enhanced.query.EnhancedQuery._ +import dbdata.Tables +import de.innfactory.bootstrapplay2.common.logging.ImplicitLogContext +import slick.basic.DatabasePublisher +import slick.dbio.{ DBIOAction, NoStream } +import slick.jdbc.{ ResultSetConcurrency, ResultSetType } import scala.concurrent.{ ExecutionContext, Future } import scala.language.implicitConversions -/** - * An implementation dependent DAO. This could be implemented by Slick, Cassandra, or a REST API. - */ @ImplementedBy(classOf[SlickCompaniesSlickDAO]) trait CompaniesDAO { - def lookup(id: UUID): Future[Result[CompanyObject]] + def lookup(id: UUID)(implicit tc: TraceContext): Future[Result[CompanyObject]] + + def all(implicit tc: TraceContext): Future[Seq[CompanyObject]] - def all(): Future[Seq[CompanyObject]] + def allWithFilter(filter: Seq[FilterOptions[Tables.Company, _]])(implicit + tc: TraceContext + ): Future[Seq[CompanyObject]] - def internal_lookupByEmail(email: String): Future[Option[CompanyObject]] + def streamedAll(implicit tc: TraceContext): DatabasePublisher[CompanyObject] - def create(CompanyObject: CompanyObject): Future[Result[CompanyObject]] + def internal_lookupByEmail(email: String)(implicit tc: TraceContext): Future[Option[CompanyObject]] - def update(CompanyObject: CompanyObject): Future[Result[CompanyObject]] + def create(CompanyObject: CompanyObject)(implicit tc: TraceContext): Future[Result[CompanyObject]] - def delete(id: UUID): Future[Result[Boolean]] + def update(CompanyObject: CompanyObject)(implicit tc: TraceContext): Future[Result[CompanyObject]] + + def delete(id: UUID)(implicit tc: TraceContext): Future[Result[Boolean]] def close(): Future[Unit] } -/** - * A CompanyObject DAO implemented with Slick, leveraging Slick code gen. - * - * Note that you must run "flyway/flywayMigrate" before "compile" here. - * - * @param db the slick database that this CompanyObject DAO is using internally, bound through Module. - * @param ec a CPU bound execution context. Slick manages blocking JDBC calls with its - * own internal thread pool, so Play's default execution context is fine here. - */ @Singleton class SlickCompaniesSlickDAO @Inject() (db: Database)(implicit ec: ExecutionContext) extends BaseSlickDAO(db) - with CompaniesDAO { - - // Class Name for identification in Database Errors - override val currentClassForDatabaseError = "SlickCompaniesDAO" + with CompaniesDAO + with ImplicitLogContext { override val profile = XPostgresProfile import profile.api._ /* - - - Compiled Queries - - - */ - private val queryById = Compiled((id: Rep[UUID]) => Company.filter(_.id === id)) + private val queryById = Compiled((id: Rep[UUID]) => Tables.Company.filter(_.id === id)) private val queryByEmail = Compiled((email: Rep[String]) => - Company.filter { cs => + Tables.Company.filter { cs => // email === firebaseUser.any is like calling .includes(email) email === cs.firebaseUser.any } ) - /** - * Lookup single object - * @param id - * @return - */ - def lookup(id: UUID): Future[Result[CompanyObject]] = - lookupGeneric[CompanyRow, CompanyObject]( + def lookup(id: UUID)(implicit tc: TraceContext): Future[Result[CompanyObject]] = + lookupGeneric( queryById(id).result.headOption ) - def all(): Future[Seq[CompanyObject]] = + def streamedAll(implicit tc: TraceContext): DatabasePublisher[CompanyObject] = + db.stream( + Tables.Company.result + .withStatementParameters( + rsType = ResultSetType.ForwardOnly, + rsConcurrency = ResultSetConcurrency.ReadOnly, + fetchSize = 1000 + ) + .transactionally + ).mapResult(companyRowToCompanyObject) + + def all(implicit tc: TraceContext): Future[Seq[CompanyObject]] = lookupSequenceGenericRawSequence( - Company.result + Tables.Company.result ) - /** - * Lookup Company by Email - * @param email - * @return - */ - def internal_lookupByEmail(email: String): Future[Option[CompanyObject]] = { - val f: Future[Option[CompanyRow]] = + def allWithFilter(filter: Seq[FilterOptions[Tables.Company, _]])(implicit + tc: TraceContext + ): Future[Seq[CompanyObject]] = { + println(filter) + lookupSequenceGenericRawSequence( + queryFromFiltersSeq(filter).result + )(c => companyRowToCompanyObject(c.copy()), tc) + } + + private def queryFromFiltersSeq(filter: Seq[FilterOptions[Tables.Company, _]]) = + Compiled(Tables.Company.filterOptions(filter)) + + def internal_lookupByEmail(email: String)(implicit tc: TraceContext): Future[Option[CompanyObject]] = { + val f: Future[Option[Tables.CompanyRow]] = db.run(queryByEmail(email).result.headOption) f.map { case Some(row) => @@ -101,58 +114,51 @@ class SlickCompaniesSlickDAO @Inject() (db: Database)(implicit ec: ExecutionCont } } - /** - * Patch object - * @param companyObject - * @return - */ - def update(companyObject: CompanyObject): Future[Result[CompanyObject]] = - updateGeneric[CompanyRow, CompanyObject]( + def update(companyObject: CompanyObject)(implicit tc: TraceContext): Future[Result[CompanyObject]] = + updateGeneric( queryById(companyObject.id.getOrElse(UUID.randomUUID())).result.headOption, (toPatch: CompanyObject) => queryById(companyObject.id.getOrElse(UUID.randomUUID())).update(companyObjectToCompanyRow(toPatch)), (old: CompanyObject) => patch(companyObject, old) ) - /** - * Delete Object - * @param id - * @return - */ - def delete(id: UUID): Future[Result[Boolean]] = - deleteGeneric[CompanyRow, CompanyObject]( + def delete(id: UUID)(implicit tc: TraceContext): Future[Result[Boolean]] = + deleteGeneric( queryById(id).result.headOption, queryById(id).delete ) - /** - * Create new Object - * @param companyObject - * @return - */ - def create(companyObject: CompanyObject): Future[Result[CompanyObject]] = - createGeneric[CompanyRow, CompanyObject]( + def create(companyObject: CompanyObject)(implicit tc: TraceContext): Future[Result[CompanyObject]] = + createGeneric( companyObject, queryById(companyObject.id.getOrElse(UUID.randomUUID())).result.headOption, - (entityToSave: CompanyRow) => (Company returning Company) += entityToSave + (entityToSave: Tables.CompanyRow) => (Tables.Company returning Tables.Company) += entityToSave ) /* - - - Mapper Functions - - - */ - implicit private def companyObjectToCompanyRow(CompanyObject: CompanyObject): CompanyRow = - CompanyRow( - id = CompanyObject.id.getOrElse(UUID.randomUUID()), - firebaseUser = CompanyObject.firebaseUser.getOrElse(List.empty[String]), - settings = CompanyObject.settings.getOrElse(Json.parse("{}")), - created = CompanyObject.created.getOrElse(DateTime.now), - updated = CompanyObject.updated.getOrElse(DateTime.now) + implicit private def companyObjectToCompanyRow(companyObject: CompanyObject): Tables.CompanyRow = + Tables.CompanyRow( + id = companyObject.id.getOrElse(UUID.randomUUID()), + firebaseUser = companyObject.firebaseUser.getOrElse(List.empty[String]), + settings = companyObject.settings.getOrElse(Json.parse("{}")), + stringAttribute1 = companyObject.stringAttribute1, + stringAttribute2 = companyObject.stringAttribute2, + longAttribute1 = companyObject.longAttribute1, + booleanAttribute = companyObject.booleanAttribute, + created = companyObject.created.getOrElse(DateTime.now), + updated = companyObject.updated.getOrElse(DateTime.now) ) - implicit private def companyRowToCompanyObject(companyRow: CompanyRow): CompanyObject = + implicit private def companyRowToCompanyObject(companyRow: Tables.CompanyRow): CompanyObject = CompanyObject( id = Some(companyRow.id), firebaseUser = Some(companyRow.firebaseUser), settings = Some(companyRow.settings), + stringAttribute1 = companyRow.stringAttribute1, + stringAttribute2 = companyRow.stringAttribute2, + longAttribute1 = companyRow.longAttribute1, + booleanAttribute = companyRow.booleanAttribute, created = Some(companyRow.created), updated = Some(companyRow.updated) ) diff --git a/app/de/innfactory/bootstrapplay2/db/DatabaseHealthSocket.scala b/app/de/innfactory/bootstrapplay2/db/DatabaseHealthSocket.scala new file mode 100644 index 00000000..21e8e1c2 --- /dev/null +++ b/app/de/innfactory/bootstrapplay2/db/DatabaseHealthSocket.scala @@ -0,0 +1,19 @@ +package de.innfactory.bootstrapplay2.db + +import play.api.inject.ApplicationLifecycle +import slick.jdbc.JdbcBackend.Database + +import java.sql.Connection +import javax.inject.{ Inject, Singleton } +import scala.concurrent.Future + +@Singleton +class DatabaseHealthSocket @Inject() (db: Database, lifecycle: ApplicationLifecycle) { + private val connection: Connection = db.source.createConnection() + + def isConnectionOpen: Boolean = connection.getSchema.nonEmpty + + lifecycle.addStopHook { () => + Future.successful(connection.close()) + } +} diff --git a/app/de/innfactory/bootstrapplay2/db/LocationsDAO.scala b/app/de/innfactory/bootstrapplay2/db/LocationsDAO.scala index 2d8902f4..8d056eca 100644 --- a/app/de/innfactory/bootstrapplay2/db/LocationsDAO.scala +++ b/app/de/innfactory/bootstrapplay2/db/LocationsDAO.scala @@ -1,14 +1,14 @@ package de.innfactory.bootstrapplay2.db import java.util.UUID - import com.google.inject.ImplementedBy import com.vividsolutions.jts.geom.Geometry import de.innfactory.common.geo.GeoPointFactory -import de.innfactory.bootstrapplay2.common.daos.BaseSlickDAO +import de.innfactory.bootstrapplay2.common.request.{ RequestContext, TraceContext } import de.innfactory.bootstrapplay2.common.results.Results.Result -import de.innfactory.bootstrapplay2.common.results.errors.Errors.{ DatabaseError, NotFound } +import de.innfactory.bootstrapplay2.common.results.errors.Errors.{ DatabaseResult, NotFound } import de.innfactory.play.db.codegen.XPostgresProfile + import javax.inject.{ Inject, Singleton } import slick.jdbc.JdbcBackend.Database import play.api.libs.json.Json @@ -19,50 +19,35 @@ import de.innfactory.bootstrapplay2.models.api.Location.patch import scala.concurrent.{ ExecutionContext, Future } import scala.language.implicitConversions -/** - * An implementation dependent DAO. This could be implemented by Slick, Cassandra, or a REST API. - */ @ImplementedBy(classOf[SlickLocationsDAO]) trait LocationsDAO { - def lookup(id: Long): Future[Result[LocationObject]] + def lookup(id: Long)(implicit tc: TraceContext): Future[Result[LocationObject]] def lookupByCompany( companyId: UUID - ): Future[Result[Seq[LocationObject]]] + )(implicit tc: TraceContext): Future[Result[Seq[LocationObject]]] def allFromDistanceByCompany( companyId: UUID, point: Geometry, distance: Long - ): Future[Result[Seq[LocationObject]]] + )(implicit tc: TraceContext): Future[Result[Seq[LocationObject]]] - def create(locationObject: LocationObject): Future[Result[LocationObject]] + def create(locationObject: LocationObject)(implicit tc: TraceContext): Future[Result[LocationObject]] - def update(locationObject: LocationObject): Future[Result[LocationObject]] + def update(locationObject: LocationObject)(implicit tc: TraceContext): Future[Result[LocationObject]] - def delete(id: Long): Future[Result[Boolean]] + def delete(id: Long)(implicit tc: TraceContext): Future[Result[Boolean]] def close(): Future[Unit] } -/** - * A locationObject DAO implemented with Slick, leveraging Slick code gen. - * - * Note that you must run "flyway/flywayMigrate" before "compile" here. - * - * @param db the slick database that this locationObject DAO is using internally, bound through Module. - * @param ec a CPU bound execution context. Slick manages blocking JDBC calls with its - * own internal thread pool, so Play's default execution context is fine here. - */ @Singleton class SlickLocationsDAO @Inject() (db: Database)(implicit ec: ExecutionContext) extends BaseSlickDAO(db) with LocationsDAO { - // Class Name for identification in Database Errors - override val currentClassForDatabaseError = "SlickLocationsDAO" - override val profile = XPostgresProfile import profile.api._ @@ -83,22 +68,12 @@ class SlickLocationsDAO @Inject() (db: Database)(implicit ec: ExecutionContext) .filter(_._2 <= maxDistance) ) - /** - * Lookup single object - * @param id - * @return - */ - def lookup(id: Long): Future[Result[LocationObject]] = + def lookup(id: Long)(implicit tc: TraceContext): Future[Result[LocationObject]] = lookupGeneric[LocationRow, LocationObject]( queryById(id).result.headOption ) - /** - * Lookup single object _internal use only - * @param id - * @return - */ - def _internal_lookup(id: Long): Future[Option[LocationObject]] = + def _internal_lookup(id: Long)(implicit tc: TraceContext): Future[Option[LocationObject]] = db.run(queryById(id).result.headOption).map { case Some(row) => Some(locationRowToLocation(row)) @@ -106,42 +81,25 @@ class SlickLocationsDAO @Inject() (db: Database)(implicit ec: ExecutionContext) None } - /** - * Lookup by Company - * @param companyId - * @return - */ def lookupByCompany( companyId: UUID - ): Future[Result[Seq[LocationObject]]] = + )(implicit tc: TraceContext): Future[Result[Seq[LocationObject]]] = lookupSequenceGeneric[LocationRow, LocationObject]( queryByCompany(companyId).result ) - /** - * Query All by distance from to index - * - * @param point - * @param distance - * @return - */ def allFromDistanceByCompany( companyId: UUID, point: Geometry, distance: Long - ): Future[Result[Seq[LocationObject]]] = + )(implicit tc: TraceContext): Future[Result[Seq[LocationObject]]] = db.run(querySortedWithDistanceFilterMaxDistance(point, distance.toFloat, companyId).result).map { seq => Right( seq.map(x => locationRowToLocationWithDistance(x._1, x._2)) ) } - /** - * Patch object - * @param locationObject - * @return - */ - def update(locationObject: LocationObject): Future[Result[LocationObject]] = + def update(locationObject: LocationObject)(implicit tc: TraceContext): Future[Result[LocationObject]] = updateGeneric[LocationRow, LocationObject]( queryById(locationObject.id.getOrElse(0)).result.headOption, (toPatch: LocationObject) => @@ -149,23 +107,13 @@ class SlickLocationsDAO @Inject() (db: Database)(implicit ec: ExecutionContext) (old: LocationObject) => patch(locationObject, old) ) - /** - * Delete Object - * @param id - * @return - */ - def delete(id: Long): Future[Result[Boolean]] = + def delete(id: Long)(implicit tc: TraceContext): Future[Result[Boolean]] = deleteGeneric[LocationRow, LocationObject]( queryById(id).result.headOption, queryById(id).delete ) - /** - * Create new Object - * @param locationObject - * @return - */ - def create(locationObject: LocationObject): Future[Result[LocationObject]] = + def create(locationObject: LocationObject)(implicit tc: TraceContext): Future[Result[LocationObject]] = createGeneric[LocationRow, LocationObject]( locationObject, queryById(locationObject.id.getOrElse(0)).result.headOption, diff --git a/app/de/innfactory/bootstrapplay2/filters/TracingFilter.scala b/app/de/innfactory/bootstrapplay2/filters/TracingFilter.scala new file mode 100644 index 00000000..08fd7c99 --- /dev/null +++ b/app/de/innfactory/bootstrapplay2/filters/TracingFilter.scala @@ -0,0 +1,86 @@ +package de.innfactory.bootstrapplay2.filters + +import akka.stream.Materializer +import com.typesafe.config.Config +import de.innfactory.bootstrapplay2.common.tracing.Common.GoogleAttributes._ +import de.innfactory.bootstrapplay2.common.tracing.Common.{ + XTRACINGID, + X_INTERNAL_SPANID, + X_INTERNAL_TRACEID, + X_INTERNAL_TRACEOPTIONS +} +import org.joda.time.DateTime +import play.api.mvc._ +import io.opencensus.scala.Tracing._ +import io.opencensus.stats.Measure.MeasureDouble +import io.opencensus.stats.Stats +import io.opencensus.trace.samplers.Samplers +import io.opencensus.trace.{ AttributeValue, Sampler, SpanBuilder, Tracing } + +import javax.inject.Inject +import scala.concurrent.ExecutionContext.Implicits.global +import scala.concurrent.Future + +class TracingFilter @Inject() (config: Config, implicit val mat: Materializer) extends Filter { + + private val statsRecorder = Stats.getStatsRecorder + private val LATENCY_MS = MeasureDouble.create("task_latency", "The task latency in milliseconds", "ms") + + def apply(next: RequestHeader => Future[Result])(request: RequestHeader): Future[Result] = { + // Start Trace Span Root + val sampler = + if (request.headers.get(XTRACINGID).isDefined) + Samplers.alwaysSample() + else + Samplers.probabilitySampler(1.00) + + val span = Tracing.getTracer.spanBuilder(request.path).setSampler(sampler).startSpan() + + var xTracingId = (XTRACINGID, span.getContext.getTraceId.toLowerBase16) + + if (request.headers.get(XTRACINGID).isDefined) + xTracingId = (XTRACINGID, request.headers.get(XTRACINGID).get) + + // Add Annotations and Attributes to newly created Root Trace + span.addAnnotation("TracingFilter") + span.putAttribute("Position", AttributeValue.stringAttributeValue("TracingFilter")) + span.putAttribute(HTTP_URL, AttributeValue.stringAttributeValue(request.host + request.uri)) + span.putAttribute(HTTP_HOST, AttributeValue.stringAttributeValue(request.host)) + span.putAttribute(HTTP_METHOD, AttributeValue.stringAttributeValue(request.method)) + + // Add new Span to internal Request + val newRequestHeaders = request.headers + .add(xTracingId) + .add((X_INTERNAL_SPANID, span.getContext.getSpanId.toLowerBase16)) + .add((X_INTERNAL_TRACEID, span.getContext.getTraceId.toLowerBase16)) + .add((X_INTERNAL_TRACEOPTIONS, span.getContext.getTraceOptions.toLowerBase16)) + + // Call Next Filter with new Headers + val result = next(request.withHeaders(newRequestHeaders)) + + // Check Start Time + val start = DateTime.now + + // Process Result + result.map { res => + // Add more Attributes to trace + span.putAttribute(HTTP_STATUS_CODE, AttributeValue.longAttributeValue(res.header.status)) + span.putAttribute(STATUS, AttributeValue.longAttributeValue(res.header.status)) + span.putAttribute(HTTP_RESPONSE_SIZE, AttributeValue.longAttributeValue(res.body.contentLength.getOrElse(0))) + + // Finish Root Span + span.end() + + // Check end Time + val end = DateTime.now + + // Add Metric with Span Processing Time + statsRecorder.newMeasureMap.put(LATENCY_MS, end.getMillis - start.getMillis).record() + + // Add xTracingId to Result Header + res.withHeaders(xTracingId) + } + + } + +} diff --git a/app/de/innfactory/bootstrapplay2/graphql/ErrorParserImpl.scala b/app/de/innfactory/bootstrapplay2/graphql/ErrorParserImpl.scala index 6a8369d6..56fe5054 100644 --- a/app/de/innfactory/bootstrapplay2/graphql/ErrorParserImpl.scala +++ b/app/de/innfactory/bootstrapplay2/graphql/ErrorParserImpl.scala @@ -1,13 +1,13 @@ package de.innfactory.bootstrapplay2.graphql import de.innfactory.bootstrapplay2.common.results.Results -import de.innfactory.bootstrapplay2.common.results.Results.ErrorStatus +import de.innfactory.bootstrapplay2.common.results.Results.ResultStatus import de.innfactory.bootstrapplay2.common.results.errors.Errors.{ BadRequest, Forbidden } import de.innfactory.grapqhl.play.result.implicits.GraphQlResult.{ BadRequestError, ForbiddenError } import de.innfactory.grapqhl.play.result.implicits.{ ErrorParser, GraphQlException } -class ErrorParserImpl extends ErrorParser[ErrorStatus] { - override def internalErrorToUserFacingError(error: ErrorStatus): GraphQlException = error match { +class ErrorParserImpl extends ErrorParser[ResultStatus] { + override def internalErrorToUserFacingError(error: ResultStatus): GraphQlException = error match { case _: BadRequest => BadRequestError("BadRequest") case _: Forbidden => ForbiddenError("Forbidden") case _ => BadRequestError("BadRequest") diff --git a/app/de/innfactory/bootstrapplay2/graphql/GraphQLExecutionContext.scala b/app/de/innfactory/bootstrapplay2/graphql/GraphQLExecutionContext.scala index eee1a9e3..ea417110 100644 --- a/app/de/innfactory/bootstrapplay2/graphql/GraphQLExecutionContext.scala +++ b/app/de/innfactory/bootstrapplay2/graphql/GraphQLExecutionContext.scala @@ -3,8 +3,11 @@ package de.innfactory.bootstrapplay2.graphql import play.api.mvc.{ AnyContent, Request } import de.innfactory.bootstrapplay2.repositories.{ CompaniesRepository, LocationRepository } +import scala.concurrent.ExecutionContext + case class GraphQLExecutionContext( request: Request[AnyContent], + ec: ExecutionContext, companiesRepository: CompaniesRepository, locationsRepository: LocationRepository ) diff --git a/app/de/innfactory/bootstrapplay2/graphql/RequestExecutor.scala b/app/de/innfactory/bootstrapplay2/graphql/RequestExecutor.scala index 783ecd31..9f8cae36 100644 --- a/app/de/innfactory/bootstrapplay2/graphql/RequestExecutor.scala +++ b/app/de/innfactory/bootstrapplay2/graphql/RequestExecutor.scala @@ -4,11 +4,16 @@ import de.innfactory.bootstrapplay2.graphql.schema.SchemaDefinition import de.innfactory.grapqhl.play.request.RequestExecutionBase import play.api.mvc.{ AnyContent, Request } +import scala.concurrent.ExecutionContext + class RequestExecutor extends RequestExecutionBase[GraphQLExecutionContext, ExecutionServices](SchemaDefinition.graphQLSchema) { - override def contextBuilder(services: ExecutionServices, request: Request[AnyContent]): GraphQLExecutionContext = + override def contextBuilder(services: ExecutionServices, request: Request[AnyContent])(implicit + ec: ExecutionContext + ): GraphQLExecutionContext = GraphQLExecutionContext( request = request, + ec = ec, companiesRepository = services.companiesRepository, locationsRepository = services.locationsRepository ) diff --git a/app/de/innfactory/bootstrapplay2/graphql/schema/SchemaDefinition.scala b/app/de/innfactory/bootstrapplay2/graphql/schema/SchemaDefinition.scala index 43b48b0a..9a30a8b3 100644 --- a/app/de/innfactory/bootstrapplay2/graphql/schema/SchemaDefinition.scala +++ b/app/de/innfactory/bootstrapplay2/graphql/schema/SchemaDefinition.scala @@ -24,7 +24,7 @@ object SchemaDefinition { Query, None, description = Some( - "Schema for Familotel API " + "Schema for Bootstrap API " ) ) diff --git a/app/de/innfactory/bootstrapplay2/graphql/schema/models/Arguments.scala b/app/de/innfactory/bootstrapplay2/graphql/schema/models/Arguments.scala new file mode 100644 index 00000000..92a20381 --- /dev/null +++ b/app/de/innfactory/bootstrapplay2/graphql/schema/models/Arguments.scala @@ -0,0 +1,12 @@ +package de.innfactory.bootstrapplay2.graphql.schema.models + +import sangria.schema.{ Argument, OptionInputType, StringType } + +object Arguments { + val FilterArg: Argument[Option[String]] = + Argument( + "filter", + OptionInputType(StringType), + description = "Filters for companies, separated by & with key=value" + ) +} diff --git a/app/de/innfactory/bootstrapplay2/graphql/schema/models/Companies.scala b/app/de/innfactory/bootstrapplay2/graphql/schema/models/Companies.scala index 695f66c4..a101264a 100644 --- a/app/de/innfactory/bootstrapplay2/graphql/schema/models/Companies.scala +++ b/app/de/innfactory/bootstrapplay2/graphql/schema/models/Companies.scala @@ -1,16 +1,32 @@ package de.innfactory.bootstrapplay2.graphql.schema.models +import de.innfactory.bootstrapplay2.common.filteroptions.FilterOptionUtils +import de.innfactory.bootstrapplay2.common.implicits.RequestToRequestContextImplicit.EnhancedRequest +import de.innfactory.bootstrapplay2.common.request.RequestContext +import de.innfactory.bootstrapplay2.graphql.GraphQLExecutionContext import de.innfactory.bootstrapplay2.models.api.Company -import sangria.macros.derive.{ deriveObjectType, ReplaceField } -import sangria.schema.{ Field, ObjectType, StringType } +import sangria.macros.derive.{ deriveObjectType, AddFields, ReplaceField } +import sangria.schema.{ Field, ListType, LongType, ObjectType, StringType } import de.innfactory.grapqhl.sangria.implicits.JsonScalarType._ import de.innfactory.grapqhl.sangria.implicits.JodaScalarType._ object Companies { - val CompanyType: ObjectType[Unit, Company] = deriveObjectType( + val CompanyType: ObjectType[GraphQLExecutionContext, Company] = deriveObjectType( ReplaceField( fieldName = "id", field = Field(name = "id", fieldType = StringType, resolve = c => c.value.id.get.toString) + ), + AddFields( + Field( + name = "subcompanies", + resolve = ctx => + ctx.ctx.request.toRequestContextAndExecute( + "allCompanies GraphQL", + (rc: RequestContext) => + ctx.ctx.companiesRepository.allGraphQl(FilterOptionUtils.optionStringToFilterOptions(None))(rc) + )(ctx.ctx.ec), + fieldType = ListType(CompanyType) + ) ) ) } diff --git a/app/de/innfactory/bootstrapplay2/graphql/schema/models/Locations.scala b/app/de/innfactory/bootstrapplay2/graphql/schema/models/Locations.scala index 6b6d1aa2..7a12f8a2 100644 --- a/app/de/innfactory/bootstrapplay2/graphql/schema/models/Locations.scala +++ b/app/de/innfactory/bootstrapplay2/graphql/schema/models/Locations.scala @@ -1,5 +1,6 @@ package de.innfactory.bootstrapplay2.graphql.schema.models +import de.innfactory.bootstrapplay2.graphql.GraphQLExecutionContext import de.innfactory.bootstrapplay2.models.api.Location import sangria.macros.derive.{ deriveObjectType, ReplaceField } import sangria.schema.{ BigDecimalType, Field, ObjectType, OptionType, StringType } @@ -21,11 +22,12 @@ object Locations { field = Field( name = "distance", fieldType = OptionType(BigDecimalType), - resolve = c => + resolve = c => { c.value.distance match { case Some(v) => Some(BigDecimal.decimal(v)) case None => None } + } ) ) ) diff --git a/app/de/innfactory/bootstrapplay2/graphql/schema/queries/Company.scala b/app/de/innfactory/bootstrapplay2/graphql/schema/queries/Company.scala index ce12cd1e..5dffffb9 100644 --- a/app/de/innfactory/bootstrapplay2/graphql/schema/queries/Company.scala +++ b/app/de/innfactory/bootstrapplay2/graphql/schema/queries/Company.scala @@ -1,16 +1,26 @@ package de.innfactory.bootstrapplay2.graphql.schema.queries +import de.innfactory.bootstrapplay2.common.filteroptions.FilterOptionUtils +import de.innfactory.bootstrapplay2.common.implicits.RequestToRequestContextImplicit.EnhancedRequest +import de.innfactory.bootstrapplay2.common.request.RequestContext import de.innfactory.bootstrapplay2.graphql.GraphQLExecutionContext import de.innfactory.bootstrapplay2.graphql.schema.models.Companies.CompanyType +import de.innfactory.bootstrapplay2.graphql.schema.models.Arguments.FilterArg import sangria.schema.{ Field, ListType } object Company { val allCompanies: Field[GraphQLExecutionContext, Unit] = Field( "allCompanies", ListType(CompanyType), - arguments = Nil, - resolve = ctx => ctx.ctx.companiesRepository.all(ctx.ctx.request), - description = Some("Familotel Filter API hotels query. Query group by id") + arguments = FilterArg :: Nil, + resolve = ctx => { + ctx.ctx.request.toRequestContextAndExecute( + "allCompanies GraphQL", + (rc: RequestContext) => + ctx.ctx.companiesRepository.allGraphQl(FilterOptionUtils.optionStringToFilterOptions(ctx arg FilterArg))(rc) + )(ctx.ctx.ec) + }, + description = Some("Bootstrap Filter API hotels query. Query group by id") ) } diff --git a/app/de/innfactory/bootstrapplay2/models/api/ApiBaseModel.scala b/app/de/innfactory/bootstrapplay2/models/api/ApiBaseModel.scala deleted file mode 100644 index 0e522187..00000000 --- a/app/de/innfactory/bootstrapplay2/models/api/ApiBaseModel.scala +++ /dev/null @@ -1,7 +0,0 @@ -package de.innfactory.bootstrapplay2.models.api - -import play.api.libs.json.JsValue - -trait ApiBaseModel { - def toJson: JsValue -} diff --git a/app/de/innfactory/bootstrapplay2/models/api/Company.scala b/app/de/innfactory/bootstrapplay2/models/api/Company.scala index 94161f29..a34f566a 100644 --- a/app/de/innfactory/bootstrapplay2/models/api/Company.scala +++ b/app/de/innfactory/bootstrapplay2/models/api/Company.scala @@ -10,18 +10,17 @@ import play.api.libs.json.JodaReads._ import play.api.libs.json.Reads import de.innfactory.bootstrapplay2.common.utils.OptionUtils._ -/** - * Implementation independent aggregate root. - */ case class Company( id: Option[UUID], firebaseUser: Option[List[String]], settings: Option[JsValue], + stringAttribute1: Option[String], + stringAttribute2: Option[String], + longAttribute1: Long, + booleanAttribute: Boolean, created: Option[DateTime], updated: Option[DateTime] -) extends ApiBaseModel { - override def toJson: JsValue = Json.toJson(this) -} +) object Company { implicit val reads = Json.reads[Company] @@ -33,6 +32,10 @@ object Company { settings = newObject.settings.getOrElseOld(oldObject.settings), firebaseUser = newObject.firebaseUser.getOrElseOld(oldObject.firebaseUser), created = oldObject.created, + stringAttribute1 = newObject.stringAttribute1, + stringAttribute2 = newObject.stringAttribute2, + longAttribute1 = newObject.longAttribute1, + booleanAttribute = newObject.booleanAttribute, updated = Some(DateTime.now) ) diff --git a/app/de/innfactory/bootstrapplay2/models/api/Location.scala b/app/de/innfactory/bootstrapplay2/models/api/Location.scala index ce4f62f4..070772b3 100644 --- a/app/de/innfactory/bootstrapplay2/models/api/Location.scala +++ b/app/de/innfactory/bootstrapplay2/models/api/Location.scala @@ -11,9 +11,6 @@ import play.api.libs.json.JodaWrites._ import play.api.libs.json.JodaReads._ import play.api.libs.json.Reads -/** - * Implementation independent aggregate root. - */ case class Location( id: Option[Long], company: UUID, @@ -29,10 +26,7 @@ case class Location( created: Option[DateTime], updated: Option[DateTime], distance: Option[Float] -) extends ApiBaseModel { - val writes: Writes[Location] = implicitly - override def toJson: JsValue = Json.toJson(this)(writes) -} +) object Location { implicit val reads = Json.reads[Location] diff --git a/app/de/innfactory/bootstrapplay2/repositories/CompaniesRepository.scala b/app/de/innfactory/bootstrapplay2/repositories/CompaniesRepository.scala index d2a61025..10ba7407 100644 --- a/app/de/innfactory/bootstrapplay2/repositories/CompaniesRepository.scala +++ b/app/de/innfactory/bootstrapplay2/repositories/CompaniesRepository.scala @@ -1,71 +1,105 @@ package de.innfactory.bootstrapplay2.repositories import java.util.UUID - -import de.innfactory.bootstrapplay2.actions.RequestWithCompany import cats.implicits._ import cats.data.EitherT import com.google.inject.ImplementedBy import de.innfactory.bootstrapplay2.common.authorization.CompanyAuthorizationMethods -import de.innfactory.bootstrapplay2.common.results.Results.{ ErrorStatus, Result } +import de.innfactory.bootstrapplay2.common.implicits.EitherTTracingImplicits.{ EnhancedTracingEitherT, TracedT } +import de.innfactory.bootstrapplay2.common.logging.ImplicitLogContext +import de.innfactory.bootstrapplay2.common.repositories.{ All, Delete, Lookup, Patch, Post } +import de.innfactory.bootstrapplay2.common.request.{ RequestContext, RequestContextWithCompany, TraceContext } +import de.innfactory.bootstrapplay2.common.results.Results.{ Result, ResultStatus } +import de.innfactory.bootstrapplay2.common.results.errors.Errors.BadRequest +import de.innfactory.bootstrapplay2.common.utils.OptionUtils.EnhancedOption import de.innfactory.bootstrapplay2.db.CompaniesDAO import de.innfactory.bootstrapplay2.graphql.ErrorParserImpl import de.innfactory.grapqhl.play.result.implicits.GraphQlResult.EnhancedFutureResult -import javax.inject.{ Inject, Singleton } + +import javax.inject.Inject import de.innfactory.bootstrapplay2.models.api.Company -import play.api.mvc.{ AnyContent, Request } +import de.innfactory.play.slick.enhanced.utils.filteroptions.FilterOptions +import play.api.mvc.AnyContent +import slick.basic.DatabasePublisher import scala.concurrent.{ ExecutionContext, Future } @ImplementedBy(classOf[CompaniesRepositoryImpl]) -trait CompaniesRepository { - def lookup(id: UUID, request: RequestWithCompany[AnyContent]): Future[Result[Company]] - def all(request: Request[AnyContent]): Future[Seq[Company]] - def patch(company: Company, request: RequestWithCompany[AnyContent]): Future[Result[Company]] - def post(company: Company, request: RequestWithCompany[AnyContent]): Future[Result[Company]] - def delete(id: UUID, request: RequestWithCompany[AnyContent]): Future[Result[Company]] +trait CompaniesRepository + extends Lookup[UUID, RequestContextWithCompany, Company] + with All[RequestContext, Company] + with Patch[RequestContextWithCompany, Company] + with Post[RequestContext, Company] + with Delete[UUID, RequestContextWithCompany, Company] { + def allGraphQl(filter: Seq[FilterOptions[_root_.dbdata.Tables.Company, _]])(implicit + rc: RequestContext + ): Future[Seq[Company]] + def streamedAll(implicit tc: TraceContext): Future[DatabasePublisher[Company]] } class CompaniesRepositoryImpl @Inject() ( companiesDAO: CompaniesDAO, authorizationMethods: CompanyAuthorizationMethods[AnyContent] )(implicit ec: ExecutionContext, errorParser: ErrorParserImpl) - extends CompaniesRepository { + extends CompaniesRepository + with ImplicitLogContext { - def lookup(id: UUID, request: RequestWithCompany[AnyContent]): Future[Result[Company]] = { + def lookup(id: UUID)(implicit rc: RequestContextWithCompany): Future[Result[Company]] = { val result = for { lookupResult <- EitherT(companiesDAO.lookup(id)) - _ <- EitherT(Future(authorizationMethods.canGet(request, lookupResult))) + _ <- EitherT(Future(authorizationMethods.canGet(lookupResult))) + } yield lookupResult + result.value + } + + def all(implicit rc: RequestContext): Future[Result[Seq[Company]]] = { + val result = for { + lookupResult <- EitherT(companiesDAO.all.map(_.asRight[ResultStatus])) } yield lookupResult result.value } - def all(request: Request[AnyContent]): Future[Seq[Company]] = { + def streamedAll(implicit tc: TraceContext): Future[DatabasePublisher[Company]] = { + val result = for { + pub <- Future(companiesDAO.streamedAll) + } yield pub + result + } + + def allGraphQl( + filter: Seq[FilterOptions[_root_.dbdata.Tables.Company, _]] + )(implicit rc: RequestContext): Future[Seq[Company]] = { val result = for { - lookupResult <- EitherT(companiesDAO.all().map(_.asRight[ErrorStatus])) + lookupResult <- EitherT(companiesDAO.allWithFilter(filter).map(_.asRight[ResultStatus])) } yield lookupResult result.value.completeOrThrow } - def patch(company: Company, request: RequestWithCompany[AnyContent]): Future[Result[Company]] = { + def patch(company: Company)(implicit rc: RequestContextWithCompany): Future[Result[Company]] = { val result = for { - oldCompany <- EitherT(companiesDAO.lookup(company.id.getOrElse(UUID.randomUUID()))) - _ <- EitherT(Future(authorizationMethods.canUpdate(request, company))) - companyUpdate <- EitherT(companiesDAO.update(company.copy(id = oldCompany.id))) + _ <- TracedT("Patch Company Repository before lookup") // Can be used as extra step + oldCompany <- EitherT(companiesDAO.lookup(company.id.getOrElse(UUID.randomUUID()))).trace("Companies DAO Lookup") + _ <- TracedT("Patch Company Repository after lookup") + _ <- EitherT(Future(authorizationMethods.canUpdate(company))).trace("Authorization Method") + companyUpdate <- EitherT(companiesDAO.update(company.copy(id = oldCompany.id))).trace("Companies DAO Update") } yield companyUpdate result.value } - def post(company: Company, request: RequestWithCompany[AnyContent]): Future[Result[Company]] = { - val result: EitherT[Future, ErrorStatus, Company] = for { - _ <- EitherT(Future(authorizationMethods.canCreate(request, company))) + def post(company: Company)(implicit rc: RequestContext): Future[Result[Company]] = { + val result: EitherT[Future, ResultStatus, Company] = for { + _ <- EitherT({ + if (company.id.isDefined) companiesDAO.lookup(company.id.get).map(_.toOption.toInverseEither(BadRequest())) + else Future(Right(())) + }) + _ <- EitherT(Future(authorizationMethods.canCreate(company))) createdResult <- EitherT(companiesDAO.create(company)) } yield createdResult result.value } - def delete(id: UUID, request: RequestWithCompany[AnyContent]): Future[Result[Company]] = { - val result: EitherT[Future, ErrorStatus, Company] = for { + def delete(id: UUID)(implicit rc: RequestContextWithCompany): Future[Result[Company]] = { + val result: EitherT[Future, ResultStatus, Company] = for { company <- EitherT(companiesDAO.lookup(id)) - _ <- EitherT(Future(authorizationMethods.canDelete(request, company))) + _ <- EitherT(Future(authorizationMethods.canDelete(company))) _ <- EitherT(companiesDAO.delete(id)) } yield company result.value diff --git a/app/de/innfactory/bootstrapplay2/repositories/LocationRepository.scala b/app/de/innfactory/bootstrapplay2/repositories/LocationRepository.scala index d85cebf3..d8032274 100644 --- a/app/de/innfactory/bootstrapplay2/repositories/LocationRepository.scala +++ b/app/de/innfactory/bootstrapplay2/repositories/LocationRepository.scala @@ -1,47 +1,46 @@ package de.innfactory.bootstrapplay2.repositories import java.util.UUID - -import de.innfactory.bootstrapplay2.actions.RequestWithCompany import cats.data.EitherT import de.innfactory.bootstrapplay2.common.authorization.LocationAuthorizationMethods -import de.innfactory.bootstrapplay2.common.results.Results.{ ErrorStatus, Result } +import de.innfactory.bootstrapplay2.common.results.Results.{ Result, ResultStatus } import javax.inject.Inject import de.innfactory.bootstrapplay2.models.api.Location import cats.implicits._ import com.google.inject.ImplementedBy +import de.innfactory.bootstrapplay2.common.logging.ImplicitLogContext +import de.innfactory.bootstrapplay2.common.repositories.{ Delete, Lookup, Patch, Post } +import de.innfactory.bootstrapplay2.common.request.RequestContextWithCompany import de.innfactory.common.geo.GeoPointFactory -import de.innfactory.bootstrapplay2.common.results.errors.Errors.Forbidden import play.api.mvc.AnyContent -import de.innfactory.bootstrapplay2.common.utils.OptionUtils._ import de.innfactory.bootstrapplay2.db.LocationsDAO import scala.concurrent.{ ExecutionContext, Future } @ImplementedBy(classOf[LocationRepositoryImpl]) -trait LocationRepository { - def lookup(id: Long, request: RequestWithCompany[AnyContent]): Future[Result[Location]] +trait LocationRepository + extends Lookup[Long, RequestContextWithCompany, Location] + with Patch[RequestContextWithCompany, Location] + with Post[RequestContextWithCompany, Location] + with Delete[Long, RequestContextWithCompany, Location] { def getByDistance( distance: Long, lat: Double, - lon: Double, - request: RequestWithCompany[AnyContent] - ): Future[Result[Seq[Location]]] - def lookupByCompany(id: UUID, request: RequestWithCompany[AnyContent]): Future[Result[Seq[Location]]] - def patch(location: Location, request: RequestWithCompany[AnyContent]): Future[Result[Location]] - def post(location: Location, request: RequestWithCompany[AnyContent]): Future[Result[Location]] - def delete(id: Long, request: RequestWithCompany[AnyContent]): Future[Result[Location]] + lon: Double + )(implicit rc: RequestContextWithCompany): Future[Result[Seq[Location]]] + def lookupByCompany(id: UUID)(implicit rc: RequestContextWithCompany): Future[Result[Seq[Location]]] } class LocationRepositoryImpl @Inject() ( locationsDAO: LocationsDAO, authorizationMethods: LocationAuthorizationMethods[AnyContent] )(implicit ec: ExecutionContext) - extends LocationRepository { - override def lookup(id: Long, request: RequestWithCompany[AnyContent]): Future[Result[Location]] = { + extends LocationRepository + with ImplicitLogContext { + override def lookup(id: Long)(implicit rc: RequestContextWithCompany): Future[Result[Location]] = { val result = for { lookupResult <- EitherT(locationsDAO.lookup(id)) - _ <- EitherT(Future(authorizationMethods.accessGet(request, lookupResult))) + _ <- EitherT(Future(authorizationMethods.accessGet(lookupResult))) } yield lookupResult result.value } @@ -49,50 +48,48 @@ class LocationRepositoryImpl @Inject() ( override def getByDistance( distance: Long, lat: Double, - lon: Double, - request: RequestWithCompany[AnyContent] - ): Future[Result[Seq[Location]]] = { + lon: Double + )(implicit rc: RequestContextWithCompany): Future[Result[Seq[Location]]] = { val geometryPoint = GeoPointFactory.createPoint(lat, lon) val result = for { - company <- EitherT(Future(request.company.toEither(Forbidden()))) - _ <- EitherT(Future(authorizationMethods.accessGetAllByCompany(company.id.getOrElse(UUID.randomUUID()), request))) + _ <- EitherT(Future(authorizationMethods.accessGetAllByCompany(rc.company.id.getOrElse(UUID.randomUUID())))) lookupResult <- EitherT( locationsDAO - .allFromDistanceByCompany(company.id.getOrElse(UUID.randomUUID()), geometryPoint, distance) + .allFromDistanceByCompany(rc.company.id.getOrElse(UUID.randomUUID()), geometryPoint, distance) ) } yield lookupResult result.value } - def lookupByCompany(id: UUID, request: RequestWithCompany[AnyContent]): Future[Result[Seq[Location]]] = { + def lookupByCompany(id: UUID)(implicit rc: RequestContextWithCompany): Future[Result[Seq[Location]]] = { val result = for { - _ <- EitherT(Future(authorizationMethods.accessGetAllByCompany(id, request))) + _ <- EitherT(Future(authorizationMethods.accessGetAllByCompany(id))) lookupResult <- EitherT(locationsDAO.lookupByCompany(id)) } yield lookupResult result.value } - def patch(location: Location, request: RequestWithCompany[AnyContent]): Future[Result[Location]] = { + def patch(location: Location)(implicit rc: RequestContextWithCompany): Future[Result[Location]] = { val result = for { oldLocation <- EitherT(locationsDAO.lookup(location.id.getOrElse(0))) - _ <- EitherT(Future(authorizationMethods.update(request, location.company, oldLocation.company))) + _ <- EitherT(Future(authorizationMethods.update(location.company, oldLocation.company))) locationUpdate <- EitherT(locationsDAO.update(location.copy(id = oldLocation.id))) } yield locationUpdate result.value } - def post(location: Location, request: RequestWithCompany[AnyContent]): Future[Result[Location]] = { - val result: EitherT[Future, ErrorStatus, Location] = for { - _ <- EitherT(authorizationMethods.createDelete(request, location.company)) + def post(location: Location)(implicit rc: RequestContextWithCompany): Future[Result[Location]] = { + val result: EitherT[Future, ResultStatus, Location] = for { + _ <- EitherT(Future(authorizationMethods.createDelete(location.company))) createdResult <- EitherT(locationsDAO.create(location)) } yield createdResult result.value } - def delete(id: Long, request: RequestWithCompany[AnyContent]): Future[Result[Location]] = { - val result: EitherT[Future, ErrorStatus, Location] = for { + def delete(id: Long)(implicit rc: RequestContextWithCompany): Future[Result[Location]] = { + val result: EitherT[Future, ResultStatus, Location] = for { location <- EitherT(locationsDAO.lookup(id)) - _ <- EitherT(authorizationMethods.createDelete(request, location.company)) + _ <- EitherT(Future(authorizationMethods.createDelete(location.company))) _ <- EitherT(locationsDAO.delete(location.id.get)) } yield location result.value diff --git a/app/de/innfactory/bootstrapplay2/websockets/actors/WebSocketActor.scala b/app/de/innfactory/bootstrapplay2/websockets/actors/WebSocketActor.scala index 2f881311..b1ed47dc 100644 --- a/app/de/innfactory/bootstrapplay2/websockets/actors/WebSocketActor.scala +++ b/app/de/innfactory/bootstrapplay2/websockets/actors/WebSocketActor.scala @@ -11,6 +11,5 @@ class WebSocketActor(out: ActorRef) extends Actor { def receive: PartialFunction[Any, Unit] = { case msg: String => out ! ("I received your message: " + msg + " | This is message: " + counter) counter += 1 - } } diff --git a/build.sbt b/build.sbt index a4d0b205..57762cf0 100755 --- a/build.sbt +++ b/build.sbt @@ -8,7 +8,6 @@ scalaVersion := Dependencies.scalaVersion resolvers += Resolver.githubPackages("innFactory") -githubTokenSource := TokenSource.Environment("GITHUB_TOKEN") val token = sys.env.getOrElse("GITHUB_TOKEN", "") val githubSettings = Seq( @@ -46,7 +45,7 @@ val generateTables = taskKey[Seq[File]]("Generate slick code") // Testing coverageExcludedPackages += ";Reverse.*;router.*;.*AuthService.*;models\\\\.data\\\\..*;dbdata.Tables*;de.innfactory.bootstrapplay2.common.jwt.*;de.innfactory.bootstrapplay2.common.errorHandling.*;de.innfactory.bootstrapplay2.common.jwt.JwtFilter;db.codegen.*;de.innfactory.bootstrapplay2.common.pubSub.*;publicmetrics.influx.*" -fork in Test := true +Test / fork := true // Commands @@ -55,7 +54,6 @@ addCommandAlias("localTests", "; clean; flyway/flywayMigrate; test") /* TaskKeys */ lazy val slickGen = taskKey[Seq[File]]("slickGen") -lazy val copyRes = TaskKey[Unit]("copyRes") /* Create db config for flyway */ def createDbConf(dbConfFile: File): DbConf = { @@ -75,32 +73,32 @@ def createDbConf(dbConfFile: File): DbConf = { def dbConfSettings = Seq( - dbConf in Global := createDbConf((resourceDirectory in Compile).value / "application.conf") + Global / dbConf := createDbConf((Compile / resourceDirectory).value / "application.conf") ) def flywaySettings = Seq( - flywayUrl := (dbConf in Global).value.url, - flywayUser := (dbConf in Global).value.user, - flywayPassword := (dbConf in Global).value.password, + flywayUrl := (Global / dbConf).value.url, + flywayUser := (Global / dbConf).value.user, + flywayPassword := (Global / dbConf).value.password, flywaySchemas := (Seq("postgis")) ) def generateTablesTask(conf: DbConf) = Def.task { val dir = sourceManaged.value - val outputDir = (dir / "slick").getPath + val outputDir = (dir / "slick/main").getPath val fname = outputDir + generatedFilePath val generator = "db.codegen.CustomizedCodeGenerator" val url = conf.url val slickProfile = conf.profile.dropRight(1) val jdbcDriver = conf.driver val pkg = "db.Tables" - val cp = (dependencyClasspath in Compile).value + val cp = (Compile / dependencyClasspath).value val username = conf.user val password = conf.password val s = streams.value - val r = (runner in Compile).value + val r = (Compile / runner).value r.run( generator, cp.files, @@ -110,7 +108,7 @@ def generateTablesTask(conf: DbConf) = Seq(file(fname)) } -slickGen := Def.taskDyn(generateTablesTask((dbConf in Global).value)).value +slickGen := Def.taskDyn(generateTablesTask((Global / dbConf).value)).value /*project definitions*/ @@ -123,9 +121,9 @@ lazy val root = (project in file(".")) libraryDependencies ++= Dependencies.list, // Adding Cache libraryDependencies ++= Seq(ehcache), + dependencyOverrides += Dependencies.sl4j, // Override to avoid problems with HikariCP 4.x swaggerDomainNameSpaces := Seq( "models", - "publicmetrics" ), // New Models have to be added here to be referencable in routes swaggerPrettyJson := true, swaggerV3 := true, @@ -135,7 +133,7 @@ lazy val root = (project in file(".")) Seq( maintainer := "innFactory", version := buildVersion, - packageName in Docker := "bootstrap-play2", + Docker / packageName := "bootstrap-play2", dockerUpdateLatest := latest, dockerRepository := dockerRegistry, dockerExposedPorts := Seq(8080, 8080), @@ -163,14 +161,11 @@ lazy val slick = (project in file("modules/slick")) lazy val globalResources = file("conf") -unmanagedResourceDirectories in Compile += globalResources -unmanagedResourceDirectories in Runtime += globalResources - /* Scala format */ -scalafmtOnCompile in ThisBuild := true // all projects +ThisBuild / scalafmtOnCompile := true // all projects /* Change compiling */ -sourceGenerators in Compile += Def.taskDyn(generateTablesTask((dbConf in Global).value)).taskValue -compile in Compile := { - (compile in Compile).value +Compile / sourceGenerators += Def.taskDyn(generateTablesTask((Global / dbConf).value)).taskValue +Compile /compile := { + (Compile / compile).value } diff --git a/conf/application.conf b/conf/application.conf index 6719e00e..dca505a4 100755 --- a/conf/application.conf +++ b/conf/application.conf @@ -30,19 +30,11 @@ bootstrap-play2 { driver = org.postgresql.Driver - // The number of threads determines how many things you can *run* in parallel - // the number of connections determines you many things you can *keep in memory* at the same time - // on the database server. - // numThreads = (core_count (hyperthreading included)) - numThreads = 20 + queueSize = 100 - // queueSize = ((core_count * 2) + effective_spindle_count) - // on a MBP 13, this is 2 cores * 2 (hyperthreading not included) + 1 hard disk - queueSize = 1000 - - // https://blog.knoldus.com/2016/01/01/best-practices-for-using-slick-on-production/ - // make larger than numThreads + queueSize - maxConnections = 20 + numThreads = 4 + maxThreads = 4 + maxConnections = 8 connectionTimeout = 7000 validationTimeout = 7000 @@ -66,7 +58,7 @@ play.http.secret.key = ${?PLAY_HTTP_SECRET_KEY} // FILTERS -play.filters.enabled = ["de.innfactory.bootstrapplay2.filters.logging.AccessLoggingFilter", "play.filters.cors.CORSFilter", "de.innfactory.bootstrapplay2.filters.access.RouteBlacklistFilter" ] +play.filters.enabled = ["de.innfactory.bootstrapplay2.filters.TracingFilter", "de.innfactory.bootstrapplay2.filters.logging.AccessLoggingFilter", "play.filters.cors.CORSFilter", "de.innfactory.bootstrapplay2.filters.access.RouteBlacklistFilter" ] play.filters.cors { pathPrefixes = ["/v1/"] @@ -82,4 +74,13 @@ play.filters.cors { logging.access.statusList = [404,403,401] logging.access.statusList = ${?LOGGING_STATUSLIST} -http.port = 8080 \ No newline at end of file +http.port = 8080 + +project.id = "bootstrap-play2" +project.id = ${?PROJECT_ID} + +opencensus-scala { + trace { + sampling-probability = 1 + } +} \ No newline at end of file diff --git a/conf/db/migration/V1__Tables.sql b/conf/db/migration/V1__Tables.sql index 7a20a0ae..5e74c73a 100755 --- a/conf/db/migration/V1__Tables.sql +++ b/conf/db/migration/V1__Tables.sql @@ -6,6 +6,10 @@ CREATE TABLE "company" "id" uuid NOT NULL, "firebase_user" varchar(500)[] NOT NULL, "settings" json NOT NULL, + "string_attribute_1" varchar, + "string_attribute_2" varchar, + "long_attribute_1" bigint NOT NULL, + "boolean_attribute" boolean NOT NULL, "created" timestamp NOT NULL, "updated" timestamp NOT NULL, CONSTRAINT "PK_company" PRIMARY KEY ( "id" ) diff --git a/conf/logback.xml b/conf/logback.xml index 273c19fb..a41b8548 100755 --- a/conf/logback.xml +++ b/conf/logback.xml @@ -18,6 +18,11 @@ + + conf/firebase.json + de.innfactory.play.logging.logback.BaseEnhancer + + @@ -39,6 +44,7 @@ + diff --git a/conf/routes b/conf/routes index fa4d43f3..7617f7fd 100755 --- a/conf/routes +++ b/conf/routes @@ -91,6 +91,24 @@ GET /readiness de.innfactor ### GET /v1/companies/me de.innfactory.bootstrapplay2.controllers.CompaniesController.getMe +### +# summary: Get All Companies +# tags: +# - companies +# responses: +# '200': +# description: success +# schema: +# $ref: '#/components/schemas/de.innfactory.bootstrapplay2.models.api.Company' +# '404': +# description: not found +# '401': +# description: not authorized +# '403': +# description: forbidden +### +GET /v1/companies de.innfactory.bootstrapplay2.controllers.CompaniesController.getStreamed + ### # summary: Get Single # security: diff --git a/conf/swagger.yml b/conf/swagger.yml index 447c10a9..a5bf75f0 100644 --- a/conf/swagger.yml +++ b/conf/swagger.yml @@ -1,6 +1,6 @@ openapi: 3.0.0 info: - title: "Bootstarp-Play2" + title: "Bootstrap-Play2" description: "REST API" version: "0.0.1" servers: diff --git a/modules/slick/src/main/scala/db/codegen/CustomizedCodeGenerator.scala b/modules/slick/src/main/scala/db/codegen/CustomizedCodeGenerator.scala index 716a007e..93613ec2 100644 --- a/modules/slick/src/main/scala/db/codegen/CustomizedCodeGenerator.scala +++ b/modules/slick/src/main/scala/db/codegen/CustomizedCodeGenerator.scala @@ -8,12 +8,14 @@ class CodeGenConfig() extends Config[XPostgresProfile] { object CustomizedCodeGenerator extends CustomizedCodeGeneratorBase( - CustomizedCodeGeneratorConfig(), + CustomizedCodeGeneratorConfig( + folder = "/target/scala-2.13/src_managed/slick/main" + ), new CodeGenConfig() ) { // Update here if new Tables are added // Each Database Table, which should be included in CodeGen // has to be added here in UPPER-CASE - override def included: Seq[String] = Seq("COMPANY", "LOCATION") + override def included: Seq[String] = Seq("company", "location").map(_.toUpperCase) } diff --git a/project/Build.scala b/project/Build.scala index 071f5c5a..11b36e06 100755 --- a/project/Build.scala +++ b/project/Build.scala @@ -26,8 +26,8 @@ object Common { "javax.inject" % "javax.inject" % "1", "joda-time" % "joda-time" % "2.9.9", "org.joda" % "joda-convert" % "1.9.2", - "com.google.inject" % "guice" % "4.2.3" + "com.google.inject" % "guice" % "5.0.1" ), - scalacOptions in Test ++= Seq("-Yrangepos") + Test / scalacOptions ++= Seq("-Yrangepos") ) } diff --git a/project/Dependencies.scala b/project/Dependencies.scala index 3e6e921d..045df0bb 100644 --- a/project/Dependencies.scala +++ b/project/Dependencies.scala @@ -5,15 +5,19 @@ object Dependencies { val scalaVersion = "2.13.3" val akkaVersion = "2.6.14" + val akkaTyped = "com.typesafe.akka" %% "akka-actor-typed" % akkaVersion + val akkaHttp = "com.typesafe.akka" %% "akka-http" % "10.1.14" val akka = "com.typesafe.akka" %% "akka-actor" % akkaVersion - val akkaJackson = - "com.typesafe.akka" %% "akka-serialization-jackson" % akkaVersion // https://github.com/akka/akka/issues/29351 - val akkaStreams = "com.typesafe.akka" %% "akka-stream" % akkaVersion + + // https://github.com/akka/akka/issues/29351 + val akkaJackson = "com.typesafe.akka" %% "akka-serialization-jackson" % akkaVersion + + val akkaStreams = "com.typesafe.akka" %% "akka-stream" % akkaVersion // innFactory Utils - val scalaUtil = "de.innfactory.scala-utils" %% "scala-utils" % "1.0.92" + val scalaUtil = "de.innfactory.scala-utils" %% "scala-utils" % "1.3.2" //Prod val slickPg = "com.github.tminglei" %% "slick-pg" % "0.19.6" @@ -25,7 +29,7 @@ object Dependencies { val slick = "com.typesafe.slick" %% "slick" % "3.3.3" val slickCodegen = "com.typesafe.slick" %% "slick-codegen" % "3.3.3" val slickHikaricp = "com.typesafe.slick" %% "slick-hikaricp" % "3.3.3" - val hikariCP = "com.zaxxer" % "HikariCP" % "4.0.3" + val hikariCP = "com.zaxxer" % "HikariCP" % "4.0.3" exclude("org.slf4j", "slf4j-api") val joda = "joda-time" % "joda-time" % "2.10.10" val postgresql = "org.postgresql" % "postgresql" % "42.2.19" val cats = "org.typelevel" %% "cats-core" % "2.6.0" @@ -34,8 +38,33 @@ object Dependencies { val playAhcWS = "com.typesafe.play" %% "play-ahc-ws" % "2.8.8" % Test val scalatestPlus = "org.scalatestplus.play" %% "scalatestplus-play" % "5.1.0" % Test + // Dependent on the trace exporters you want to use add one or more of the following + val opencensusStackdriver = "io.opencensus" % "opencensus-exporter-trace-stackdriver" % "0.25.0" + val opencensusLoggging = "io.opencensus" % "opencensus-exporter-trace-logging" % "0.25.0" + val opencensusJaeger = "io.opencensus" % "opencensus-exporter-trace-jaeger" % "0.25.0" + + val opencensusStatsStackdriver = "io.opencensus" % "opencensus-exporter-stats-stackdriver" % "0.28.3" + + // If you want to use opencensus-scala inside an Akka HTTP project + val opencensusAkkaHttp = "com.github.sebruck" %% "opencensus-scala-akka-http" % "0.7.2" + + val sl4j = "org.slf4j" % "slf4j-api" % "1.7.30" intransitive + val sharedDeps = "com.google.cloud" % "google-cloud-shared-dependencies" % "0.18.0" + val logback = "ch.qos.logback" % "logback-classic" % "1.2.3" + val logbackCore = "ch.qos.logback" % "logback-core" % "1.2.3" + lazy val list = Seq( scalaUtil, + sl4j, + sharedDeps, + logback, + logbackCore, + akkaHttp, + opencensusStackdriver, + opencensusLoggging, + opencensusStatsStackdriver, + opencensusJaeger, + opencensusAkkaHttp, akka, akkaTyped, akkaJackson, diff --git a/test/controllers/AuthenticationTest.scala b/test/controllers/AuthenticationTest.scala index b0a6b887..a233f332 100644 --- a/test/controllers/AuthenticationTest.scala +++ b/test/controllers/AuthenticationTest.scala @@ -15,7 +15,6 @@ import org.joda.time.format.DateTimeFormat import play.api.libs.json.JodaWrites._ import play.api.libs.json.JodaReads._ import play.api.libs.json.Reads -import de.innfactory.bootstrapplay2.common.utils.PagedGen._ import play.api.test.CSRFTokenHelper._ import testutils.BaseFakeRequest import testutils.BaseFakeRequest._ @@ -44,7 +43,7 @@ class AuthenticationTest extends PlaySpec with BaseOneAppPerSuite with TestAppli "Authentication on Company" must { "get me" in { BaseFakeRequest(GET, "/v1/companies/me").get checkStatus 401 - BaseFakeRequest(GET, "/v1/companies/me").withHeader(("Authorization", invalidEmail)).get checkStatus 404 + BaseFakeRequest(GET, "/v1/companies/me").withHeader(("Authorization", invalidEmail)).get checkStatus 403 BaseFakeRequest(GET, "/v1/companies/me").withHeader(("Authorization", company1ValidEmail)).get checkStatus 200 } @@ -65,10 +64,12 @@ class AuthenticationTest extends PlaySpec with BaseOneAppPerSuite with TestAppli | ], |"settings": { |"test": "test" - |} + | }, + |"booleanAttribute": true, + |"longAttribute1": 9 |} |""".stripMargin)) - .getWithBody checkStatus 403 + .getWithBody checkStatus 200 } "patch" in { diff --git a/test/controllers/CompaniesControllerTest.scala b/test/controllers/CompaniesControllerTest.scala index 40c10a19..5f9ed0e7 100644 --- a/test/controllers/CompaniesControllerTest.scala +++ b/test/controllers/CompaniesControllerTest.scala @@ -1,27 +1,13 @@ package controllers import java.util.UUID - -import com.google.inject.Inject -import com.typesafe.config.Config import de.innfactory.bootstrapplay2.models.api._ import org.scalatestplus.play.{ BaseOneAppPerSuite, PlaySpec } import play.api.libs.json._ -import play.api.mvc.Result -import play.api.test.FakeRequest import play.api.test.Helpers._ -import org.joda.time.DateTime -import org.joda.time.format.DateTimeFormat -import play.api.libs.json.JodaWrites._ -import play.api.libs.json.JodaReads._ -import play.api.libs.json.Reads -import de.innfactory.bootstrapplay2.common.utils.PagedGen._ -import play.api.test.CSRFTokenHelper._ import testutils.BaseFakeRequest import testutils.BaseFakeRequest._ -import scala.concurrent.Future - class CompaniesControllerTest extends PlaySpec with BaseOneAppPerSuite with TestApplicationFactory { /** ———————————————— */ @@ -44,7 +30,7 @@ class CompaniesControllerTest extends PlaySpec with BaseOneAppPerSuite with Test } "get me empty" in { - BaseFakeRequest(GET, "/v1/companies/me").withHeader(("Authorization", "test7@test7.de")).get checkStatus 404 + BaseFakeRequest(GET, "/v1/companies/me").withHeader(("Authorization", "test7@test7.de")).get checkStatus 403 } "get single" in { @@ -69,16 +55,18 @@ class CompaniesControllerTest extends PlaySpec with BaseOneAppPerSuite with Test .withJsonBody(Json.parse(s""" |{ |"firebaseUser": [ - |"test5@test5.de" + |"noUser@noUser.de" | ], |"settings": { |"test": "test" - |} + |}, + |"booleanAttribute": true, + |"longAttribute1": 9 |} |""".stripMargin)) .getWithBody .parseContent[Company] - result.firebaseUser.get.contains("test5@test5.de") mustEqual true + result.firebaseUser.get.contains("noUser@noUser.de") mustEqual true } "post duplicate" in { @@ -92,7 +80,9 @@ class CompaniesControllerTest extends PlaySpec with BaseOneAppPerSuite with Test | ], |"settings": { |"test": "test" - |} + |}, + |"booleanAttribute": true, + |"longAttribute1": 9 |} |""".stripMargin)) .getWithBody checkStatus 400 @@ -106,7 +96,9 @@ class CompaniesControllerTest extends PlaySpec with BaseOneAppPerSuite with Test { | "id": "231f5e3d-31db-4be5-9db9-92955e03507c", | "firebaseUser": ["test@test.de"], - | "settings": {"test2": "test2"} + | "settings": {"test2": "test2"}, + | "booleanAttribute": false, + | "longAttribute1": 15 |} |""".stripMargin)) .getWithBody @@ -126,7 +118,7 @@ class CompaniesControllerTest extends PlaySpec with BaseOneAppPerSuite with Test .get checkStatus 204 BaseFakeRequest(DELETE, "/v1/companies/b492fa98-ab60-4596-ac3c-256cc4957797") .withHeader(("Authorization", "test@test6.de")) - .get checkStatus 404 + .get checkStatus 403 } } diff --git a/test/controllers/CompaniesGraphqlControllerTest.scala b/test/controllers/CompaniesGraphqlControllerTest.scala new file mode 100644 index 00000000..6c2224a3 --- /dev/null +++ b/test/controllers/CompaniesGraphqlControllerTest.scala @@ -0,0 +1,53 @@ +package controllers + +import de.innfactory.bootstrapplay2.models.api._ +import org.scalatestplus.play.{ BaseOneAppPerSuite, PlaySpec } +import play.api.libs.json._ +import play.api.test.Helpers._ +import testutils.BaseFakeRequest +import testutils.BaseFakeRequest._ +import testutils.grapqhl.CompanyRequests +import testutils.grapqhl.FakeGraphQLRequest.{ getFake, routeResult } + +import java.util.UUID + +class CompaniesGraphqlControllerTest extends PlaySpec with BaseOneAppPerSuite with TestApplicationFactory { + + /** ———————————————— */ + /** COMPANIES */ + /** ———————————————— */ + "CompaniesController" must { + + "getAll" in { + val fake = + routeResult( + getFake( + CompanyRequests.CompanyRequest + .getRequest(filter = None) + ) + ) + val content = contentAsJson(fake) + status(fake) mustBe 200 + val parsed = content.as[CompanyRequests.CompanyRequest.CompanyRequestResult] + parsed.data.allCompanies.length mustBe 2 + + } + + "getAll with boolean Filter" in { + val fake = + routeResult( + getFake( + CompanyRequests.CompanyRequest + .getRequest(filter = Some("booleanAttributeEquals=true")) + ) + ) + val content = contentAsJson(fake) + status(fake) mustBe 200 + val parsed = content.as[CompanyRequests.CompanyRequest.CompanyRequestResult] + parsed.data.allCompanies.length mustBe 1 + + } + + } + +} diff --git a/test/controllers/FunctionalSpec.scala b/test/controllers/FunctionalSpec.scala index 001fb0bc..91c3a15c 100644 --- a/test/controllers/FunctionalSpec.scala +++ b/test/controllers/FunctionalSpec.scala @@ -1,23 +1,8 @@ package controllers -import de.innfactory.bootstrapplay2.common.results.errors.Errors.DatabaseError -import de.innfactory.bootstrapplay2.common.utils.PagedGen -import de.innfactory.bootstrapplay2.models.api._ import org.scalatestplus.play.{ BaseOneAppPerSuite, PlaySpec } -import play.api.libs.json._ -import play.api.mvc.Result import play.api.test.FakeRequest import play.api.test.Helpers._ -import org.joda.time.DateTime -import org.joda.time.format.DateTimeFormat -import play.api.libs.json.JodaWrites._ -import play.api.libs.json.JodaReads._ -import play.api.libs.json.Reads -import de.innfactory.bootstrapplay2.common.utils.PagedGen._ -import testutils.BaseFakeRequest -import testutils.BaseFakeRequest._ - -import scala.concurrent.Future /** * Runs a functional test with the application, using an in memory @@ -25,10 +10,6 @@ import scala.concurrent.Future */ class FunctionalSpec extends PlaySpec with BaseOneAppPerSuite with TestApplicationFactory { - implicit val nilReader = Json.reads[scala.collection.immutable.Nil.type] - implicit val nilWriter = Json.writes[scala.collection.immutable.Nil.type] - implicit val dateFormat = "yyyy-MM-dd'T'HH:mm:ss.SSSZ" - "App" should { "work with postgres Database" in { val future = route( @@ -39,32 +20,4 @@ class FunctionalSpec extends PlaySpec with BaseOneAppPerSuite with TestApplicati } } - "SelfLoggingError" should { - "log" in { - DatabaseError("Test", "Test", "Test", "Test") - } - } - - "PagedGen" should { - "generate correct prev and next links" in { - val prevLink = PagedGen.prevGen(8, 5, 12, "test", None) - val prevZeroLink = PagedGen.prevGen(8, 5, 0, "test", None) - val prevOneLink = PagedGen.prevGen(8, 3, 12, "test", None) - val prevTwoLink = PagedGen.prevGen(0, 0, 12, "test", None) - val nextLink = PagedGen.nextGen(8, 5, 12, "test", None) - val nextZeroLink = PagedGen.nextGen(8, 5, 0, "test", None) - val nextOneLink = - PagedGen.nextGen(10, 5, 12, "test", Some("&lat=0&lon=0")) - val nextTwoLink = PagedGen.nextGen(11, 11, 12, "test", None) - prevLink mustEqual "test?startIndex=1&endIndex=4" - prevZeroLink mustEqual "" - prevOneLink mustEqual "test?startIndex=0&endIndex=2" - prevTwoLink mustEqual "" - nextLink mustEqual "test?startIndex=9&endIndex=11" - nextZeroLink mustEqual "" - nextOneLink mustEqual "test?startIndex=11&endIndex=11&lat=0&lon=0" - nextTwoLink mustEqual "" - } - } - } diff --git a/test/resources/application.conf b/test/resources/application.conf index 982adc3f..8f8abba6 100755 --- a/test/resources/application.conf +++ b/test/resources/application.conf @@ -23,3 +23,9 @@ test = { url = "jdbc:postgresql://"${?test.database.host}":"${?test.database.port}"/"${?test.database.db} } } + +opencensus-scala { + trace { + sampling-probability = 1 + } +} diff --git a/test/resources/migration/V999__DATA.sql b/test/resources/migration/V999__DATA.sql index 7350c8e2..8dd13141 100644 --- a/test/resources/migration/V999__DATA.sql +++ b/test/resources/migration/V999__DATA.sql @@ -1,32 +1,55 @@ -INSERT INTO "company" ("id", "firebase_user", "settings", "created", "updated") VALUES ( - '231f5e3d-31db-4be5-9db9-92955e03507c', - '{"test@test.de","test2@test.de"}', - JSON '{"region": "region"}', - CURRENT_TIMESTAMP, - CURRENT_TIMESTAMP -); +INSERT INTO "company" ("id", + "firebase_user", + "settings", + "created", + "updated", + "string_attribute_1", + "string_attribute_2", + "long_attribute_1", + "boolean_attribute") +VALUES ('231f5e3d-31db-4be5-9db9-92955e03507c', + '{"test@test.de","test2@test.de"}', + JSON '{"region": "region"}', + CURRENT_TIMESTAMP, + CURRENT_TIMESTAMP, + 'notest1', + 'notest2', + 10, + true); -INSERT INTO "company" ("id", "firebase_user", "settings", "created", "updated") VALUES ( - 'b492fa98-ab60-4596-ac3c-256cc4957797', - '{"test@test6.de","test2@test6.de"}', - JSON '{"region": "region"}', - CURRENT_TIMESTAMP, - CURRENT_TIMESTAMP -); +INSERT INTO "company" ("id", + "firebase_user", + "settings", + "created", + "updated", + "string_attribute_1", + "string_attribute_2", + "long_attribute_1", + "boolean_attribute") +VALUES ('b492fa98-ab60-4596-ac3c-256cc4957797', + '{"test@test6.de","test2@test6.de"}', + JSON '{"region": "region"}', + CURRENT_TIMESTAMP, + CURRENT_TIMESTAMP, + 'test1', + 'test2', + 5, + false + ); -INSERT INTO "location" ("company", "name", "settings","position", "address_line_1", "address_line_2","city", "zip", "country", "created", "updated") VALUES ( - '231f5e3d-31db-4be5-9db9-92955e03507c', - 'Location-1', - JSON '{"location": "location"}', - ST_GeomFromText('POINT(0.0 0.0)', 4326), - 'location_1_address_line_1', - 'location_1_address_line_2', - 'city1', - 'zip1', - 'country1', - CURRENT_TIMESTAMP, - CURRENT_TIMESTAMP -); +INSERT INTO "location" ("company", "name", "settings", "position", "address_line_1", "address_line_2", "city", "zip", + "country", "created", "updated") +VALUES ('231f5e3d-31db-4be5-9db9-92955e03507c', + 'Location-1', + JSON '{"location": "location"}', + ST_GeomFromText('POINT(0.0 0.0)', 4326), + 'location_1_address_line_1', + 'location_1_address_line_2', + 'city1', + 'zip1', + 'country1', + CURRENT_TIMESTAMP, + CURRENT_TIMESTAMP); diff --git a/test/testutils/grapqhl/CompanyRequests.scala b/test/testutils/grapqhl/CompanyRequests.scala new file mode 100644 index 00000000..f5d47abe --- /dev/null +++ b/test/testutils/grapqhl/CompanyRequests.scala @@ -0,0 +1,39 @@ +package testutils.grapqhl + +import de.innfactory.bootstrapplay2.models.api.Company +import play.api.libs.json.{ JsObject, Json } + +object CompanyRequests { + + object CompanyRequest { + private val body = Json.parse("""{"operationName":"Companies"}""") + + implicit val writesData = Json.reads[Data] + implicit val writesCompanyRequestResult = Json.reads[CompanyRequestResult] + + case class Data(allCompanies: List[Company]) + + case class CompanyRequestResult(data: Data) + + def getRequest(filter: Option[String]): JsObject = { + val addition = if (filter.isDefined) "( filter: \"" + filter.get + "\")" else "" + body.as[JsObject] ++ Json.obj( + "query" -> + s"""query Companies { + | allCompanies$addition { + | id + | firebaseUser + | settings + | stringAttribute1 + | stringAttribute2 + | longAttribute1 + | booleanAttribute + | created + | updated + | } + |}""".stripMargin + ) + } + } + +} diff --git a/test/testutils/grapqhl/FakeGraphQLRequest.scala b/test/testutils/grapqhl/FakeGraphQLRequest.scala new file mode 100644 index 00000000..c60cc468 --- /dev/null +++ b/test/testutils/grapqhl/FakeGraphQLRequest.scala @@ -0,0 +1,19 @@ +package testutils.grapqhl + +import play.api.Application +import play.api.libs.json.JsObject +import play.api.mvc.{ Headers, Result } +import play.api.test.FakeRequest +import play.api.test.Helpers.{ route, POST } + +import scala.concurrent.Future + +object FakeGraphQLRequest { + def getFake(body: JsObject, headers: (String, String)*)(implicit app: Application): FakeRequest[JsObject] = + FakeRequest(POST, "/graphql") + .withBody(body) + .withHeaders(new Headers(headers)) + + def routeResult(fakeRequest: FakeRequest[JsObject])(implicit app: Application): Future[Result] = + route(app, fakeRequest).get +}