Skip to content

Commit 3e93b61

Browse files
Merge pull request #4 from davenverse/sqlitesjs
Add sqlite-sjs support
2 parents 1577295 + 7c819e9 commit 3e93b61

File tree

9 files changed

+104
-16
lines changed

9 files changed

+104
-16
lines changed

build.sbt

+2
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,7 @@ lazy val core = crossProject(JVMPlatform, JSPlatform)
5656
scalaJSLinkerConfig ~= { _.withModuleKind(ModuleKind.CommonJSModule)},
5757
libraryDependencies ++= Seq(
5858
"io.chrisdavenport" %%% "publicsuffix-retrieval-client" % publicSuffixV,
59+
"io.chrisdavenport" %%% "sqlite-sjs" % "0.0.1",
5960
)
6061
// TODO Actually Implement with this
6162
// npmDependencies ++= Seq(
@@ -79,6 +80,7 @@ lazy val examples = crossProject(JVMPlatform, JSPlatform)
7980
"org.http4s" %%% "http4s-ember-client" % http4sV,
8081
)
8182
)
83+
.jsConfigure { project => project.enablePlugins(ScalaJSBundlerPlugin) }
8284
.jsSettings(
8385
scalaJSUseMainModuleInitializer := true,
8486
scalaJSLinkerConfig ~= { _.withModuleKind(ModuleKind.CommonJSModule)},
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,96 @@
11
package io.chrisdavenport.snickerdoodle.persistence
22

3+
import cats.syntax.all._
4+
import io.chrisdavenport.snickerdoodle.SnCookie
5+
import io.chrisdavenport.snickerdoodle.SnCookie.SnCookieKey
36
import io.chrisdavenport.snickerdoodle.SnCookiePersistence
47
import cats.effect.kernel._
5-
8+
import io.chrisdavenport.sqlitesjs.Sqlite
9+
import io.circe.syntax._
10+
import io.circe.Decoder
11+
import io.circe.HCursor
612

713
trait SqlitePersistencePlatform {
814

9-
def apply[F[_]: Async](path: fs2.io.file.Path): SnCookiePersistence[F] =
10-
throw new RuntimeException("scala.js sqlite not yet implemented")
15+
def apply[F[_]: Async](path: fs2.io.file.Path): Resource[F, SnCookiePersistence[F]] =
16+
Sqlite.fromFile[F](path.toString).map(new SqlitePersistenceImpl[F](_))
17+
18+
19+
private class SqlitePersistenceImpl[F[_]: Async](sqlite: Sqlite[F]) extends SnCookiePersistence[F]{
20+
21+
def updateLastAccessed(key: SnCookieKey, lastAccessed: Long): F[Unit] = {
22+
val updateStatement = "Update cookies SET lastAccessed = ? where name = ? and domain = ? and path = ?"
23+
sqlite.run(updateStatement, List(lastAccessed.asJson, key.name.asJson, key.domain.asJson, key.path.asJson))
24+
.void
25+
}
26+
27+
def clear: F[Unit] = {
28+
val clearStatement = "DELETE FROM cookies"
29+
sqlite.exec(clearStatement)
30+
}
31+
def clearExpired(now: Long): F[Unit] = {
32+
val clearExpiredStatment = "DELETE FROM cookies where expiry < ?"
33+
sqlite.run(clearExpiredStatment, List(now.asJson)).void
34+
}
35+
def create(cookie: SnCookie): F[Unit] = {
36+
val raw = SnCookie.toRaw(cookie)
37+
val insertStatement = "INSERT OR REPLACE INTO cookies (name, value, domain, path, expiry, lastAccessed, creationTime, isSecure, isHttpOnly, isHostOnly, sameSite, scheme, extension) values (?,?,?,?,?,?,?,?,?,?,?,?,?)"
38+
sqlite.run(insertStatement, List(raw.name.asJson, raw.value.asJson, raw.domain.asJson, raw.path.asJson, raw.expiry.asJson, raw.lastAccessed.asJson, raw.creationTime.asJson, raw.isSecure.asJson, raw.isHttpOnly.asJson, raw.isHostOnly.asJson, raw.sameSite.asJson, raw.scheme.asJson, raw.extension.asJson))
39+
.void
40+
}
41+
42+
def createTable: F[Unit] = {
43+
sqlite.exec(createTableStatement)
44+
}
45+
def getAll: F[List[SnCookie]] = {
46+
val selectStatement = "SELECT name,value,domain,path,expiry,lastAccessed,creationTime,isSecure,isHttpOnly, isHostOnly, sameSite,scheme,extension FROM cookies"
47+
sqlite.all(selectStatement).flatMap(
48+
l => l.traverse(_.as[SnCookie.RawSnCookie](rawDecoder).liftTo[F].map(SnCookie.fromRaw))
49+
)
50+
}
51+
52+
}
53+
54+
private val createTableStatement = {
55+
"""CREATE TABLE IF NOT EXISTS cookies (
56+
name TEXT NOT NULL,
57+
value TEXT NOT NULL,
58+
domain TEXT NOT NULL,
59+
path TEXT NOT NULL,
60+
expiry INTEGER NOT NULL, -- Either MaxAge (relative to time called) or Expires (explicit) or HttpDate.MaxValue
61+
lastAccessed INTEGER NOT NULL,
62+
creationTime INTEGER NOT NULL,
63+
isSecure INTEGER NOT NULL, -- Boolean
64+
isHttpOnly INTEGER NOT NULL, -- Boolean
65+
isHostOnly INTEGER NOT NULL, -- Boolean
66+
sameSite INTEGER NOT NULL, --
67+
scheme INTEGER,
68+
extension TEXT,
69+
CONSTRAINT cookiesunique UNIQUE (name, domain, path)
70+
)"""
71+
}
72+
73+
private val rawDecoder = new Decoder[SnCookie.RawSnCookie]{
74+
def apply(c: HCursor): Decoder.Result[SnCookie.RawSnCookie] = (
75+
c.downField("name").as[String],
76+
c.downField("value").as[String],
77+
c.downField("domain").as[String],
78+
c.downField("path").as[String],
79+
c.downField("expiry").as[Long],
80+
c.downField("lastAccessed").as[Long],
81+
c.downField("creationTime").as[Long],
82+
c.downField("isSecure").as[Boolean](intBoolean),
83+
c.downField("isHttpOnly").as[Boolean](intBoolean),
84+
c.downField("isHostOnly").as[Boolean](intBoolean),
85+
c.downField("sameSite").as[Int],
86+
c.downField("scheme").as[Option[Int]],
87+
c.downField("extensione").as[Option[String]]
88+
).mapN(SnCookie.RawSnCookie.apply)
89+
}
90+
91+
private val intBoolean: Decoder[Boolean] = Decoder[Int].emap{
92+
case 0 => false.asRight
93+
case 1 => true.asRight
94+
case _ => "Invalid Integer Boolean".asLeft
95+
}
1196
}

core/jvm/src/main/scala/io/chrisdavenport/snickerdoodle/persistence/SqlitePersistencePlatform.scala

+3-3
Original file line numberDiff line numberDiff line change
@@ -52,7 +52,7 @@ trait SqlitePersistencePlatform { self =>
5252
}
5353

5454
private[snickerdoodle] def create[F[_]: MonadCancelThrow](xa: Transactor[F])(cookie: SnCookie): F[Int] = {
55-
Update[SnCookie.RawSnCookie]("INSERT OR REPLACE INTO cookies values (?,?,?,?,?,?,?,?,?,?,?,?,?)")
55+
Update[SnCookie.RawSnCookie]("INSERT OR REPLACE INTO cookies (name, value, domain, path, expiry, lastAccessed, creationTime, isSecure, isHttpOnly, isHostOnly, sameSite, scheme, extension) values (?,?,?,?,?,?,?,?,?,?,?,?,?)")
5656
.run(SnCookie.toRaw(cookie))
5757
.transact(xa)
5858
}
@@ -76,7 +76,7 @@ trait SqlitePersistencePlatform { self =>
7676
.transact(xa)
7777
}
7878

79-
def apply[F[_]: Async](path: fs2.io.file.Path): SnCookiePersistence[F] = {
79+
def apply[F[_]: Async](path: fs2.io.file.Path): Resource[F, SnCookiePersistence[F]] = {
8080
val xa = transactor[F](path)
8181
new SnCookiePersistence[F] {
8282
def updateLastAccessed(key: SnCookie.SnCookieKey, lastAccessed: Long): F[Unit] =
@@ -96,6 +96,6 @@ trait SqlitePersistencePlatform { self =>
9696

9797
def getAll: F[List[SnCookie]] = self.selectAll(xa)
9898

99-
}
99+
}.pure[Resource[F, *]]
100100
}
101101
}

core/shared/src/main/scala/io/chrisdavenport/snickerdoodle/SnCookieJarBuilder.scala

+1-1
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ final class SnCookieJarBuilder[F[_]: Async] private (
2020
def withSupervisor(s: Supervisor[F]) = copy(supervisorO = s.some)
2121

2222
def withSqlitePersistence(path: Path) =
23-
copy(persistenceO = SnCookiePersistence.sqlite(path).pure[Resource[F, *]].some)
23+
copy(persistenceO = SnCookiePersistence.sqlite(path).some)
2424
def withPersistence(c: SnCookiePersistence[F]) =
2525
copy(persistenceO = c.pure[Resource[F, *]].some)
2626

core/shared/src/main/scala/io/chrisdavenport/snickerdoodle/SnCookiePersistence.scala

+1-1
Original file line numberDiff line numberDiff line change
@@ -17,5 +17,5 @@ trait SnCookiePersistence[F[_]]{
1717

1818

1919
object SnCookiePersistence {
20-
def sqlite[F[_]: Async](path: fs2.io.file.Path): SnCookiePersistence[F] = persistence.Sqlite[F](path)
20+
def sqlite[F[_]: Async](path: fs2.io.file.Path): Resource[F, SnCookiePersistence[F]] = persistence.SqlitePersistence[F](path)
2121
}

core/shared/src/main/scala/io/chrisdavenport/snickerdoodle/persistence/Sqlite.scala

-3
This file was deleted.
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
package io.chrisdavenport.snickerdoodle.persistence
2+
3+
object SqlitePersistence extends SqlitePersistencePlatform

examples/src/main/scala/Main.scala

+4-4
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ object Main extends IOApp {
1414
def run(args: List[String]): IO[ExitCode] = {
1515
(
1616
SnCookieJarBuilder.default[IO]
17-
.withSqlitePersistence(fs2.io.file.Path("sample.sqlite")) // Comment this line for JS
17+
.withSqlitePersistence(fs2.io.file.Path("sample.sqlite"))
1818
.expert // Usually you would just use `build` we use buildWithState to expose the internals
1919
.buildWithState,
2020
EmberClientBuilder.default[IO].build
@@ -47,10 +47,10 @@ object Main extends IOApp {
4747
} >>
4848
{
4949
IO.println("") >>
50-
// Comment this block for JS
5150
IO.println("As well as persisted to disk in the sqlite database.") >>
52-
SnCookiePersistence.sqlite[IO](fs2.io.file.Path("sample.sqlite"))
53-
.getAll
51+
SnCookiePersistence.sqlite[IO](fs2.io.file.Path("sample.sqlite")).use(
52+
_.getAll
53+
)
5454
.flatTap(_.traverse_(Console[IO].println(_))) >>
5555
IO.println("") >>
5656
//

project/plugins.sbt

+2-1
Original file line numberDiff line numberDiff line change
@@ -2,4 +2,5 @@ addSbtPlugin("org.typelevel" % "sbt-typelevel-ci-release" % "0.4.12")
22
addSbtPlugin("org.typelevel" % "sbt-typelevel-site" % "0.4.12")
33
addSbtPlugin("org.typelevel" % "sbt-typelevel-settings" % "0.4.12")
44
addSbtPlugin("org.scala-js" % "sbt-scalajs" % "1.10.0")
5-
addSbtPlugin("org.portable-scala" % "sbt-scalajs-crossproject" % "1.2.0")
5+
addSbtPlugin("org.portable-scala" % "sbt-scalajs-crossproject" % "1.2.0")
6+
addSbtPlugin("ch.epfl.scala" % "sbt-scalajs-bundler" % "0.20.0")

0 commit comments

Comments
 (0)