1
1
package io .chrisdavenport .snickerdoodle .persistence
2
2
3
+ import cats .syntax .all ._
4
+ import io .chrisdavenport .snickerdoodle .SnCookie
5
+ import io .chrisdavenport .snickerdoodle .SnCookie .SnCookieKey
3
6
import io .chrisdavenport .snickerdoodle .SnCookiePersistence
4
7
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
6
12
7
13
trait SqlitePersistencePlatform {
8
14
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
+ }
11
96
}
0 commit comments