Skip to content

Commit b54c4fd

Browse files
authored
1 parent 7c75eb0 commit b54c4fd

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

60 files changed

+3386
-0
lines changed

.github/workflows/pull_request.yml

+29
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
name: Build the server app
2+
3+
on:
4+
push:
5+
branches: [ master ]
6+
pull_request:
7+
branches: [ master ]
8+
9+
jobs:
10+
build:
11+
12+
runs-on: ubuntu-latest
13+
14+
steps:
15+
- name: checkout
16+
uses: actions/checkout@v2
17+
18+
- name: Setup Scala
19+
uses: japgolly/setup-everything-scala@v1.0
20+
21+
- name: Check code format
22+
run: sbt scalafmtCheckAll
23+
24+
- name: Compile
25+
run: sbt +compile
26+
27+
- name: Run tests
28+
run: sbt +test
29+

.github/workflows/release.yml

+22
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
name: Release
2+
on:
3+
push:
4+
branches: [master, main]
5+
tags: ["*"]
6+
jobs:
7+
publish:
8+
runs-on: ubuntu-20.04
9+
steps:
10+
- uses: actions/checkout@v2.3.4
11+
with:
12+
fetch-depth: 0
13+
- uses: olafurpg/setup-scala@v13
14+
# Sometimes compiling the project leads to yarn.lock changes which creates a conflict with the project version
15+
# derived from sbt-git, this version problem causes the release to fail.
16+
# The workaround is to remove the local changes (git stash) and try releasing again.
17+
- run: sbt ci-release || (git stash && sbt ci-release)
18+
env:
19+
PGP_PASSPHRASE: ${{ secrets.PGP_PASSPHRASE }}
20+
PGP_SECRET: ${{ secrets.PGP_SECRET }}
21+
SONATYPE_PASSWORD: ${{ secrets.SONATYPE_PASSWORD }}
22+
SONATYPE_USERNAME: ${{ secrets.SONATYPE_USERNAME }}

.gitignore

