Giter Site home page Giter Site logo

gvolpe / pfps-shopping-cart Goto Github PK

View Code? Open in Web Editor NEW
526.0 18.0 165.0 1.67 MB

:shopping_cart: The Shopping Cart application developed in the book "Practical FP in Scala: A hands-on approach"

Home Page: https://leanpub.com/pfp-scala

License: Apache License 2.0

Scala 99.07% Nix 0.59% Shell 0.34%
functional-programming fp cats cats-effect http4s fs2 skunk refined newtypes tagless-final

pfps-shopping-cart's Introduction

shopping-cart

CI Status MergifyStatus Scala Steward badge Cats friendly

⚠️ IMPORTANT NOTICE If you are reading the first edition of Practical FP in Scala, go to the master branch. You're now on the second-edition branch, which is the new default. ⚠️

Components Overview

Here's an overview of the different components that make this application.

components

  • Both Services and Authentication are algebras. The latter are mainly dependencies for some of the services.
  • Programs shows Checkout, the business logic that combines most of the services.
  • Effects show our custom interfaces required implicitly.
  • The lines connecting services to Redis and PostgreSQL show which ones access which storage.
  • The HTTP layer shows the client and the different routes.
  • At the very end, we have both the modules and the entry point to the application.

Authentication Data

For didactic purposes, this is made available but in a real application THIS SHOULD NEVER BE MADE PUBLIC.

For Admin users, the following environment variables are needed:

  • SC_JWT_SECRET_KEY
  • SC_JWT_CLAIM
  • SC_ADMIN_USER_TOKEN

For access token (manipulation of the shopping cart):

  • SC_ACCESS_TOKEN_SECRET_KEY

For password encryption:

  • SC_PASSWORD_SALT

See the files docker-compose.yml or .env for more details.

Generate your own auth data

In order to generate a valid JWT token, you need a secret key, which can be any String, and a JWT Claim, which can be any valid JSON. You can then generate a token, as shown below:

val claim = JwtClaim(
  """
    {"uuid": "6290c116-4153-11ea-b77f-2e728ce88125"}
  """
)

val secretKey = JwtSecretKey("any-secret")

val algo = JwtAlgorithm.HS256

val mkToken: IO[JwtToken] =
  jwtEncode[IO](claim, secretKey, algo)

In our case, our claim contains a UUID, which is used to identify the Admin Id. In practice, though, a JWT can be any valid JSON.

Take a look at the TokenGenerator program to learn more.

Tests

To run Unit Tests:

sbt test

To run Integration Tests we need to run both PostgreSQL and Redis:

docker-compose up
sbt it:test
docker-compose down

Access Redis & Postgres

We can interact with both servers directly using the following commands:

$ docker-compose exec Redis /usr/local/bin/redis-cli
$ docker-compose exec Postgres usr/local/bin/psql -d store -U postgres

Build Docker image

sbt docker:publishLocal

Our image should now be built. We can check it by running the following command:

> docker images | grep shopping-cart
REPOSITORY                    TAG                 IMAGE ID            CREATED             SIZE
shopping-cart                 latest              646501a87362        2 seconds ago       138MB

To run our application using our Docker image, run the following command:

cd /app
docker-compose up

Payments Client

The configured test payment client is a fake API that always returns 200 with a Payment Id. Users are encouraged to make modifications, e.g. return 409 with another Payment Id (you can create one here) or any other HTTP status to see how our application handles the different cases.

This fake API can be modified at: https://beeceptor.com/console/payments

HTTP API Resources

If you use the Insomnia REST Client, you can import the shopping cart resources using the insomnia.json file.

LICENSE

Licensed under the Apache License, Version 2.0 (the "License"); you may not use this project except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0.

Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License.

pfps-shopping-cart's People

Contributors

cedric-corbiere-elitt avatar gpoirier avatar gvolpe avatar jopecko avatar kubukoz avatar mergify[bot] avatar minedeljkovic avatar nkgm avatar scala-steward avatar toniogela avatar yoohaemin avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

pfps-shopping-cart's Issues

Discussion: Use Codec Composition for Joins

