Scala.js facade for WebSQL API
It's relying on the following reference implementation: https://github.com/nolanlawson/node-websql
This API can be backed by SQLite on Node.js
and react-native
platforms.
val scommonsWebSqlVer = "1.0.0-SNAPSHOT"
libraryDependencies ++= Seq(
"org.scommons.websql" %%% "scommons-websql-core" % scommonsWebSqlVer,
// see migrations/README.md
"org.scommons.websql" %%% "scommons-websql-migrations" % scommonsWebSqlVer,
// high level IO effect API (already includes core)
"org.scommons.websql" %%% "scommons-websql-io" % scommonsWebSqlVer
)
Latest SNAPSHOT
version is published to Sonatype Repo, just make sure you added
the proper dependency resolver to your build.sbt
settings:
resolvers += "Sonatype Snapshots" at "https://oss.sonatype.org/content/repositories/snapshots/"
On react-native
project (using
scommons-expo
module):
import scommons.expo.sqlite.SQLite
val db = SQLite.openDatabase("myfirst.db")
On Node.js
project:
import scommons.websql.WebSQL
val db = WebSQL.openDatabase("myfirst.db")
// or in-memory DB, useful for testing
val db = WebSQL.openDatabase(":memory:")
You can use tx.executeSql
method to run raw SQL queries:
db.transaction { tx =>
tx.executeSql(
"""CREATE TABLE IF NOT EXISTS categories (
| id integer primary key,
| category_name text NOT NULL,
| created_at timestamp NOT NULL DEFAULT (strftime('%s','now') * 1000),
| UNIQUE (category_name)
|)
|""".stripMargin
)
tx.executeSql(
"INSERT INTO categories (category_name) VALUES (?), (?)",
Seq(
"test category 1",
"test category 2"
)
)
}
It can be fully automated by using migrations module.
Example
SQLite
DB context:
import scommons.websql.Database
import scommons.websql.io.SqliteContext
class ShowcaseDBContext(db: Database) extends SqliteContext(db) {
// example of custom encoder
implicit val categoryIdToInt: MappedEncoding[CategoryId, Int] = mappedEncoding[CategoryId, Int](_.value)
implicit val categoryIdEncoder: Encoder[CategoryId] = mappedEncoder[CategoryId, Int]
// example of custom decoder
implicit val intToCategoryId: MappedEncoding[Int, CategoryId] = mappedEncoding[Int, CategoryId](CategoryId)
implicit val categoryIdDecoder: Decoder[CategoryId] = mappedDecoder[Int, CategoryId]
}
Example
DB entity
class:
case class CategoryEntity(id: Int,
categoryName: String)
Data Access Object (DAO
) layer has very similar query IO
API
interface as quill, except that SQL
has
to be written explicitly rather than generated during the build.
Example 1
DAO
class with basic DB queries/actions:
import scommons.websql.io.dao.CommonDao
import scommons.websql.io.showcase.domain._
import scala.concurrent.Future
class CategoryDao(val ctx: ShowcaseDBContext) extends CommonDao {
import ctx._
def getByIdQuery(id: Int): IO[Seq[CategoryEntity], Effect.Read] = {
ctx.runQuery(
sql = "SELECT id, category_name FROM categories WHERE id = ?",
args = id,
extractor = CategoryEntity.tupled
)
}
def getById(id: Int): Future[Option[CategoryEntity]] = {
getOne("getById", ctx.performIO(getByIdQuery(id)))
}
def count(): Future[Int] = {
ctx.performIO(
ctx.runQuerySingle("SELECT count(*) FROM categories", identity[Int])
)
}
def list(optOffset: Option[Int],
limit: Int,
symbols: Option[String]
): Future[(Seq[CategoryEntity], Option[Int])] = {
val text = s"%${symbols.getOrElse("")}%"
val offset = optOffset.getOrElse(0)
val countQuery = optOffset match {
case Some(_) => IO.successful(None)
case None => ctx.runQuerySingle(
sql = "SELECT count(*) FROM categories WHERE category_name LIKE ?",
args = text,
extractor = identity[Int]
).map(Some(_))
}
val fetchQuery = ctx.runQuery(
sql =
"""SELECT
| id,
| category_name
|FROM
| categories
|WHERE
| category_name LIKE ?
|ORDER BY
| category_name
|LIMIT ?
|OFFSET ?
|""".stripMargin,
args = (text, limit, offset),
extractor = CategoryEntity.tupled
)
val q = for {
maybeCount <- countQuery
results <- fetchQuery
} yield {
(results, maybeCount)
}
// internally IO is always performed within transaction
ctx.performIO(q)
}
def insertQuery(entity: CategoryEntity): IO[Int, Effect.Write] = {
ctx.runActionReturning(
"INSERT INTO categories (category_name) VALUES (?)", entity.categoryName
).map(_.toInt)
}
def insert(entity: CategoryEntity): Future[Int] = {
ctx.performIO(insertQuery(entity))
}
def insertMany(list: Seq[CategoryEntity]): Future[Seq[Int]] = {
ctx.performIO(IO.sequence(list.map(insertQuery)))
}
def upsert(entity: CategoryEntity): Future[CategoryEntity] = {
val q = for {
maybeCategory <- ctx.runQuery(
sql = "SELECT id, category_name FROM categories WHERE category_name = ?",
args = entity.categoryName,
extractor = CategoryEntity.tupled
).map(_.headOption)
id <- maybeCategory match {
case None => insertQuery(entity)
case Some(c) =>
updateQuery(entity.copy(id = c.id))
.map(_ => c.id)
}
res <- getByIdQuery(id).map(_.head)
} yield res
ctx.performIO(q)
}
def updateQuery(entity: CategoryEntity): IO[Long, Effect.Write] = {
ctx.runAction(
sql = "UPDATE categories SET category_name = ? WHERE id = ?",
args = (entity.categoryName, entity.id)
)
}
def update(entity: CategoryEntity): Future[Boolean] = {
isUpdated(ctx.performIO(updateQuery(entity)))
}
def updateMany(list: Seq[CategoryEntity]): Future[Seq[Boolean]] = {
ctx.performIO(IO.sequence(list.map(updateQuery)).map { results =>
results.map(_ > 0)
})
}
def deleteAll(): Future[Long] = {
ctx.performIO(ctx.runAction("DELETE FROM categories"))
}
}
Example 2
DAO
class with more advanced DB queries:
import scommons.websql.io.dao.CommonDao
import scommons.websql.io.showcase.domain._
import scala.concurrent.Future
class ProductDao(val ctx: ShowcaseDBContext) extends CommonDao {
import ctx._
def allProducts(): Future[Seq[ProductEntity]] = {
ctx.performIO(ctx.runQuery(
"SELECT id, name, category_id FROM products ORDER BY id",
ProductEntity.tupled
))
}
def joinProducts(): Future[Seq[(ProductEntity, CategoryEntity)]] = {
ctx.performIO(ctx.runQuery(
sql =
"""SELECT
| p.id AS _0, -- *******************************
| p.name AS _1, -- * NOTE:
| p.category_id AS _2, -- * for JOIN queries from different tables
| c.id AS _3, -- * ALWAYS specify custom unique fields names !!!
| c.category_name AS _4 -- *******************************
|FROM (
| SELECT id, name, category_id FROM products ORDER BY id
|) AS p
|INNER JOIN categories c ON p.category_id = c.id
|""".stripMargin,
extractor = { case (p, c) =>
(ProductEntity.tupled(p), CategoryEntity.tupled(c))
}: (((Int, String, Option[Int]), (Int, String))) =>
(ProductEntity, CategoryEntity)
))
}
def leftJoinProducts(): Future[Seq[(ProductEntity, Option[CategoryEntity])]] = {
ctx.performIO(ctx.runQuery(
sql =
"""SELECT
| p.id AS _0, -- *******************************
| p.name AS _1, -- * NOTE:
| p.category_id AS _2, -- * for JOIN queries from different tables
| c.id AS _3, -- * ALWAYS specify custom unique fields names !!!
| c.category_name AS _4 -- *******************************
|FROM (
| SELECT id, name, category_id FROM products ORDER BY id
|) AS p
|LEFT JOIN categories c ON p.category_id = c.id
|""".stripMargin,
extractor = { case (p, c) =>
(ProductEntity.tupled(p), c.map(CategoryEntity.tupled))
}: (((Int, String, Option[Int]), Option[(Int, String)])) =>
(ProductEntity, Option[CategoryEntity])
))
}
}
Example business logic / service layer:
import scommons.websql.io.showcase.domain.CategoryEntity
import scommons.websql.io.showcase.domain.dao.CategoryDao
import scala.concurrent.ExecutionContext.Implicits.global
import scala.concurrent.Future
class CategoryService(dao: CategoryDao) {
def getById(id: Int): Future[CategoryEntity] = {
dao.getById(id).map(ensureCategory(id, _))
}
def add(entity: CategoryEntity): Future[CategoryEntity] = {
for {
insertId <- dao.insert(entity)
entity <- dao.getById(insertId).map(ensureCategory(insertId, _))
} yield entity
}
private def ensureCategory(id: Int, maybeCat: Option[CategoryEntity]): CategoryEntity = {
maybeCat.getOrElse {
throw new IllegalArgumentException(s"Category is not found, categoryId: $id")
}
}
}
You can find more documentation here