Skip to content

Commit 3654428

Browse files
authored
Use DB, file or memory to store mindmups (#12)
1 parent 7eb2da6 commit 3654428

File tree

16 files changed

+550
-265
lines changed

16 files changed

+550
-265
lines changed

build.sbt

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,11 +27,12 @@ lazy val quizz =
2727
library.doobieCore,
2828
library.doobiePostgres,
2929
library.doobieQuill,
30-
library.doobieScalatest % Test
30+
library.doobieScalatest % Test,
3131
// library.tapirHttp4s,
3232
// library.tapirJson,
3333
// library.bazelServer,
3434
// library.bazelClient
35+
library.betterFiles
3536
)
3637
)
3738

@@ -52,6 +53,7 @@ lazy val library =
5253
val doobie = "0.9.0"
5354
val sttp = "2.2.8"
5455
val sttpTapirJsonCirce = "0.16.16"
56+
val betterFiles = "3.9.1"
5557
}
5658
val scalaCheck = "org.scalacheck" %% "scalacheck" % Version.scalaCheck
5759
val scalaTest = "org.scalatest" %% "scalatest" % Version.scalaTest
@@ -67,6 +69,7 @@ lazy val library =
6769
val sttpClient = "com.softwaremill.sttp.client" %% "core" % Version.sttp
6870
val sttpClientCirce = "com.softwaremill.sttp.client" %% "circe" % Version.sttp
6971
val tapirJsonCirce = "com.softwaremill.sttp.tapir" %% "tapir-json-circe" % Version.sttpTapirJsonCirce
72+
val betterFiles = "com.github.pathikrit" %% "better-files" % Version.betterFiles
7073

7174
val logback = "ch.qos.logback" % "logback-classic" % Version.logback
7275
val doobieCore = "org.tpolecat" %% "doobie-core" % Version.doobie

docker-compose.yml

Lines changed: 25 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,13 +4,35 @@ services:
44
quizz:
55
image: otrebski/quizz:latest
66
volumes:
7-
- '/tmp/mindmups/:/tmp/mindmups/'
7+
- '/tmp/mindmups/:/mindmups/'
88
environment:
9-
LOAD_FROM_DIR: "/tmp/mindmups/"
10-
USE_SLACK: "false"
119
SLACK_TOKEN: ""
10+
FEEDBACK_USE_SLACK: "false"
11+
FEEDBACK_USE_DB: "false"
12+
MINDMUP_STORAGE: "file" #use file, memory or database
13+
FILESTORAGE_DIR: "/mindmups"
14+
# DB_HOST: "db"
15+
# DB_PORT: "5432"
16+
# DB_NAME: "quizz"
17+
# DB_USERNAME: "postgres"
18+
# DB_PASSWORD: "password"
19+
links:
20+
- db
21+
1222

1323
gui:
1424
image: otrebski/quizz-gui:latest
1525
ports:
1626
- 8080:80
27+
links:
28+
- quizz
29+
30+
db:
31+
image: library/postgres:13
32+
# ports:
33+
# - 5432:15432
34+
environment:
35+
POSTGRES_PASSWORD: "password"
36+
POSTGRES_DB: "quizz"
37+
volumes:
38+
- ./postgres-data:/var/lib/postgresql/data
Lines changed: 25 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -1,30 +1,35 @@
1-
quizz {
2-
loader {
3-
dir = ""
4-
dir = ${?LOAD_FROM_DIR}
5-
}
6-
}
7-
81
feedback {
92
slack {
103
use = false
11-
use = ${?USE_SLACK}
4+
use = ${?FEEDBACK_USE_SLACK}
125
token = ""
136
token = ${?SLACK_TOKEN}
147
}
8+
database {
9+
use = false
10+
use = ${?FEEDBACK_USE_DB}
11+
}
12+
}
13+
14+
mindmup {
15+
store-type = "file" //file, database or memory
16+
store-type = ${?MINDMUP_STORAGE}
17+
}
18+
19+
filestorage {
20+
dir = "mindmups"
21+
dir = ${?FILESTORAGE_DIR}
1522
}
1623