Currently schemas that require a Join query are defined similar to the following:

 val decoder: Decoder[Item] =
    (itemId ~ itemName ~ itemDesc ~ money ~ brandId ~ brandName ~ categoryId ~ categoryName).map {
      case i ~ n ~ d ~ p ~ bi ~ bn ~ ci ~ cn =>
        Item(i, n, d, p, Brand(bi, bn), Category(ci, cn))
    }

I would like to argue that this doesn't scale well and isn't very pretty (at least at scale). Given that we already have Codecs for Brand and Category so I think this Decoder should be defined in terms of composition (which as functional idea is also worth iterating once again here).

This would require the SQL objects to be at least package private

Opening the issue for discussion but happy to make a PR if people are open to it

Can't run shopping app

Hi Gabriel Volpe

First of all, thanks for your excellent book.
I can't run the shopping app. i'm on mac and I use docker for mac. Need your help, I'm newbie.

`Removing app_shopping_cart_1
Recreating a5556a84df46_app_shopping_cart_1 ... error

ERROR: for a5556a84df46_app_shopping_cart_1 Cannot start service shopping_cart: OCI runtime create failed: container_linux.go:380: starting container process caused: exec: "/opt/docker/bin/shopping-cart-core": stat /opt/docker/bin/shopping-cart-core: no such file or directory: unknown

ERROR: for shopping_cart Cannot start service shopping_cart: OCI runtime create failed: container_linux.go:380: starting container process caused: exec: "/opt/docker/bin/shopping-cart-core": stat /opt/docker/bin/shopping-cart-core: no such file or directory: unknown
ERROR: Encountered errors while bringing up the project.`

Document needed authentication data

Add a section in the README file that explains to users how to set up their own authentication data. For example, the JWT claim must be a valid JSON object.

Investigate possible Http4s bug

It seems to happen when the authorization token is invalid / expired:

core 21:31:04.031 [ioapp-compute-2] ERROR org.http4s.server.service-errors - Error servicing request: POST /v1/cart from 127.0.0.1
core java.lang.ClassCastException: org.http4s.ContextRequest cannot be cast to org.http4s.AuthedRequest
core 	at cats.data.Kleisli.apply(Kleisli.scala:119)
core 	at org.http4s.server.package$AuthMiddleware$.$anonfun$apply$5(package.scala:122)
core 	at cats.effect.internals.IORunLoop$.cats$effect$internals$IORunLoop$$loop(IORunLoop.scala:139)
core 	at cats.effect.internals.IORunLoop$RestartCallback.signal(IORunLoop.scala:355)
core 	at cats.effect.internals.IORunLoop$RestartCallback.apply(IORunLoop.scala:376)
core 	at cats.effect.internals.IORunLoop$RestartCallback.apply(IORunLoop.scala:316)
core 	at cats.effect.internals.IORunLoop$.cats$effect$internals$IORunLoop$$loop(IORunLoop.scala:136)
core 	at cats.effect.internals.IORunLoop$RestartCallback.signal(IORunLoop.scala:355)
core 	at cats.effect.internals.IORunLoop$RestartCallback.apply(IORunLoop.scala:376)
core 	at cats.effect.internals.IORunLoop$RestartCallback.apply(IORunLoop.scala:316)
core 	at cats.effect.internals.IOShift$Tick.run(IOShift.scala:36)
core 	at cats.effect.internals.PoolUtils$$anon$2$$anon$3.run(PoolUtils.scala:51)
core 	at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1149)
core 	at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:624)
core 	at java.lang.Thread.run(Thread.java:748)

A business logic bug within auth.scala

Hey! Found a business logic bug within Auth algebra.

https://github.com/gvolpe/pfps-shopping-cart/blob/second-edition/modules/core/src/main/scala/shop/algebras/auth.scala#L68

Basically, users.find will return Some(User) only in case if caller has provided correct username & password. I suppose you need to introduce a different method that performs a search only via username.

Otherwise, given a username conflict it will go to None branch, then will attempt inserting a user with conflicting username and will raise a postgres-level exception instead of your custom one.

