From f3f5108a6c9700a8153e4a9fcceb4a70e46b3e80 Mon Sep 17 00:00:00 2001 From: patsta32 Date: Thu, 18 Feb 2021 12:14:31 +0100 Subject: [PATCH 1/4] IF-60 update to V3 --- .DS_Store | Bin 6148 -> 6148 bytes app/Module.scala | 111 +++++++-- .../actions/CompanyForUserExtractAction.scala | 49 +++- .../actions/JwtValidationAction.scala | 68 ++---- .../actions/TracingCompanyAction.scala | 19 ++ .../services/HelloWorldService.scala | 2 +- .../CompanyAuthorizationMethods.scala | 35 +-- .../LocationAuthorizationMethods.scala | 40 ++-- .../common/daos/BaseSlickDAO.scala | 138 ----------- .../filteroptions/FilterOptionUtils.scala | 46 ++++ .../filteroptions/FilterOptionsConfig.scala | 20 ++ .../implicits/EitherTTracingImplicits.scala | 30 +++ .../implicits/FutureTracingImplicits.scala | 30 +++ .../bootstrapplay2/common/implicits/JWT.scala | 16 ++ .../RequestToRequestContextImplicit.scala | 43 ++++ .../common/logging/ImplicitLogContext.scala | 12 + .../common/logging/LoggingEnhancer.scala | 29 +++ .../common/repositories/All.scala | 10 + .../common/repositories/Delete.scala | 9 + .../common/repositories/Lookup.scala | 9 + .../common/repositories/Patch.scala | 9 + .../common/repositories/Post.scala | 9 + .../common/request/RequestContext.scala | 54 +++++ .../common/request/logger/TraceLogger.scala | 29 +++ .../common/results/Results.scala | 32 +-- .../common/results/errors/Errors.scala | 12 +- .../common/tracing/Common.scala | 17 ++ .../common/utils/CompanyUtils.scala | 16 +- .../common/utils/OptionUtils.scala | 4 +- .../controllers/CompaniesController.scala | 47 ++-- .../controllers/LocationsController.scala | 54 ++--- .../bootstrapplay2/db/BaseSlickDAO.scala | 219 ++++++++++++++++++ .../bootstrapplay2/db/CompaniesDAO.scala | 104 ++++++--- .../bootstrapplay2/db/LocationsDAO.scala | 32 +-- .../filters/TracingFilter.scala | 86 +++++++ .../graphql/ErrorParserImpl.scala | 6 +- .../graphql/GraphQLExecutionContext.scala | 3 + .../graphql/RequestExecutor.scala | 7 +- .../graphql/schema/SchemaDefinition.scala | 2 +- .../graphql/schema/models/Arguments.scala | 12 + .../graphql/schema/models/Companies.scala | 2 +- .../graphql/schema/models/Locations.scala | 9 +- .../graphql/schema/queries/Company.scala | 14 +- .../bootstrapplay2/models/api/Company.scala | 10 +- .../bootstrapplay2/models/api/Location.scala | 3 +- .../repositories/CompaniesRepository.scala | 77 +++--- .../repositories/LocationRepository.scala | 63 +++-- .../websockets/actors/WebSocketActor.scala | 1 - build.sbt | 1 - conf/application.conf | 10 +- conf/db/migration/V1__Tables.sql | 4 + conf/logback.xml | 6 + project/Dependencies.scala | 17 +- project/plugins.sbt | 2 +- test/controllers/AuthenticationTest.scala | 8 +- .../controllers/CompaniesControllerTest.scala | 20 +- .../CompaniesGraphqlControllerTest.scala | 53 +++++ test/controllers/FunctionalSpec.scala | 4 +- test/resources/application.conf | 6 + test/resources/migration/V999__DATA.sql | 77 +++--- test/testutils/grapqhl/CompanyRequests.scala | 39 ++++ .../grapqhl/FakeGraphQLRequest.scala | 19 ++ 62 files changed, 1392 insertions(+), 523 deletions(-) create mode 100644 app/de/innfactory/bootstrapplay2/actions/TracingCompanyAction.scala delete mode 100644 app/de/innfactory/bootstrapplay2/common/daos/BaseSlickDAO.scala create mode 100644 app/de/innfactory/bootstrapplay2/common/filteroptions/FilterOptionUtils.scala create mode 100644 app/de/innfactory/bootstrapplay2/common/filteroptions/FilterOptionsConfig.scala create mode 100644 app/de/innfactory/bootstrapplay2/common/implicits/EitherTTracingImplicits.scala create mode 100644 app/de/innfactory/bootstrapplay2/common/implicits/FutureTracingImplicits.scala create mode 100644 app/de/innfactory/bootstrapplay2/common/implicits/JWT.scala create mode 100644 app/de/innfactory/bootstrapplay2/common/implicits/RequestToRequestContextImplicit.scala create mode 100644 app/de/innfactory/bootstrapplay2/common/logging/ImplicitLogContext.scala create mode 100644 app/de/innfactory/bootstrapplay2/common/logging/LoggingEnhancer.scala create mode 100644 app/de/innfactory/bootstrapplay2/common/repositories/All.scala create mode 100644 app/de/innfactory/bootstrapplay2/common/repositories/Delete.scala create mode 100644 app/de/innfactory/bootstrapplay2/common/repositories/Lookup.scala create mode 100644 app/de/innfactory/bootstrapplay2/common/repositories/Patch.scala create mode 100644 app/de/innfactory/bootstrapplay2/common/repositories/Post.scala create mode 100644 app/de/innfactory/bootstrapplay2/common/request/RequestContext.scala create mode 100644 app/de/innfactory/bootstrapplay2/common/request/logger/TraceLogger.scala create mode 100644 app/de/innfactory/bootstrapplay2/common/tracing/Common.scala create mode 100644 app/de/innfactory/bootstrapplay2/db/BaseSlickDAO.scala create mode 100644 app/de/innfactory/bootstrapplay2/filters/TracingFilter.scala create mode 100644 app/de/innfactory/bootstrapplay2/graphql/schema/models/Arguments.scala create mode 100644 test/controllers/CompaniesGraphqlControllerTest.scala create mode 100644 test/testutils/grapqhl/CompanyRequests.scala create mode 100644 test/testutils/grapqhl/FakeGraphQLRequest.scala diff --git a/.DS_Store b/.DS_Store index 5c9bed05d5a7fbc92dec10b578a1fb8165e79871..9117d6658c0592cbac6756074dc89e8d0971d114 100644 GIT binary patch delta 124 zcmZoMXfc@J&&aVcU^g=($7UXu&y1363?&SS3`Gp-45 + 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("bootstrap-play2") + .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..cc1d03a9 100644 --- a/app/de/innfactory/bootstrapplay2/actions/CompanyForUserExtractAction.scala +++ b/app/de/innfactory/bootstrapplay2/actions/CompanyForUserExtractAction.scala @@ -1,35 +1,60 @@ 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.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, Unauthorized } +import play.api.mvc.{ AnyContent, 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("").asLeft[RequestWithCompany[A]] + } + case None => + Future( + 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..9c96c0b6 --- /dev/null +++ b/app/de/innfactory/bootstrapplay2/common/filteroptions/FilterOptionsConfig.scala @@ -0,0 +1,20 @@ +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") + ) + +} 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/Results.scala b/app/de/innfactory/bootstrapplay2/common/results/Results.scala index 92cce531..d1752b65 100644 --- a/app/de/innfactory/bootstrapplay2/common/results/Results.scala +++ b/app/de/innfactory/bootstrapplay2/common/results/Results.scala @@ -11,7 +11,7 @@ object Results { private val logger = Logger("application") - trait ErrorStatus + trait ResultStatus /** * Extend from this error class to have the error logging itself @@ -21,13 +21,13 @@ object Results { * @param errorMethod * @param internalErrorMessage */ - abstract class SelfLoggingError( + abstract class SelfLoggingResult( message: String, statusCode: Int, errorClass: String, errorMethod: String, internalErrorMessage: String - ) extends ErrorStatus { + ) extends ResultStatus { var currentStackTrace = new Throwable() logger.error( s"DatabaseError | message=$message statusCode=$statusCode | Error in class $errorClass in method $errorMethod $internalErrorMessage!", @@ -35,18 +35,18 @@ object Results { ) } - abstract class NotLoggingError() extends ErrorStatus + abstract class NotLoggingResult() extends ResultStatus - 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 _: DatabaseResult => MvcResults.Status(500)("") + case _: Forbidden => MvcResults.Status(403)("") + case _: BadRequest => MvcResults.Status(400)("") + case _: NotFound => MvcResults.Status(404)("") + case _ => MvcResults.Status(400)("") } } @@ -54,24 +54,24 @@ object Results { def toJson: JsValue = Json.toJson(value.map(_.toJson)) } - implicit class RichResult(value: Future[Either[ErrorStatus, ApiBaseModel]])(implicit ec: ExecutionContext) { + implicit class RichResult(value: Future[Either[ResultStatus, ApiBaseModel]])(implicit ec: ExecutionContext) { def completeResult(statusCode: Int = 200): Future[play.api.mvc.Result] = value.map { - case Left(error: ErrorStatus) => error.mapToResult + case Left(error: ResultStatus) => error.mapToResult case Right(value: ApiBaseModel) => MvcResults.Status(statusCode)(value.toJson) } def completeResultWithoutBody(statusCode: Int = 200): Future[play.api.mvc.Result] = value.map { - case Left(error: ErrorStatus) => error.mapToResult + case Left(error: ResultStatus) => error.mapToResult case Right(value: ApiBaseModel) => MvcResults.Status(statusCode)("") } } - implicit class RichSeqResult(value: Future[Either[ErrorStatus, Seq[ApiBaseModel]]])(implicit ec: ExecutionContext) { + implicit class RichSeqResult(value: Future[Either[ResultStatus, Seq[ApiBaseModel]]])(implicit ec: ExecutionContext) { def completeResult: Future[play.api.mvc.Result] = value.map { - case Left(error: ErrorStatus) => error.mapToResult + case Left(error: ResultStatus) => error.mapToResult case Right(value: Seq[ApiBaseModel]) => MvcResults.Status(200)(value.toJson) } } diff --git a/app/de/innfactory/bootstrapplay2/common/results/errors/Errors.scala b/app/de/innfactory/bootstrapplay2/common/results/errors/Errors.scala index bb4273e9..b6e62191 100644 --- a/app/de/innfactory/bootstrapplay2/common/results/errors/Errors.scala +++ b/app/de/innfactory/bootstrapplay2/common/results/errors/Errors.scala @@ -1,15 +1,15 @@ package de.innfactory.bootstrapplay2.common.results.errors -import de.innfactory.bootstrapplay2.common.results.Results.{ NotLoggingError, SelfLoggingError } +import de.innfactory.bootstrapplay2.common.results.Results.{ NotLoggingResult, SelfLoggingResult } object Errors { - case class DatabaseError(message: String, errorClass: String, errorMethod: String, internalErrorMessage: String) - extends SelfLoggingError(message, 400, errorClass, errorMethod, internalErrorMessage) + case class DatabaseResult(message: String, errorClass: String, errorMethod: String, internalErrorMessage: String) + extends SelfLoggingResult(message, 400, errorClass, errorMethod, internalErrorMessage) - case class BadRequest() extends NotLoggingError() + case class BadRequest() extends NotLoggingResult() - case class NotFound() extends NotLoggingError() + case class NotFound() extends NotLoggingResult() - case class Forbidden() extends NotLoggingError() + case class Forbidden() 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/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/controllers/CompaniesController.scala b/app/de/innfactory/bootstrapplay2/controllers/CompaniesController.scala index 383a74db..37ab3f22 100644 --- a/app/de/innfactory/bootstrapplay2/controllers/CompaniesController.scala +++ b/app/de/innfactory/bootstrapplay2/controllers/CompaniesController.scala @@ -1,11 +1,12 @@ package de.innfactory.bootstrapplay2.controllers import java.util.UUID - -import de.innfactory.bootstrapplay2.actions.{ CompanyForUserExtractAction, JwtValidationAction } +import de.innfactory.bootstrapplay2.actions.{ CompanyForUserExtractAction, JwtValidationAction, 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._ @@ -18,53 +19,49 @@ 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 Single Company").async { implicit request => + companiesRepository.lookup(UUID.fromString(id))(requestContextWithCompany).completeResult() } 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 stock = json.as[Company] + val result: EitherT[Future, ResultStatus, Company] = for { _ <- EitherT(Future(json.validateFor)) - created <- EitherT(companiesRepository.patch(stock, request)) + created <- EitherT(companiesRepository.patch(stock)(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 stock = 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(stock)(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/LocationsController.scala b/app/de/innfactory/bootstrapplay2/controllers/LocationsController.scala index cbf0005a..83af6c75 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 stock = 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(stock)(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 stock = json.as[Location] + val result: EitherT[Future, ResultStatus, Location] = for { + _ <- EitherT(Future(json.validateFor[Location])) + created <- EitherT(locationRepository.post(stock)(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..df30c030 --- /dev/null +++ b/app/de/innfactory/bootstrapplay2/db/BaseSlickDAO.scala @@ -0,0 +1,219 @@ +package de.innfactory.bootstrapplay2.db + +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.{ Result, ResultStatus } +import de.innfactory.bootstrapplay2.common.results.errors.Errors.{ BadRequest, DatabaseResult, 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 de.innfactory.bootstrapplay2.common.implicits.FutureTracingImplicits.EnhancedFuture +import de.innfactory.bootstrapplay2.common.logging.ImplicitLogContext +import de.innfactory.bootstrapplay2.common.request.{ RequestContext, TraceContext } +import slick.dbio.{ DBIOAction, Effect, NoStream } + +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 with ImplicitLogContext { + + val currentClassForDatabaseError = "BaseSlickDAO" + + 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 + Left( + DatabaseResult("Could not replace entity", currentClassForDatabaseError, "update", "row not updated") + ) + } + .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 + Left( + DatabaseResult( + "could not delete entity", + currentClassForDatabaseError, + "delete", + "entity was deleted" + ) + ) + } + .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..26d1345a 100644 --- a/app/de/innfactory/bootstrapplay2/db/CompaniesDAO.scala +++ b/app/de/innfactory/bootstrapplay2/db/CompaniesDAO.scala @@ -1,21 +1,27 @@ 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 javax.inject.{Inject, Singleton} import slick.jdbc.JdbcBackend.Database import play.api.libs.json.Json -import de.innfactory.bootstrapplay2.models.api.{ Company => CompanyObject } +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 scala.concurrent.{ ExecutionContext, Future } +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 scala.concurrent.{ExecutionContext, Future} import scala.language.implicitConversions /** @@ -24,17 +30,21 @@ import scala.language.implicitConversions @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 internal_lookupByEmail(email: String)(implicit tc: TraceContext): Future[Option[CompanyObject]] - def create(CompanyObject: CompanyObject): Future[Result[CompanyObject]] + def create(CompanyObject: CompanyObject)(implicit tc: TraceContext): Future[Result[CompanyObject]] - def update(CompanyObject: CompanyObject): Future[Result[CompanyObject]] + def update(CompanyObject: CompanyObject)(implicit tc: TraceContext): Future[Result[CompanyObject]] - def delete(id: UUID): Future[Result[Boolean]] + def delete(id: UUID)(implicit tc: TraceContext): Future[Result[Boolean]] def close(): Future[Unit] } @@ -51,7 +61,7 @@ trait CompaniesDAO { @Singleton class SlickCompaniesSlickDAO @Inject() (db: Database)(implicit ec: ExecutionContext) extends BaseSlickDAO(db) - with CompaniesDAO { + with CompaniesDAO with ImplicitLogContext { // Class Name for identification in Database Errors override val currentClassForDatabaseError = "SlickCompaniesDAO" @@ -61,10 +71,10 @@ class SlickCompaniesSlickDAO @Inject() (db: Database)(implicit ec: ExecutionCont /* - - - 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 } @@ -75,23 +85,35 @@ class SlickCompaniesSlickDAO @Inject() (db: Database)(implicit ec: ExecutionCont * @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 all(implicit tc: TraceContext): Future[Seq[CompanyObject]] = lookupSequenceGenericRawSequence( - Company.result + Tables.Company.result ) + 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)) + /** * Lookup Company by Email * @param email * @return */ - def internal_lookupByEmail(email: String): Future[Option[CompanyObject]] = { - val f: Future[Option[CompanyRow]] = + 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) => @@ -106,8 +128,8 @@ class SlickCompaniesSlickDAO @Inject() (db: Database)(implicit ec: ExecutionCont * @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)), @@ -119,8 +141,8 @@ class SlickCompaniesSlickDAO @Inject() (db: Database)(implicit ec: ExecutionCont * @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 ) @@ -130,29 +152,37 @@ class SlickCompaniesSlickDAO @Inject() (db: Database)(implicit ec: ExecutionCont * @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/LocationsDAO.scala b/app/de/innfactory/bootstrapplay2/db/LocationsDAO.scala index 2d8902f4..2a3596b5 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 @@ -25,23 +25,23 @@ import scala.language.implicitConversions @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] } @@ -88,7 +88,7 @@ class SlickLocationsDAO @Inject() (db: Database)(implicit ec: ExecutionContext) * @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 ) @@ -98,7 +98,7 @@ class SlickLocationsDAO @Inject() (db: Database)(implicit ec: ExecutionContext) * @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)) @@ -113,7 +113,7 @@ class SlickLocationsDAO @Inject() (db: Database)(implicit ec: ExecutionContext) */ def lookupByCompany( companyId: UUID - ): Future[Result[Seq[LocationObject]]] = + )(implicit tc: TraceContext): Future[Result[Seq[LocationObject]]] = lookupSequenceGeneric[LocationRow, LocationObject]( queryByCompany(companyId).result ) @@ -129,7 +129,7 @@ class SlickLocationsDAO @Inject() (db: Database)(implicit ec: ExecutionContext) 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)) @@ -141,7 +141,7 @@ class SlickLocationsDAO @Inject() (db: Database)(implicit ec: ExecutionContext) * @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) => @@ -154,7 +154,7 @@ class SlickLocationsDAO @Inject() (db: Database)(implicit ec: ExecutionContext) * @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 @@ -165,7 +165,7 @@ class SlickLocationsDAO @Inject() (db: Database)(implicit ec: ExecutionContext) * @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..ff8fb6db 100644 --- a/app/de/innfactory/bootstrapplay2/graphql/schema/models/Companies.scala +++ b/app/de/innfactory/bootstrapplay2/graphql/schema/models/Companies.scala @@ -11,6 +11,6 @@ object Companies { ReplaceField( fieldName = "id", field = Field(name = "id", fieldType = StringType, resolve = c => c.value.id.get.toString) - ) + ), ) } diff --git a/app/de/innfactory/bootstrapplay2/graphql/schema/models/Locations.scala b/app/de/innfactory/bootstrapplay2/graphql/schema/models/Locations.scala index 6b6d1aa2..a30fc8bf 100644 --- a/app/de/innfactory/bootstrapplay2/graphql/schema/models/Locations.scala +++ b/app/de/innfactory/bootstrapplay2/graphql/schema/models/Locations.scala @@ -1,8 +1,9 @@ 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 } +import sangria.macros.derive.{ReplaceField, deriveObjectType} +import sangria.schema.{BigDecimalType, Field, ObjectType, OptionType, StringType} import de.innfactory.grapqhl.sangria.implicits.JsonScalarType._ import de.innfactory.grapqhl.sangria.implicits.JodaScalarType._ @@ -21,11 +22,13 @@ 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..ee68c049 100644 --- a/app/de/innfactory/bootstrapplay2/graphql/schema/queries/Company.scala +++ b/app/de/innfactory/bootstrapplay2/graphql/schema/queries/Company.scala @@ -1,15 +1,25 @@ 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), + 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("Familotel Filter API hotels query. Query group by id") ) diff --git a/app/de/innfactory/bootstrapplay2/models/api/Company.scala b/app/de/innfactory/bootstrapplay2/models/api/Company.scala index 94161f29..4674d99b 100644 --- a/app/de/innfactory/bootstrapplay2/models/api/Company.scala +++ b/app/de/innfactory/bootstrapplay2/models/api/Company.scala @@ -17,10 +17,14 @@ 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) + override def toJson: JsValue = Json.toJson(this)(Company.writes) } object Company { @@ -33,6 +37,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..4ee405cd 100644 --- a/app/de/innfactory/bootstrapplay2/models/api/Location.scala +++ b/app/de/innfactory/bootstrapplay2/models/api/Location.scala @@ -30,8 +30,7 @@ case class Location( updated: Option[DateTime], distance: Option[Float] ) extends ApiBaseModel { - val writes: Writes[Location] = implicitly - override def toJson: JsValue = Json.toJson(this)(writes) + override def toJson: JsValue = Json.toJson(this)(Location.writes) } object Location { diff --git a/app/de/innfactory/bootstrapplay2/repositories/CompaniesRepository.scala b/app/de/innfactory/bootstrapplay2/repositories/CompaniesRepository.scala index d2a61025..772c7223 100644 --- a/app/de/innfactory/bootstrapplay2/repositories/CompaniesRepository.scala +++ b/app/de/innfactory/bootstrapplay2/repositories/CompaniesRepository.scala @@ -1,71 +1,96 @@ 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 } +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 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]] } 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 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..69f30016 100755 --- a/build.sbt +++ b/build.sbt @@ -55,7 +55,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 = { diff --git a/conf/application.conf b/conf/application.conf index 6719e00e..bbc0e10f 100755 --- a/conf/application.conf +++ b/conf/application.conf @@ -66,7 +66,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 +82,10 @@ 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 + +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/project/Dependencies.scala b/project/Dependencies.scala index d8267374..cbc6dbda 100644 --- a/project/Dependencies.scala +++ b/project/Dependencies.scala @@ -6,6 +6,7 @@ object Dependencies { val scalaVersion = "2.13.3" val akkaVersion = "2.6.10" val akkaTyped = "com.typesafe.akka" %% "akka-actor-typed" % akkaVersion + val akkaHttp = "com.typesafe.akka" %% "akka-http" % "10.1.12" val akka = "com.typesafe.akka" %% "akka-actor" % akkaVersion val akkaJackson = "com.typesafe.akka" %% "akka-serialization-jackson" % akkaVersion // https://github.com/akka/akka/issues/29351 @@ -13,7 +14,7 @@ object Dependencies { // 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.4" @@ -34,8 +35,22 @@ object Dependencies { val playAhcWS = "com.typesafe.play" %% "play-ahc-ws" % "2.8.5" % 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" + + + // If you want to use opencensus-scala inside an Akka HTTP project + val opencensusAkkaHttp = "com.github.sebruck" %% "opencensus-scala-akka-http" % "0.7.2" + lazy val list = Seq( scalaUtil, + akkaHttp, + opencensusStackdriver, + opencensusLoggging, + opencensusJaeger, + opencensusAkkaHttp, akka, akkaTyped, akkaJackson, diff --git a/project/plugins.sbt b/project/plugins.sbt index 1476d1b2..47236ee9 100755 --- a/project/plugins.sbt +++ b/project/plugins.sbt @@ -4,7 +4,7 @@ resolvers += "Flyway" at "https://davidmweber.github.io/flyway-sbt.repo" resolvers += Resolver.url("play-sbt-plugins", url("https://dl.bintray.com/playframework/sbt-plugin-releases/"))(Resolver.ivyStylePatterns) // Database migration -addSbtPlugin("io.github.davidmweber" % "flyway-sbt" % "6.5.0") +addSbtPlugin("io.github.davidmweber" % "flyway-sbt" % "7.4.0") // Slick code generation addSbtPlugin("com.github.tototoshi" % "sbt-slick-codegen" % "1.4.0") diff --git a/test/controllers/AuthenticationTest.scala b/test/controllers/AuthenticationTest.scala index b0a6b887..76e7f225 100644 --- a/test/controllers/AuthenticationTest.scala +++ b/test/controllers/AuthenticationTest.scala @@ -44,7 +44,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 +65,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..2e4cb72e 100644 --- a/test/controllers/CompaniesControllerTest.scala +++ b/test/controllers/CompaniesControllerTest.scala @@ -44,7 +44,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 +69,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 +94,9 @@ class CompaniesControllerTest extends PlaySpec with BaseOneAppPerSuite with Test | ], |"settings": { |"test": "test" - |} + |}, + |"booleanAttribute": true, + |"longAttribute1": 9 |} |""".stripMargin)) .getWithBody checkStatus 400 @@ -106,7 +110,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 +132,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..1872cb2c 100644 --- a/test/controllers/FunctionalSpec.scala +++ b/test/controllers/FunctionalSpec.scala @@ -1,6 +1,6 @@ package controllers -import de.innfactory.bootstrapplay2.common.results.errors.Errors.DatabaseError +import de.innfactory.bootstrapplay2.common.results.errors.Errors.DatabaseResult import de.innfactory.bootstrapplay2.common.utils.PagedGen import de.innfactory.bootstrapplay2.models.api._ import org.scalatestplus.play.{ BaseOneAppPerSuite, PlaySpec } @@ -41,7 +41,7 @@ class FunctionalSpec extends PlaySpec with BaseOneAppPerSuite with TestApplicati "SelfLoggingError" should { "log" in { - DatabaseError("Test", "Test", "Test", "Test") + DatabaseResult("Test", "Test", "Test", "Test") } } 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 +} From c20ea588c8da9913779a21c63cdd39c94d4661d8 Mon Sep 17 00:00:00 2001 From: patsta32 Date: Mon, 19 Apr 2021 13:04:36 +0200 Subject: [PATCH 2/4] update dependencies --- project/Dependencies.scala | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/project/Dependencies.scala b/project/Dependencies.scala index 3af511c2..0c7003f8 100644 --- a/project/Dependencies.scala +++ b/project/Dependencies.scala @@ -6,7 +6,7 @@ object Dependencies { val scalaVersion = "2.13.3" val akkaVersion = "2.6.12" val akkaTyped = "com.typesafe.akka" %% "akka-actor-typed" % akkaVersion - val akkaHttp = "com.typesafe.akka" %% "akka-http" % "10.1.12" + val akkaHttp = "com.typesafe.akka" %% "akka-http" % "10.1.13" val akka = "com.typesafe.akka" %% "akka-actor" % akkaVersion val akkaJackson = "com.typesafe.akka" %% "akka-serialization-jackson" % akkaVersion // https://github.com/akka/akka/issues/29351 @@ -26,7 +26,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.2" + 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.17" val cats = "org.typelevel" %% "cats-core" % "2.4.1" @@ -40,6 +40,7 @@ object Dependencies { 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" @@ -49,6 +50,7 @@ object Dependencies { akkaHttp, opencensusStackdriver, opencensusLoggging, + opencensusStatsStackdriver, opencensusJaeger, opencensusAkkaHttp, akka, From 012c997894d23f9b5dd9f46cdfb8dbf2f3f566ff Mon Sep 17 00:00:00 2001 From: patsta32 Date: Mon, 19 Apr 2021 15:20:00 +0200 Subject: [PATCH 3/4] update --- {local-runner => .bin}/docker-compose.yml | 0 {local-runner => .bin}/mkdocs.yml | 0 {local-runner => .bin}/runFor.sh | 2 +- {local-runner => .bin}/runForDoc.md | 2 +- .deployment/runtest.sh => .bin/test-local.sh | 0 .bsp/sbt.json | 2 +- .gitignore | 5 +- app/Module.scala | 2 +- .../actions/CompanyForUserExtractAction.scala | 9 +- .../common/results/ErrorResponse.scala | 18 ++++ .../common/results/Results.scala | 85 +++++++++---------- .../common/results/errors/Errors.scala | 28 ++++-- .../common/utils/NilUtils.scala | 8 -- .../common/utils/PagedData.scala | 71 ---------------- .../controllers/CompaniesController.scala | 23 ++--- .../controllers/HealthController.scala | 12 ++- .../controllers/LocationsController.scala | 6 +- .../bootstrapplay2/db/BaseSlickDAO.scala | 33 ++----- .../bootstrapplay2/db/CompaniesDAO.scala | 40 --------- .../db/DatabaseHealthSocket.scala | 19 +++++ .../bootstrapplay2/db/LocationsDAO.scala | 52 ------------ .../models/api/ApiBaseModel.scala | 7 -- .../bootstrapplay2/models/api/Company.scala | 7 +- .../bootstrapplay2/models/api/Location.scala | 7 +- build.sbt | 33 ++++--- conf/application.conf | 19 ++--- conf/swagger.yml | 2 +- .../db/codegen/CustomizedCodeGenerator.scala | 6 +- project/Build.scala | 4 +- project/Dependencies.scala | 11 ++- test/controllers/AuthenticationTest.scala | 1 - .../controllers/CompaniesControllerTest.scala | 14 --- test/controllers/FunctionalSpec.scala | 47 ---------- 33 files changed, 182 insertions(+), 393 deletions(-) rename {local-runner => .bin}/docker-compose.yml (100%) rename {local-runner => .bin}/mkdocs.yml (100%) rename {local-runner => .bin}/runFor.sh (99%) rename {local-runner => .bin}/runForDoc.md (94%) rename .deployment/runtest.sh => .bin/test-local.sh (100%) create mode 100644 app/de/innfactory/bootstrapplay2/common/results/ErrorResponse.scala delete mode 100644 app/de/innfactory/bootstrapplay2/common/utils/NilUtils.scala delete mode 100644 app/de/innfactory/bootstrapplay2/common/utils/PagedData.scala create mode 100644 app/de/innfactory/bootstrapplay2/db/DatabaseHealthSocket.scala delete mode 100644 app/de/innfactory/bootstrapplay2/models/api/ApiBaseModel.scala 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 4bae97bd..cb536e81 100644 --- a/.bsp/sbt.json +++ b/.bsp/sbt.json @@ -1 +1 @@ -{"name":"sbt","version":"1.4.7","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 a82fbd75..f76a06c1 100755 --- a/app/Module.scala +++ b/app/Module.scala @@ -110,7 +110,7 @@ class StackdriverTracingCreator @Inject() (lifecycle: ApplicationLifecycle, conf val credentials: GoogleCredentials = GoogleCredentials.fromStream(serviceAccount) val stackDriverTraceExporterConfig: StackdriverTraceConfiguration = StackdriverTraceConfiguration .builder() - .setProjectId("bootstrap-play2") + .setProjectId(config.getString("project.id")) .setCredentials(credentials) .setFixedAttributes( Map( diff --git a/app/de/innfactory/bootstrapplay2/actions/CompanyForUserExtractAction.scala b/app/de/innfactory/bootstrapplay2/actions/CompanyForUserExtractAction.scala index cc1d03a9..dc914111 100644 --- a/app/de/innfactory/bootstrapplay2/actions/CompanyForUserExtractAction.scala +++ b/app/de/innfactory/bootstrapplay2/actions/CompanyForUserExtractAction.scala @@ -4,13 +4,14 @@ 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 de.innfactory.play.tracing.{ RequestWithTrace, TraceRequest, UserExtractionActionBase } import io.opencensus.trace.Span import play.api.Environment -import play.api.mvc.Results.{ Forbidden, Unauthorized } -import play.api.mvc.{ AnyContent, BodyParsers, Request, Result, WrappedRequest } +import play.api.mvc.Results.Forbidden +import play.api.mvc.{ BodyParsers, Request, Result, WrappedRequest } import scala.concurrent.{ ExecutionContext, Future } @@ -49,11 +50,11 @@ class CompanyForUserExtractAction @Inject() ( request.request, request.traceSpan ).asRight[Result] - case None => Forbidden("").asLeft[RequestWithCompany[A]] + case None => Forbidden(ErrorResponse.fromMessage("Forbidden")).asLeft[RequestWithCompany[A]] } case None => Future( - Forbidden("").asLeft[RequestWithCompany[A]] + Forbidden(ErrorResponse.fromMessage("Forbidden")).asLeft[RequestWithCompany[A]] ) } }.flatten 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 d1752b65..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 - /** - * Extend from this error class to have the error logging itself - * @param message - * @param statusCode - * @param errorClass - * @param errorMethod - * @param internalErrorMessage - */ - abstract class SelfLoggingResult( - message: String, - statusCode: Int, - errorClass: String, - errorMethod: String, - internalErrorMessage: String - ) extends ResultStatus { - 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 NotLoggingResult() extends ResultStatus - type Result[T] = Either[ResultStatus, T] implicit class RichError(value: ResultStatus)(implicit ec: ExecutionContext) { def mapToResult: play.api.mvc.Result = value match { - case _: DatabaseResult => MvcResults.Status(500)("") - case _: Forbidden => MvcResults.Status(403)("") - case _: BadRequest => MvcResults.Status(400)("") - case _: NotFound => MvcResults.Status(404)("") + 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[ResultStatus, 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: ResultStatus) => 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: ResultStatus) => error.mapToResult - case Right(value: ApiBaseModel) => MvcResults.Status(statusCode)("") + case Left(error: ResultStatus) => error.mapToResult + case Right(_: T) => MvcResults.Status(statusCode)("") } } - implicit class RichSeqResult(value: Future[Either[ResultStatus, Seq[ApiBaseModel]]])(implicit ec: ExecutionContext) { - def completeResult: Future[play.api.mvc.Result] = + 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[ApiBaseModel]) => MvcResults.Status(200)(value.toJson) + case Left(error: ResultStatus) => error.mapToResult + case Right(value: Seq[T]) => MvcResults.Status(200)(Json.toJson(value)) + } + } + + 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: 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 b6e62191..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.{ NotLoggingResult, SelfLoggingResult } +import de.innfactory.bootstrapplay2.common.results.Results.NotLoggingResult object Errors { - case class DatabaseResult(message: String, errorClass: String, errorMethod: String, internalErrorMessage: String) - extends SelfLoggingResult(message, 400, errorClass, errorMethod, internalErrorMessage) - case class BadRequest() extends NotLoggingResult() + case class DatabaseResult( + message: String = "Entity or request malformed", + additionalInfoToLog: Option[String] = None, + additionalInfoErrorCode: Option[String] = None + ) extends NotLoggingResult() - case class NotFound() extends NotLoggingResult() + case class BadRequest( + message: String = "Entity or request malformed", + additionalInfoToLog: Option[String] = None, + additionalInfoErrorCode: Option[String] = None + ) extends NotLoggingResult() - case class Forbidden() extends NotLoggingResult() + 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/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/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 740e3ef7..cb13570e 100644 --- a/app/de/innfactory/bootstrapplay2/controllers/CompaniesController.scala +++ b/app/de/innfactory/bootstrapplay2/controllers/CompaniesController.scala @@ -1,23 +1,19 @@ package de.innfactory.bootstrapplay2.controllers -import akka.stream.scaladsl.{ Concat, Source } - +import akka.stream.scaladsl.Source import java.util.UUID -import de.innfactory.bootstrapplay2.actions.{ CompanyForUserExtractAction, JwtValidationAction, TracingCompanyAction } +import de.innfactory.bootstrapplay2.actions.TracingCompanyAction import cats.data.EitherT import cats.implicits._ 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 play.api.http.Writeable -import play.api.libs.json.Json import scala.concurrent.{ ExecutionContext, Future } @@ -44,19 +40,18 @@ class CompaniesController @Inject() ( def getStreamed = tracingAction("get companies streamed").async { implicit request => - companiesRepository.streamedAll(requestContext).map { r => - val source = Source.fromPublisher(r.mapResult(_.toJson.toString())).intersperse("[", ", ", "]") - Ok.chunked(source, Some("application/json")) - } + val result = + EitherT.right[ResultStatus](companiesRepository.streamedAll(requestContext).map(Source.fromPublisher(_))) + result.value.completeSourceChunked() } def patch: Action[AnyContent] = tracingCompanyAction("patch Company").async { implicit request => val json = request.body.asJson.get - val stock = json.as[Company] + val entity = json.as[Company] val result: EitherT[Future, ResultStatus, Company] = for { _ <- EitherT(Future(json.validateFor)) - created <- EitherT(companiesRepository.patch(stock)(requestContextWithCompany)) + created <- EitherT(companiesRepository.patch(entity)(requestContextWithCompany)) } yield created result.value.completeResult() } @@ -64,7 +59,7 @@ class CompaniesController @Inject() ( def post: Action[AnyContent] = tracingAction("post Company").async { implicit request => val json = request.body.asJson.get - val entity = json.as[Company] + val entity = json.as[Company] val result: EitherT[Future, ResultStatus, Company] = for { _ <- EitherT(Future(json.validateFor[Company])) created <- EitherT(companiesRepository.post(entity)(requestContext)) 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 dcf942e5..2cb35ad1 100644 --- a/app/de/innfactory/bootstrapplay2/controllers/LocationsController.scala +++ b/app/de/innfactory/bootstrapplay2/controllers/LocationsController.scala @@ -49,11 +49,11 @@ class LocationsController @Inject() ( def patch: Action[AnyContent] = tracingCompanyAction("Patch Location").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 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(stock)(requestContextWithCompany) + locationRepository.patch(entity)(requestContextWithCompany) ) // call locationRepository to patch the object } yield updated result.value @@ -63,7 +63,7 @@ class LocationsController @Inject() ( def post: Action[AnyContent] = tracingCompanyAction("Post Location").async { implicit request => val json = request.body.asJson.get - val entity = json.as[Location] + val entity = json.as[Location] val result: EitherT[Future, ResultStatus, Location] = for { _ <- EitherT(Future(json.validateFor[Location])) created <- EitherT(locationRepository.post(entity)(requestContextWithCompany)) diff --git a/app/de/innfactory/bootstrapplay2/db/BaseSlickDAO.scala b/app/de/innfactory/bootstrapplay2/db/BaseSlickDAO.scala index df30c030..7762df50 100644 --- a/app/de/innfactory/bootstrapplay2/db/BaseSlickDAO.scala +++ b/app/de/innfactory/bootstrapplay2/db/BaseSlickDAO.scala @@ -1,38 +1,22 @@ package de.innfactory.bootstrapplay2.db -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.{ Result, ResultStatus } import de.innfactory.bootstrapplay2.common.results.errors.Errors.{ BadRequest, DatabaseResult, 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 de.innfactory.bootstrapplay2.common.implicits.FutureTracingImplicits.EnhancedFuture import de.innfactory.bootstrapplay2.common.logging.ImplicitLogContext -import de.innfactory.bootstrapplay2.common.request.{ RequestContext, TraceContext } +import de.innfactory.bootstrapplay2.common.request.TraceContext import slick.dbio.{ DBIOAction, Effect, NoStream } -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 with ImplicitLogContext { - val currentClassForDatabaseError = "BaseSlickDAO" - override val profile = XPostgresProfile def lookupGeneric[R, T]( @@ -143,10 +127,12 @@ class BaseSlickDAO(db: Database)(implicit ec: ExecutionContext) extends Tables w db.run(update(patchedObject)) .map { x => if (x != 0) Right(patchedObject) - else + else { + tc.log.error("Database Result Updating entity") Left( - DatabaseResult("Could not replace entity", currentClassForDatabaseError, "update", "row not updated") + DatabaseResult("Could not update entity") ) + } } .trace("updateGeneric update") ) @@ -194,15 +180,14 @@ class BaseSlickDAO(db: Database)(implicit ec: ExecutionContext) extends Tables w .map { x => if (x != 0) Right(true) - else + else { + tc.log.error("Database Error deleting entity") Left( DatabaseResult( - "could not delete entity", - currentClassForDatabaseError, - "delete", - "entity was deleted" + "could not delete entity" ) ) + } } .trace("deleteGeneric delete") } yield dbDeleteResult diff --git a/app/de/innfactory/bootstrapplay2/db/CompaniesDAO.scala b/app/de/innfactory/bootstrapplay2/db/CompaniesDAO.scala index 4c991ebb..a3233017 100644 --- a/app/de/innfactory/bootstrapplay2/db/CompaniesDAO.scala +++ b/app/de/innfactory/bootstrapplay2/db/CompaniesDAO.scala @@ -26,9 +26,6 @@ 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 { @@ -53,24 +50,12 @@ trait CompaniesDAO { 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 with ImplicitLogContext { - // Class Name for identification in Database Errors - override val currentClassForDatabaseError = "SlickCompaniesDAO" - override val profile = XPostgresProfile import profile.api._ @@ -85,11 +70,6 @@ class SlickCompaniesSlickDAO @Inject() (db: Database)(implicit ec: ExecutionCont } ) - /** - * Lookup single object - * @param id - * @return - */ def lookup(id: UUID)(implicit tc: TraceContext): Future[Result[CompanyObject]] = lookupGeneric( queryById(id).result.headOption @@ -123,11 +103,6 @@ class SlickCompaniesSlickDAO @Inject() (db: Database)(implicit ec: ExecutionCont private def queryFromFiltersSeq(filter: Seq[FilterOptions[Tables.Company, _]]) = Compiled(Tables.Company.filterOptions(filter)) - /** - * Lookup Company by Email - * @param email - * @return - */ def internal_lookupByEmail(email: String)(implicit tc: TraceContext): Future[Option[CompanyObject]] = { val f: Future[Option[Tables.CompanyRow]] = db.run(queryByEmail(email).result.headOption) @@ -139,11 +114,6 @@ class SlickCompaniesSlickDAO @Inject() (db: Database)(implicit ec: ExecutionCont } } - /** - * Patch object - * @param companyObject - * @return - */ def update(companyObject: CompanyObject)(implicit tc: TraceContext): Future[Result[CompanyObject]] = updateGeneric( queryById(companyObject.id.getOrElse(UUID.randomUUID())).result.headOption, @@ -152,22 +122,12 @@ class SlickCompaniesSlickDAO @Inject() (db: Database)(implicit ec: ExecutionCont (old: CompanyObject) => patch(companyObject, old) ) - /** - * Delete Object - * @param id - * @return - */ 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)(implicit tc: TraceContext): Future[Result[CompanyObject]] = createGeneric( companyObject, 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 2a3596b5..8d056eca 100644 --- a/app/de/innfactory/bootstrapplay2/db/LocationsDAO.scala +++ b/app/de/innfactory/bootstrapplay2/db/LocationsDAO.scala @@ -19,9 +19,6 @@ 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 { @@ -46,23 +43,11 @@ trait LocationsDAO { 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,21 +68,11 @@ class SlickLocationsDAO @Inject() (db: Database)(implicit ec: ExecutionContext) .filter(_._2 <= maxDistance) ) - /** - * Lookup single object - * @param id - * @return - */ 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)(implicit tc: TraceContext): Future[Option[LocationObject]] = db.run(queryById(id).result.headOption).map { case Some(row) => @@ -106,11 +81,6 @@ class SlickLocationsDAO @Inject() (db: Database)(implicit ec: ExecutionContext) None } - /** - * Lookup by Company - * @param companyId - * @return - */ def lookupByCompany( companyId: UUID )(implicit tc: TraceContext): Future[Result[Seq[LocationObject]]] = @@ -118,13 +88,6 @@ class SlickLocationsDAO @Inject() (db: Database)(implicit ec: ExecutionContext) queryByCompany(companyId).result ) - /** - * Query All by distance from to index - * - * @param point - * @param distance - * @return - */ def allFromDistanceByCompany( companyId: UUID, point: Geometry, @@ -136,11 +99,6 @@ class SlickLocationsDAO @Inject() (db: Database)(implicit ec: ExecutionContext) ) } - /** - * Patch object - * @param locationObject - * @return - */ def update(locationObject: LocationObject)(implicit tc: TraceContext): Future[Result[LocationObject]] = updateGeneric[LocationRow, LocationObject]( queryById(locationObject.id.getOrElse(0)).result.headOption, @@ -149,22 +107,12 @@ class SlickLocationsDAO @Inject() (db: Database)(implicit ec: ExecutionContext) (old: LocationObject) => patch(locationObject, old) ) - /** - * Delete Object - * @param id - * @return - */ 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)(implicit tc: TraceContext): Future[Result[LocationObject]] = createGeneric[LocationRow, LocationObject]( locationObject, 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 4674d99b..a34f566a 100644 --- a/app/de/innfactory/bootstrapplay2/models/api/Company.scala +++ b/app/de/innfactory/bootstrapplay2/models/api/Company.scala @@ -10,9 +10,6 @@ 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]], @@ -23,9 +20,7 @@ case class Company( booleanAttribute: Boolean, created: Option[DateTime], updated: Option[DateTime] -) extends ApiBaseModel { - override def toJson: JsValue = Json.toJson(this)(Company.writes) -} +) object Company { implicit val reads = Json.reads[Company] diff --git a/app/de/innfactory/bootstrapplay2/models/api/Location.scala b/app/de/innfactory/bootstrapplay2/models/api/Location.scala index 4ee405cd..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,9 +26,7 @@ case class Location( created: Option[DateTime], updated: Option[DateTime], distance: Option[Float] -) extends ApiBaseModel { - override def toJson: JsValue = Json.toJson(this)(Location.writes) -} +) object Location { implicit val reads = Json.reads[Location] diff --git a/build.sbt b/build.sbt index 69f30016..5a89a4c5 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 @@ -74,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, @@ -109,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*/ @@ -122,6 +121,7 @@ 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" @@ -134,7 +134,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), @@ -162,14 +162,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 bbc0e10f..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 @@ -84,6 +76,9 @@ logging.access.statusList = ${?LOGGING_STATUSLIST} http.port = 8080 +project.id = "bootstrap-play2" +project.id = ${?PROJECT_ID} + opencensus-scala { trace { sampling-probability = 1 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 982e7e6e..33ee3ed1 100644 --- a/project/Dependencies.scala +++ b/project/Dependencies.scala @@ -6,7 +6,7 @@ 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.13" + 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 @@ -45,8 +45,17 @@ object Dependencies { // 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, diff --git a/test/controllers/AuthenticationTest.scala b/test/controllers/AuthenticationTest.scala index 76e7f225..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._ diff --git a/test/controllers/CompaniesControllerTest.scala b/test/controllers/CompaniesControllerTest.scala index 2e4cb72e..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 { /** ———————————————— */ diff --git a/test/controllers/FunctionalSpec.scala b/test/controllers/FunctionalSpec.scala index 1872cb2c..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.DatabaseResult -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 { - DatabaseResult("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 "" - } - } - } From 1e6b55f987fa7043a68e6d48abf54bea2fa0f4f6 Mon Sep 17 00:00:00 2001 From: patsta32 Date: Mon, 19 Apr 2021 15:22:26 +0200 Subject: [PATCH 4/4] update --- build.sbt | 1 - project/Dependencies.scala | 11 +++++++---- 2 files changed, 7 insertions(+), 5 deletions(-) diff --git a/build.sbt b/build.sbt index 5a89a4c5..57762cf0 100755 --- a/build.sbt +++ b/build.sbt @@ -124,7 +124,6 @@ lazy val root = (project in file(".")) 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, diff --git a/project/Dependencies.scala b/project/Dependencies.scala index 33ee3ed1..693d894f 100644 --- a/project/Dependencies.scala +++ b/project/Dependencies.scala @@ -5,12 +5,15 @@ 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 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