diff --git a/build.sbt b/build.sbt index 52805f0..a9041a6 100644 --- a/build.sbt +++ b/build.sbt @@ -56,6 +56,7 @@ lazy val core = crossProject(JVMPlatform, JSPlatform) scalaJSLinkerConfig ~= { _.withModuleKind(ModuleKind.CommonJSModule)}, libraryDependencies ++= Seq( "io.chrisdavenport" %%% "publicsuffix-retrieval-client" % publicSuffixV, + "io.chrisdavenport" %%% "sqlite-sjs" % "0.0.1", ) // TODO Actually Implement with this // npmDependencies ++= Seq( @@ -79,6 +80,7 @@ lazy val examples = crossProject(JVMPlatform, JSPlatform) "org.http4s" %%% "http4s-ember-client" % http4sV, ) ) + .jsConfigure { project => project.enablePlugins(ScalaJSBundlerPlugin) } .jsSettings( scalaJSUseMainModuleInitializer := true, scalaJSLinkerConfig ~= { _.withModuleKind(ModuleKind.CommonJSModule)}, diff --git a/core/js/src/main/scala/io/chrisdavenport/snickerdoodle/persistence/SqlitePersistencePlatform.scala b/core/js/src/main/scala/io/chrisdavenport/snickerdoodle/persistence/SqlitePersistencePlatform.scala index eb87516..ab10465 100644 --- a/core/js/src/main/scala/io/chrisdavenport/snickerdoodle/persistence/SqlitePersistencePlatform.scala +++ b/core/js/src/main/scala/io/chrisdavenport/snickerdoodle/persistence/SqlitePersistencePlatform.scala @@ -1,11 +1,96 @@ package io.chrisdavenport.snickerdoodle.persistence +import cats.syntax.all._ +import io.chrisdavenport.snickerdoodle.SnCookie +import io.chrisdavenport.snickerdoodle.SnCookie.SnCookieKey import io.chrisdavenport.snickerdoodle.SnCookiePersistence import cats.effect.kernel._ - +import io.chrisdavenport.sqlitesjs.Sqlite +import io.circe.syntax._ +import io.circe.Decoder +import io.circe.HCursor trait SqlitePersistencePlatform { - def apply[F[_]: Async](path: fs2.io.file.Path): SnCookiePersistence[F] = - throw new RuntimeException("scala.js sqlite not yet implemented") + def apply[F[_]: Async](path: fs2.io.file.Path): Resource[F, SnCookiePersistence[F]] = + Sqlite.fromFile[F](path.toString).map(new SqlitePersistenceImpl[F](_)) + + + private class SqlitePersistenceImpl[F[_]: Async](sqlite: Sqlite[F]) extends SnCookiePersistence[F]{ + + def updateLastAccessed(key: SnCookieKey, lastAccessed: Long): F[Unit] = { + val updateStatement = "Update cookies SET lastAccessed = ? where name = ? and domain = ? and path = ?" + sqlite.run(updateStatement, List(lastAccessed.asJson, key.name.asJson, key.domain.asJson, key.path.asJson)) + .void + } + + def clear: F[Unit] = { + val clearStatement = "DELETE FROM cookies" + sqlite.exec(clearStatement) + } + def clearExpired(now: Long): F[Unit] = { + val clearExpiredStatment = "DELETE FROM cookies where expiry < ?" + sqlite.run(clearExpiredStatment, List(now.asJson)).void + } + def create(cookie: SnCookie): F[Unit] = { + val raw = SnCookie.toRaw(cookie) + val insertStatement = "INSERT OR REPLACE INTO cookies (name, value, domain, path, expiry, lastAccessed, creationTime, isSecure, isHttpOnly, isHostOnly, sameSite, scheme, extension) values (?,?,?,?,?,?,?,?,?,?,?,?,?)" + 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)) + .void + } + + def createTable: F[Unit] = { + sqlite.exec(createTableStatement) + } + def getAll: F[List[SnCookie]] = { + val selectStatement = "SELECT name,value,domain,path,expiry,lastAccessed,creationTime,isSecure,isHttpOnly, isHostOnly, sameSite,scheme,extension FROM cookies" + sqlite.all(selectStatement).flatMap( + l => l.traverse(_.as[SnCookie.RawSnCookie](rawDecoder).liftTo[F].map(SnCookie.fromRaw)) + ) + } + + } + + private val createTableStatement = { + """CREATE TABLE IF NOT EXISTS cookies ( + name TEXT NOT NULL, + value TEXT NOT NULL, + domain TEXT NOT NULL, + path TEXT NOT NULL, + expiry INTEGER NOT NULL, -- Either MaxAge (relative to time called) or Expires (explicit) or HttpDate.MaxValue + lastAccessed INTEGER NOT NULL, + creationTime INTEGER NOT NULL, + isSecure INTEGER NOT NULL, -- Boolean + isHttpOnly INTEGER NOT NULL, -- Boolean + isHostOnly INTEGER NOT NULL, -- Boolean + sameSite INTEGER NOT NULL, -- + scheme INTEGER, + extension TEXT, + CONSTRAINT cookiesunique UNIQUE (name, domain, path) + )""" + } + + private val rawDecoder = new Decoder[SnCookie.RawSnCookie]{ + def apply(c: HCursor): Decoder.Result[SnCookie.RawSnCookie] = ( + c.downField("name").as[String], + c.downField("value").as[String], + c.downField("domain").as[String], + c.downField("path").as[String], + c.downField("expiry").as[Long], + c.downField("lastAccessed").as[Long], + c.downField("creationTime").as[Long], + c.downField("isSecure").as[Boolean](intBoolean), + c.downField("isHttpOnly").as[Boolean](intBoolean), + c.downField("isHostOnly").as[Boolean](intBoolean), + c.downField("sameSite").as[Int], + c.downField("scheme").as[Option[Int]], + c.downField("extensione").as[Option[String]] + ).mapN(SnCookie.RawSnCookie.apply) + } + + private val intBoolean: Decoder[Boolean] = Decoder[Int].emap{ + case 0 => false.asRight + case 1 => true.asRight + case _ => "Invalid Integer Boolean".asLeft + } } \ No newline at end of file diff --git a/core/jvm/src/main/scala/io/chrisdavenport/snickerdoodle/persistence/SqlitePersistencePlatform.scala b/core/jvm/src/main/scala/io/chrisdavenport/snickerdoodle/persistence/SqlitePersistencePlatform.scala index af420b2..41df335 100644 --- a/core/jvm/src/main/scala/io/chrisdavenport/snickerdoodle/persistence/SqlitePersistencePlatform.scala +++ b/core/jvm/src/main/scala/io/chrisdavenport/snickerdoodle/persistence/SqlitePersistencePlatform.scala @@ -52,7 +52,7 @@ trait SqlitePersistencePlatform { self => } private[snickerdoodle] def create[F[_]: MonadCancelThrow](xa: Transactor[F])(cookie: SnCookie): F[Int] = { - Update[SnCookie.RawSnCookie]("INSERT OR REPLACE INTO cookies values (?,?,?,?,?,?,?,?,?,?,?,?,?)") + Update[SnCookie.RawSnCookie]("INSERT OR REPLACE INTO cookies (name, value, domain, path, expiry, lastAccessed, creationTime, isSecure, isHttpOnly, isHostOnly, sameSite, scheme, extension) values (?,?,?,?,?,?,?,?,?,?,?,?,?)") .run(SnCookie.toRaw(cookie)) .transact(xa) } @@ -76,7 +76,7 @@ trait SqlitePersistencePlatform { self => .transact(xa) } - def apply[F[_]: Async](path: fs2.io.file.Path): SnCookiePersistence[F] = { + def apply[F[_]: Async](path: fs2.io.file.Path): Resource[F, SnCookiePersistence[F]] = { val xa = transactor[F](path) new SnCookiePersistence[F] { def updateLastAccessed(key: SnCookie.SnCookieKey, lastAccessed: Long): F[Unit] = @@ -96,6 +96,6 @@ trait SqlitePersistencePlatform { self => def getAll: F[List[SnCookie]] = self.selectAll(xa) - } + }.pure[Resource[F, *]] } } \ No newline at end of file diff --git a/core/shared/src/main/scala/io/chrisdavenport/snickerdoodle/SnCookieJarBuilder.scala b/core/shared/src/main/scala/io/chrisdavenport/snickerdoodle/SnCookieJarBuilder.scala index fdbd949..2d37439 100644 --- a/core/shared/src/main/scala/io/chrisdavenport/snickerdoodle/SnCookieJarBuilder.scala +++ b/core/shared/src/main/scala/io/chrisdavenport/snickerdoodle/SnCookieJarBuilder.scala @@ -20,7 +20,7 @@ final class SnCookieJarBuilder[F[_]: Async] private ( def withSupervisor(s: Supervisor[F]) = copy(supervisorO = s.some) def withSqlitePersistence(path: Path) = - copy(persistenceO = SnCookiePersistence.sqlite(path).pure[Resource[F, *]].some) + copy(persistenceO = SnCookiePersistence.sqlite(path).some) def withPersistence(c: SnCookiePersistence[F]) = copy(persistenceO = c.pure[Resource[F, *]].some) diff --git a/core/shared/src/main/scala/io/chrisdavenport/snickerdoodle/SnCookiePersistence.scala b/core/shared/src/main/scala/io/chrisdavenport/snickerdoodle/SnCookiePersistence.scala index 76f3fd3..4fa675f 100644 --- a/core/shared/src/main/scala/io/chrisdavenport/snickerdoodle/SnCookiePersistence.scala +++ b/core/shared/src/main/scala/io/chrisdavenport/snickerdoodle/SnCookiePersistence.scala @@ -17,5 +17,5 @@ trait SnCookiePersistence[F[_]]{ object SnCookiePersistence { - def sqlite[F[_]: Async](path: fs2.io.file.Path): SnCookiePersistence[F] = persistence.Sqlite[F](path) + def sqlite[F[_]: Async](path: fs2.io.file.Path): Resource[F, SnCookiePersistence[F]] = persistence.SqlitePersistence[F](path) } \ No newline at end of file diff --git a/core/shared/src/main/scala/io/chrisdavenport/snickerdoodle/persistence/Sqlite.scala b/core/shared/src/main/scala/io/chrisdavenport/snickerdoodle/persistence/Sqlite.scala deleted file mode 100644 index 007888e..0000000 --- a/core/shared/src/main/scala/io/chrisdavenport/snickerdoodle/persistence/Sqlite.scala +++ /dev/null @@ -1,3 +0,0 @@ -package io.chrisdavenport.snickerdoodle.persistence - -object Sqlite extends SqlitePersistencePlatform \ No newline at end of file diff --git a/core/shared/src/main/scala/io/chrisdavenport/snickerdoodle/persistence/SqlitePersistence.scala b/core/shared/src/main/scala/io/chrisdavenport/snickerdoodle/persistence/SqlitePersistence.scala new file mode 100644 index 0000000..9e8ed19 --- /dev/null +++ b/core/shared/src/main/scala/io/chrisdavenport/snickerdoodle/persistence/SqlitePersistence.scala @@ -0,0 +1,3 @@ +package io.chrisdavenport.snickerdoodle.persistence + +object SqlitePersistence extends SqlitePersistencePlatform \ No newline at end of file diff --git a/examples/src/main/scala/Main.scala b/examples/src/main/scala/Main.scala index 388d3aa..965131b 100644 --- a/examples/src/main/scala/Main.scala +++ b/examples/src/main/scala/Main.scala @@ -14,7 +14,7 @@ object Main extends IOApp { def run(args: List[String]): IO[ExitCode] = { ( SnCookieJarBuilder.default[IO] - .withSqlitePersistence(fs2.io.file.Path("sample.sqlite")) // Comment this line for JS + .withSqlitePersistence(fs2.io.file.Path("sample.sqlite")) .expert // Usually you would just use `build` we use buildWithState to expose the internals .buildWithState, EmberClientBuilder.default[IO].build @@ -47,10 +47,10 @@ object Main extends IOApp { } >> { IO.println("") >> - // Comment this block for JS IO.println("As well as persisted to disk in the sqlite database.") >> - SnCookiePersistence.sqlite[IO](fs2.io.file.Path("sample.sqlite")) - .getAll + SnCookiePersistence.sqlite[IO](fs2.io.file.Path("sample.sqlite")).use( + _.getAll + ) .flatTap(_.traverse_(Console[IO].println(_))) >> IO.println("") >> // diff --git a/project/plugins.sbt b/project/plugins.sbt index 5006d86..6bcde10 100644 --- a/project/plugins.sbt +++ b/project/plugins.sbt @@ -2,4 +2,5 @@ addSbtPlugin("org.typelevel" % "sbt-typelevel-ci-release" % "0.4.12") addSbtPlugin("org.typelevel" % "sbt-typelevel-site" % "0.4.12") addSbtPlugin("org.typelevel" % "sbt-typelevel-settings" % "0.4.12") addSbtPlugin("org.scala-js" % "sbt-scalajs" % "1.10.0") -addSbtPlugin("org.portable-scala" % "sbt-scalajs-crossproject" % "1.2.0") \ No newline at end of file +addSbtPlugin("org.portable-scala" % "sbt-scalajs-crossproject" % "1.2.0") +addSbtPlugin("ch.epfl.scala" % "sbt-scalajs-bundler" % "0.20.0") \ No newline at end of file