Crypto Is Non-deterministic

I didn't know if this should go in discussion or here as I imagine this was part intentional.

Because IvParameterSpec is generated on random bytes, whenever you run the application the spec changes between runs, resulting in different encrypted strings on the same raw string (this is the whole point of initialization vectors). This means if you have an EncryptedPassword already saved in the database, you won't be able login using the same password if the application restarts. crypto.encrypt("mypassword") will result in a different EncryptedPassword than what is in the database.

This doesn't seem like a very desirable behavior. What are your thoughts?

Steps to reproduce:

  1. Run the application and create a user
  2. login with the created user --> 200 Ok
  3. Restart the application
  4. Try logging in with the same user --> 403 Forbidden

To work around this, IV needs to be stored in the users table as well

Upgrade to scala 3?

Hi,

I was wondering if you have any plans to upgrade the projects from the Practical FP in Scala book to Scala 3. Maybe another edition of the book?

Thanks

java.lang.NumberFormatException: For input string: "0"uuid": "004b4457"

Hi,
I am getting very weird error, see below. I tried with different SC_JWT_CLAIM environment variable but I think it is not related to this variable. What can be the issue?
The error is :

java.lang.NumberFormatException: For input string: "0"uuid": "004b4457"
	at java.lang.NumberFormatException.forInputString(NumberFormatException.java:65)
	at java.lang.Long.parseLong(Long.java:589)
	at java.lang.Long.valueOf(Long.java:776)
	at java.lang.Long.decode(Long.java:928)
	at java.util.UUID.fromString(UUID.java:198)
	at relay.effects.GenUUID$$anon$1.$anonfun$read$1(GenUUID.scala:28)
	at cats.ApplicativeError.catchNonFatal(ApplicativeError.scala:136)
	at cats.ApplicativeError.catchNonFatal$(ApplicativeError.scala:135)
	at cats.effect.IOLowPriorityInstances$IOEffect.catchNonFatal(IO.scala:767)
	at relay.effects.GenUUID$$anon$1.read(GenUUID.scala:28)
	at relay.modules.Security$.$anonfun$make$1(Security.scala:44)
	at cats.effect.internals.IORunLoop$.cats$effect$internals$IORunLoop$$loop(IORunLoop.scala:139)
	at cats.effect.internals.IORunLoop$.startCancelable(IORunLoop.scala:41)
	at cats.effect.internals.IOBracket$BracketStart.run(IOBracket.scala:86)
	at cats.effect.internals.Trampoline.cats$effect$internals$Trampoline$$immediateLoop(Trampoline.scala:70)
	at cats.effect.internals.Trampoline.startLoop(Trampoline.scala:36)
	at cats.effect.internals.TrampolineEC$JVMTrampoline.super$startLoop(TrampolineEC.scala:93)
	at cats.effect.internals.TrampolineEC$JVMTrampoline.$anonfun$startLoop$1(TrampolineEC.scala:93)
	at scala.runtime.java8.JFunction0$mcV$sp.apply(JFunction0$mcV$sp.scala:18)
	at scala.concurrent.BlockContext$.withBlockContext(BlockContext.scala:94)
	at cats.effect.internals.TrampolineEC$JVMTrampoline.startLoop(TrampolineEC.scala:93)
	at cats.effect.internals.Trampoline.execute(Trampoline.scala:43)
	at cats.effect.internals.TrampolineEC.execute(TrampolineEC.scala:44)
	at cats.effect.internals.IOBracket$BracketStart.apply(IOBracket.scala:72)
	at cats.effect.internals.IOBracket$BracketStart.apply(IOBracket.scala:52)
	at cats.effect.internals.IORunLoop$.cats$effect$internals$IORunLoop$$loop(IORunLoop.scala:136)
	at cats.effect.internals.IORunLoop$RestartCallback.signal(IORunLoop.scala:355)
	at cats.effect.internals.IORunLoop$RestartCallback.apply(IORunLoop.scala:376)
	at cats.effect.internals.IORunLoop$RestartCallback.apply(IORunLoop.scala:316)
	at cats.effect.internals.IORunLoop$.cats$effect$internals$IORunLoop$$loop(IORunLoop.scala:136)
	at cats.effect.internals.IORunLoop$RestartCallback.signal(IORunLoop.scala:355)
	at cats.effect.internals.IORunLoop$RestartCallback.apply(IORunLoop.scala:376)
	at cats.effect.internals.IORunLoop$RestartCallback.apply(IORunLoop.scala:316)
	at cats.effect.internals.IOShift$Tick.run(IOShift.scala:36)
	at cats.effect.internals.PoolUtils$$anon$2$$anon$3.run(PoolUtils.scala:51)
	at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1149)
	at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:624)
	at java.lang.Thread.run(Thread.java:748)