+9
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
target/
2+
.idea/
3+
.bsp/
4+
.vscode/
5+
.metals/**
6+
*.iml
7+
logs/
8+
admin/build/
9+
web/build/

.nvmrc

+2
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
16.7.0
2+

.sbtopts

+3
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
-J-Xmx4G
2+
-J-XX:MaxMetaspaceSize=4G
3+
-J-XX:+CMSClassUnloadingEnabled

.scalafmt.conf

+16
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
version = 3.5.3
2+
project.git = true
3+
project.excludeFilters = [
4+
]
5+
6+
runner.dialect=scala213
7+
8+
maxColumn = 120
9+
assumeStandardLibraryStripMargin = false
10+
11+
continuationIndent.callSite = 2
12+
continuationIndent.defnSite = 4
13+
14+
align.preset = none
15+
16+
onTestFailure = "To fix this, run 'sbt scalafmt' from the project directory, to avoid this issue, ensure you set up IntelliJ to format the code using scalafmt, see https://scalameta.org/scalafmt/docs/installation.html#intellij"

LICENSE

+21
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
MIT License
2+
3+
Copyright (c) 2021 wiringbits
4+
5+
Permission is hereby granted, free of charge, to any person obtaining a copy
6+
of this software and associated documentation files (the "Software"), to deal
7+
in the Software without restriction, including without limitation the rights
8+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9+
copies of the Software, and to permit persons to whom the Software is
10+
furnished to do so, subject to the following conditions:
11+
12+
The above copyright notice and this permission notice shall be included in all
13+
copies or substantial portions of the Software.
14+
15+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21+
SOFTWARE.
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,178 @@
1+
package net.wiringbits.webapp.utils.api
2+
3+
import net.wiringbits.webapp.utils.api.models._
4+
import play.api.libs.json._
5+
import sttp.client3._
6+
import sttp.model._
7+
8+
import scala.concurrent.{ExecutionContext, Future}
9+
import scala.util.{Failure, Success, Try}
10+
11+
trait AdminDataExplorerApiClient {
12+
def getTables: Future[AdminGetTables.Response]
13+
14+
def getTableMetadata(
15+
tableName: String,
16+
sort: List[String],
17+
range: List[Int],
18+
filters: String
19+
): Future[List[Map[String, String]]]
20+
21+
def viewItem(tableName: String, id: String): Future[Map[String, String]]
22+
23+
def viewItems(tableName: String, ids: List[String]): Future[List[Map[String, String]]]
24+
25+
def createItem(tableName: String, request: AdminCreateTable.Request): Future[AdminCreateTable.Response]
26+
27+
def updateItem(tableName: String, id: String, request: Map[String, String]): Future[AdminUpdateTable.Response]
28+
29+
def deleteItem(tableName: String, id: String): Future[AdminDeleteTable.Response]
30+
}
31+
32+
object AdminDataExplorerApiClient {
33+
case class Config(serverUrl: String)
34+
35+
private def asJson[R: Reads] = {
36+
asString
37+
.map {
38+
case Right(response) =>
39+
// handles 2xx responses
40+
Success(response)
41+
case Left(response) =>
42+
// handles non 2xx responses
43+
Try {
44+
val json = Json.parse(response)
45+
// TODO: Unify responses to match the play error format
46+
json
47+
.asOpt[ErrorResponse]
48+
.orElse {
49+
json
50+
.asOpt[PlayErrorResponse]
51+
.map(model => ErrorResponse(model.error.message))
52+
}
53+
.getOrElse(throw new RuntimeException(s"Unexpected JSON response: $response"))
54+
} match {
55+
case Failure(exception) =>
56+
println(s"Unexpected response: ${exception.getMessage}")
57+
exception.printStackTrace()
58+
Failure(new RuntimeException(s"Unexpected response, please try again in a minute"))
59+
case Success(value) =>
60+
Failure(new RuntimeException(value.error))
61+
}
62+
}
63+
.map { t =>
64+
t.map(Json.parse).map(_.as[R])
65+
}
66+
}
67+
68+
class DefaultImpl(config: Config)(implicit
69+
backend: SttpBackend[Future, _],
70+
ec: ExecutionContext
71+
) extends AdminDataExplorerApiClient {
72+
73+
private val ServerAPI = sttp.model.Uri
74+
.parse(config.serverUrl)
75+
.getOrElse(throw new RuntimeException("Invalid server url"))
76+
77+
private def prepareRequest[R: Reads] = {
78+
basicRequest
79+
.contentType(MediaType.ApplicationJson)
80+
.response(asJson[R])
81+
}
82+
83+
override def getTables: Future[AdminGetTables.Response] = {
84+
val path = ServerAPI.path :+ "admin" :+ "tables"
85+
val uri = ServerAPI.withPath(path)
86+
87+
prepareRequest[AdminGetTables.Response]
88+
.get(uri)
89+
.send(backend)
90+
.map(_.body)
91+
.flatMap(Future.fromTry)
92+
}
93+
94+
override def getTableMetadata(
95+
tableName: String,
96+
sort: List[String],
97+
range: List[Int],
98+
filters: String
99+
): Future[List[Map[String, String]]] = {
100+
val path = ServerAPI.path :+ "admin" :+ "tables" :+ tableName
101+
val parameters: Map[String, String] = Map(
102+
"sort" -> sort.mkString("[", ",", "]"),
103+
"range" -> range.mkString("[", ",", "]"),
104+
"filters" -> filters
105+
)
106+
val uri = ServerAPI
107+
.withPath(path)
108+
.addParams(parameters)
109+
110+
prepareRequest[List[Map[String, String]]]
111+
.get(uri)
112+
.send(backend)
113+
.map(_.body)
114+
.flatMap(Future.fromTry)
115+
}
116+
117+
override def viewItem(tableName: String, id: String): Future[Map[String, String]] = {
118+
val path = ServerAPI.path :+ "admin" :+ "tables" :+ tableName :+ id
119+
val uri = ServerAPI.withPath(path)
120+
121+
prepareRequest[Map[String, String]]
122+
.get(uri)
123+
.send(backend)
124+
.map(_.body)
125+
.flatMap(Future.fromTry)
126+
}
127+
128+
override def viewItems(tableName: String, id: List[String]): Future[List[Map[String, String]]] = {
129+
val path = ServerAPI.path :+ "admin" :+ "tables" :+ tableName
130+
val primaryKeyParam = Json.toJson(Map("id" -> id)).toString()
131+
val uri = ServerAPI.withPath(path).withParams(Map("filter" -> primaryKeyParam))
132+
prepareRequest[List[Map[String, String]]]
133+
.get(uri)
134+
.send(backend)
135+
.map(_.body)
136+
.flatMap(Future.fromTry)
137+
}
138+
139+
override def createItem(tableName: String, request: AdminCreateTable.Request): Future[AdminCreateTable.Response] = {
140+
val path = ServerAPI.path :+ "admin" :+ "tables" :+ tableName
141+
val uri = ServerAPI.withPath(path)
142+
143+
prepareRequest[AdminCreateTable.Response]
144+
.post(uri)
145+
.body(Json.toJson(request).toString())
146+
.send(backend)
147+
.map(_.body)
148+
.flatMap(Future.fromTry)
149+
}
150+
151+
override def updateItem(
152+
tableName: String,
153+
id: String,
154+
request: Map[String, String]
155+
): Future[AdminUpdateTable.Response] = {
156+
val path = ServerAPI.path :+ "admin" :+ "tables" :+ tableName :+ id
157+
val uri = ServerAPI.withPath(path)
158+
159+
prepareRequest[AdminUpdateTable.Response]
160+
.put(uri)
161+
.body(Json.toJson(request).toString())
162+
.send(backend)
163+
.map(_.body)
164+
.flatMap(Future.fromTry)
165+
}
166+
167+
override def deleteItem(tableName: String, id: String): Future[AdminDeleteTable.Response] = {
168+
val path = ServerAPI.path :+ "admin" :+ "tables" :+ tableName :+ id
169+
val uri = ServerAPI.withPath(path)
170+
171+
prepareRequest[AdminDeleteTable.Response]
172+
.delete(uri)
173+
.send(backend)
174+
.map(_.body)
175+
.flatMap(Future.fromTry)
176+
}
177+
}
178+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
package net.wiringbits.webapp.utils.api.models
2+
3+
import play.api.libs.json.{Format, Json}
4+
5+
object AdminCreateTable {
6+
case class Request(data: Map[String, String])
7+
8+
case class Response(noData: String = "")
9+
10+
implicit val adminCreateTableRequestFormat: Format[Request] =
11+
Json.format[Request]
12+
13+
implicit val adminCreateTableResponseFormat: Format[Response] =
14+
Json.format[Response]
15+
16+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
package net.wiringbits.webapp.utils.api.models
2+
3+
import play.api.libs.json.{Format, Json}
4+
5+
object AdminDeleteTable {
6+
case class Response(noData: String = "")
7+
8+
implicit val adminDeleteTableResponseFormat: Format[Response] =
9+
Json.format[Response]
10+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
package net.wiringbits.webapp.utils.api.models
2+
3+
import play.api.libs.json.{Format, Json}
4+
5+
object AdminGetTables {
6+
case class Response(data: List[Response.DatabaseTable])
7+
object Response {
8+
case class DatabaseTable(name: String, columns: List[TableColumn], primaryKeyName: String, canBeDeleted: Boolean)
9+
case class TableColumn(
10+
name: String,
11+
`type`: String,
12+
editable: Boolean,
13+
reference: Option[TableReference],
14+
filterable: Boolean
15+
)
16+
case class TableReference(referencedTable: String, referenceField: String)
17+
18+
implicit val adminTableReferenceResponseFormat: Format[TableReference] = Json.format[TableReference]
19+
implicit val adminTableColumnResponseFormat: Format[TableColumn] = Json.format[TableColumn]
20+
implicit val adminDatabaseTableResponseFormat: Format[DatabaseTable] = Json.format[DatabaseTable]
21+
}
22+
implicit val adminGetTablesResponseFormat: Format[Response] = Json.format[Response]
23+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
package net.wiringbits.webapp.utils.api.models
2+
3+
import play.api.libs.json.{Format, Json}
4+
5+
object AdminUpdateTable {
6+
case class Request(data: Map[String, String])
7+
case class Response(id: String)
8+
9+
implicit val adminUpdateTableRequestFormat: Format[Request] =
10+
Json.format[Request]
11+
12+
implicit val adminUpdateTableResponseFormat: Format[Response] =
13+
Json.format[Response]
14+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
package net.wiringbits.webapp.utils.api.models
2+
3+
import play.api.libs.json.{Format, Json}
4+
5+
// play json errors are like:
6+
// {"error":{"requestId":2,"message":"Invalid Json: ..."}}
7+
case class PlayErrorResponse(error: PlayErrorResponse.PlayError)
8+
9+
object PlayErrorResponse {
10+
case class PlayError(message: String)
11+
12+
implicit val playErrorResponseErrorFormat: Format[PlayError] = Json.format[PlayError]
13+
implicit val playErrorResponseFormat: Format[PlayErrorResponse] = Json.format[PlayErrorResponse]
14+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
package net.wiringbits.webapp.utils.api
2+
3+
import play.api.libs.json._
4+
5+
import java.time.Instant
6+
7+
package object models {
8+
9+
/** For some reason, play-json doesn't provide support for Instant in the scalajs version, grabbing the jvm values
10+
* seems to work:
11+
* - https://github.com/playframework/play-json/blob/master/play-json/jvm/src/main/scala/play/api/libs/json/EnvReads.scala
12+
* - https://github.com/playframework/play-json/blob/master/play-json/jvm/src/main/scala/play/api/libs/json/EnvWrites.scala
13+
*/
14+
implicit val instantFormat: Format[Instant] = Format[Instant](
15+
fjs = implicitly[Reads[String]].map(string => Instant.parse(string)),
16+
tjs = Writes[Instant](i => JsString(i.toString))
17+
)
18+
19+
case class ErrorResponse(error: String)
20+
implicit val errorResponseFormat: Format[ErrorResponse] = Json.format[ErrorResponse]
21+
}

0 commit comments

Comments
 (0)