1724
database {
18-
use = false
19-
use = ${?USE_DB}
20-
host = "localhost"
21-
host = ${?DB_HOST}
22-
port = 5432
23-
port = ${?DB_PORT}
24-
dbname = "quizz"
25-
dbname = ${?DB_NAME}
26-
user = "postgres"
27-
user = ${?DB_USERNAME}
28-
password = "password"
29-
password = ${?DB_PASSWORD}
25+
host = "localhost"
26+
host = ${?DB_HOST}
27+
port = 5432
28+
port = ${?DB_PORT}
29+
dbname = "quizz"
30+
dbname = ${?DB_NAME}
31+
user = "postgres"
32+
user = ${?DB_USERNAME}
33+
password = "password"
34+
password = ${?DB_PASSWORD}
3035
}

src/main/scala/mindmup/Parser.scala

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,9 @@ object Parser extends LazyLogging {
2222
val r: Either[Error, V3IdString.Mindmap] = parsedJson.flatMap(mindMupDecoder.decodeJson)
2323
r match {
2424
case Left(e) =>
25-
logger.info(s"Parsing was not successful due to ${e.getMessage}, will try different parser")
25+
logger.debug(
26+
s"Parsing was not successful due to ${e.getMessage}, will try different parser"
27+
)
2628
parsedJson.flatMap(mindMupDecoderInt.decodeJson).map(_.toV3IdString) match {
2729
case Left(_) =>
2830
logger.info(

src/main/scala/mindmup/package.scala

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -84,7 +84,6 @@ package object mindmup extends LazyLogging {
8484
label.getOrElse("?") -> toStep(v)
8585
}
8686
Question(id, title, stringToStep)
87-
8887
}
8988
}
9089

src/main/scala/quizz/data/ExamplesData.scala

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -21,11 +21,11 @@ import cats.syntax.show._
2121
import com.typesafe.scalalogging.LazyLogging
2222
import mindmup.Parser
2323
import quizz.model.{ FailureStep, Question, Quizz, SuccessStep }
24-
import quizz.web.WebApp.Api.{ Answer, HistoryStep, QuizzState, Step }
24+
import quizz.web.Api.{ Answer, HistoryStep, QuizzState, Step }
2525

2626
object ExamplesData extends LazyLogging {
2727

28-
val quiz = Question(
28+
val quiz: Question = Question(
2929
"root",
3030
"What kind of problem do you have",
3131
Map(
@@ -117,7 +117,7 @@ object ExamplesData extends LazyLogging {
117117

118118
object Fake {
119119

120-
val exampleStateInProgress = QuizzState(
120+
val exampleStateInProgress: QuizzState = QuizzState(
121121
path = "root",
122122
currentStep = Step(
123123
"a",
@@ -154,12 +154,12 @@ object ExamplesData extends LazyLogging {
154154
)
155155
)
156156
)
157-
val exampleStateFinalSuccess = QuizzState(
157+
val exampleStateFinalSuccess: QuizzState = QuizzState(
158158
path = "asdfsdf",
159159
currentStep = Step("a", "I co dalej?", List.empty, success = Some(true)),
160160
history = List()
161161
)
162-
val exampleStateFinalFailure = QuizzState(
162+
val exampleStateFinalFailure: QuizzState = QuizzState(
163163
path = "asdfsdf",
164164
currentStep = Step("a", "I co dalej?", List.empty, success = Some(false)),
165165
history = List()
Lines changed: 142 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,142 @@
1+
package quizz.data
2+
3+
import cats.effect.{ Async, ContextShift, IO, Sync }
4+
5+
import scala.language.higherKinds
6+
import better.files._
7+
import cats.{ Applicative, FlatMap }
8+
import cats.effect.concurrent.Ref
9+
import doobie.Transactor
10+
import doobie.util.transactor.Transactor.Aux
11+
import quizz.db.DatabaseConfig
12+
13+
trait MindmupStore[F[_]] {
14+
15+
def store(name: String, content: String): F[Unit]
16+
17+
def listNames(): F[Set[String]]
18+
19+
def load(name: String): F[String]
20+
21+
def delete(name: String): F[Unit]
22+
}
23+
24+
object FileMindmupStore {
25+
def apply[F[_]](dir: File)(implicit ev: Sync[F]): F[FileMindmupStore[F]] =
26+
Sync[F].delay {
27+
dir.createDirectoryIfNotExists(createParents = true)
28+
new FileMindmupStore(dir)
29+
}
30+
}
31+
32+
class FileMindmupStore[F[_]: Sync](dir: File) extends MindmupStore[F] {
33+
34+
override def store(name: String, content: String): F[Unit] =
35+
Sync[F].delay {
36+
(dir / name).overwrite(content)
37+
}
38+
39+
override def listNames(): F[Set[String]] =
40+
Sync[F].delay {
41+
dir.list
42+
.filter(_.isRegularFile)
43+
.map(_.name)
44+
.toSet
45+
}
46+
47+
override def load(name: String): F[String] =
48+
Sync[F].delay {
49+
(dir / name).contentAsString
50+
}
51+
52+
override def delete(name: String): F[Unit] =
53+
Sync[F].delay {
54+
val toDelete = dir / name
55+
toDelete.delete(swallowIOExceptions = false)
56+
}
57+
}
58+
59+
object MemoryMindmupStore {
60+
def apply[F[_]]()(implicit ev: Sync[F]): F[MemoryMindmupStore[F]] = {
61+
val ref: F[Ref[F, Map[String, String]]] =
62+
Ref.of[F, Map[String, String]](Map.empty[String, String])
63+
Applicative[F].map(ref)(r => new MemoryMindmupStore[F](r))
64+
}
65+
66+
}
67+
68+
class MemoryMindmupStore[F[_]: Sync](ref: Ref[F, Map[String, String]]) extends MindmupStore[F] {
69+
override def store(name: String, content: String): F[Unit] =
70+
ref.update(x => x.updated(name, content))
71+
72+
override def listNames(): F[Set[String]] =
73+
FlatMap[F].flatMap(ref.get)(m => Applicative[F].pure(m.toList.map(_._1).toSet))
74+
75+
override def load(name: String): F[String] =
76+
FlatMap[F].flatMap(ref.get)(m => Applicative[F].pure(m(name)))
77+
78+
override def delete(name: String): F[Unit] =
79+
FlatMap[F].flatMap(ref.update(v => v.removed(name)))(_ => Applicative[F].pure(()))
80+
}
81+
82+
object DbMindMupStore {
83+
def apply[F[_]](
84+
dbConfig: DatabaseConfig
85+
)(implicit ec2: Async[F], ev3: ContextShift[F]): F[DbMindMupStore[F]] =
86+
Async[F].delay {
87+
88+
implicit val cs: ContextShift[IO] = IO.contextShift(scala.concurrent.ExecutionContext.global)
89+
90+
val xa: Aux[F, Unit] = Transactor.fromDriverManager[F](
91+
driver = "org.postgresql.Driver",
92+
url = s"jdbc:postgresql://${dbConfig.host}:${dbConfig.port}/${dbConfig.database}",
93+
user = dbConfig.user,
94+
pass = dbConfig.password
95+
)
96+
97+
new DbMindMupStore(xa)
98+
}
99+
}
100+
101+
class DbMindMupStore[F[_]: Async: ContextShift](xa: Aux[F, Unit]) extends MindmupStore[F] {
102+
103+
case class Mindmup(id: String, json: String)
104+
105+
import doobie.quill.DoobieContext
106+
import io.getquill.Literal
107+
108+
val dc = new DoobieContext.Postgres(Literal) // Literal naming scheme
109+
110+
import dc._
111+
import doobie.implicits._
112+
113+
override def store(name: String, json: String): F[Unit] = {
114+
val q: dc.Quoted[dc.Insert[Mindmup]] = quote {
115+
query[Mindmup]
116+
.insert(lift(Mindmup(name, json)))
117+
.onConflictUpdate(_.id)((t, e) => t.json -> e.json)
118+
}
119+
Applicative[F].map(run(q).transact(xa))(_ => ())
120+
}
121+
122+
override def listNames(): F[Set[String]] = {
123+
val q = quote {
124+
query[Mindmup].map(_.id)
125+
}
126+
Applicative[F].map(run(q).transact(xa))(_.toSet)
127+
}
128+
129+
override def load(name: String): F[String] = {
130+
val q = quote {
131+
query[Mindmup].map(x => x.json)
132+
}
133+
Applicative[F].map(run(q).transact(xa))(_.head)
134+
}
135+
136+
override def delete(name: String): F[Unit] = {
137+
val q = quote {
138+
query[Mindmup].filter(_.id == lift(name)).delete
139+
}
140+
Applicative[F].map(run(q).transact(xa))(_ => ())
141+
}
142+
}
Lines changed: 23 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,15 @@
11
package quizz.db
22

33
import cats.effect.IO
4+
import cats.implicits._
45

56
object DatabaseInitializer {
67

78
def initDatabase(cfg: DatabaseConfig): IO[Int] = {
9+
import cats.effect._
810
import doobie._
911
import doobie.implicits._
1012
import doobie.util.ExecutionContexts
11-
import cats.effect._
1213
implicit val cs: ContextShift[IO] = IO.contextShift(ExecutionContexts.synchronous)
1314

1415
val xa = Transactor.fromDriverManager[IO](
@@ -17,18 +18,27 @@ object DatabaseInitializer {
1718
cfg.user, // user
1819
cfg.password // password
1920
)
20-
val create =
21-
sql""" CREATE TABLE IF NOT EXISTS feedback
22-
(
23-
id SERIAL PRIMARY KEY,
24-
timestamp timestamp NOT NULL,
25-
quizzId varchar(300) NOT NULL,
26-
path varchar(2000) NOT NULL,
27-
comment varchar(5000) NOT NULL,
28-
rate INT NOT NULL
29-
);
30-
""".update.run
31-
create.transact(xa)
21+
22+
val createFeedback: doobie.ConnectionIO[Int] =
23+
sql"""CREATE TABLE IF NOT EXISTS feedback
24+
|(
25+
| id SERIAL PRIMARY KEY,
26+
| timestamp timestamp NOT NULL,
27+
| quizzId varchar(300) NOT NULL,
28+
| path varchar(2000) NOT NULL,
29+
| comment varchar(5000) NOT NULL,
30+
| rate INT NOT NULL
31+
|);
32+
| """.stripMargin.update.run
33+
val createMindmup: doobie.ConnectionIO[Int] =
34+
sql"""CREATE TABLE IF NOT EXISTS mindmup
35+
|(
36+
| id varchar(500) primary key,
37+
| json varchar(100000)
38+
|)""".stripMargin.update.run
39+
40+
(createFeedback, createMindmup).mapN(_ + _).transact(xa)
41+
3242
}
3343

3444
}

src/main/scala/quizz/feedback/FeedbackSender.scala

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ import com.typesafe.scalalogging.LazyLogging
66
import doobie.quill.DoobieContext
77
import io.getquill.Literal
88
import quizz.db.{ DatabaseConfig, Feedback }
9-
import quizz.web.WebApp.Api.{ FeedbackSend, QuizzState }
9+
import quizz.web.Api.{ FeedbackSend, QuizzState }
1010

1111
import scala.language.higherKinds
1212

0 commit comments

Comments
 (0)