Why is HealthCheck memoized?

The status field in HealthCheck[F] is val and not def on line 46 of Healthcheck.scala. Doesn't this mean that whenever status is called, healthCheck.status will return whatever is cached from the first call of status? Shouldn't it recompute the status since a successful status at one point doesn't mean the service will be healthy at every point afterwards?

LiveCrypto can't decrypt password

Using the LiveCrypto to decrypt a password produces the following error

javax.crypto.BadPaddingException: Given final block not properly padded. Such issues can arise if a bad key is used during decryption.

Newtype coersion with newtype > 0.4.4

in newtype 0.4.4 repr becomes a private

https/repo1.maven.org/maven2/io/estatico/newtype_2.13/0.4.4/newtype_2.13-0.4.4-sources.jar!/io/estatico/newtype/Coercible.scala

final class CoercibleIdOps[A](private val repr: A) extends AnyVal {
  @inline def coerce[B](implicit ev: Coercible[A, B]): B = repr.asInstanceOf[B]
}

thus

  implicit def coercibleEncoder[A: Coercible[B, *], B: Encoder]: Encoder[A] =
    Encoder[B].contramap(_.repr.asInstanceOf[B])
pfps-shopping-cart/modules/core/src/main/scala/shop/http/json.scala:46:28: value repr is not a member of type parameter A
[error]     Encoder[B].contramap(_.repr.asInstanceOf[B])

Unable to deploy app using docker

When trying to deploy the app using the compose file in the app/ directory as is, the app port doesn't appear to be exposed to the host (unable to hit the endpoints using the imsomnia.json import to insomnia and lsof -i tcp:8080 yield empty result). I tried adding the "ports" option with a value of "8080:8080" in the compose file but the "ports" option is not compatible with "network_mode". When I remove network mode and include the "ports" option, I am then unable to connect to the databases and get a "ConnectionRefusedError". It looks like the port should be exposed by the "DockerPlugin" but it doesn't seem to work for me. Can you please advise?

The logs show http4s starting at

HTTP Server started at /0:0:0:0:0:0:0:0:8080

which looks weird. I have

        HttpServerConfig(
          host = host"0.0.0.0",
          port = port"8080"

in Config

Very, very helpful and awesome book by the way. A little over halfway through and have already learned so very much :)

Create Payments client API

We need a fake Payments Client API so we can see that our application effectively hits a remote endpoint.

Should be a little independent project, can be written in Haskell or any other language :)

Fix test suite

Right now, we are doing unsafeToFuture but the return type is Unit, so we are actually discarding its value! Can't believe this slip through but better to fix it later than never.

Postgressuite and ItemsRoute intermittent failures

If you change shop.generators.nonEmptyStringGen to generate shorter strings, ex. to:

  val nonEmptyStringGen: Gen[String] =
    Gen
      .chooseNum(1, 40)
      .flatMap { n =>
        Gen.buildableOfN[String, Char](n, Gen.alphaChar)
      }

PostgresSuite will more likely fail with unique constraint violations and ItemsRuiteSuite:items by brand succeeds will fail because of toLowerCase.capitalize in BrandParam.toDomain string manipulation.

Design Diagram or Flow Diagram

I won't say it is an issue but for the sake of request, I opened an issue. May we get any Design Diagram (UML) or Flow Diagram, that picture the whole application flow.
(Beside that, I am following the book with the code, that is super awesome, thanks for such a great content.)

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.