Skip to content

Commit 8348a65

Browse files
committed
Add POST /query. It calls gitbase and returns json
Signed-off-by: Carlos Martín <[email protected]>
1 parent 66fd811 commit 8348a65

File tree

4 files changed

+124
-6
lines changed

4 files changed

+124
-6
lines changed

cmd/server/main.go

+17-2
Original file line numberDiff line numberDiff line change
@@ -1,25 +1,33 @@
11
package main
22

33
import (
4+
"database/sql"
45
"fmt"
56
"net/http"
67

78
"github.com/src-d/gitbase-playground/server"
89
"github.com/src-d/gitbase-playground/server/handler"
910
"github.com/src-d/gitbase-playground/server/service"
1011

12+
_ "github.com/go-sql-driver/mysql"
1113
"github.com/kelseyhightower/envconfig"
1214
)
1315

1416
// version will be replaced automatically by the CI build.
1517
// See https://github.com/src-d/ci/blob/v1/Makefile.main#L56
1618
var version = "dev"
1719

20+
// Note: maxAllowedPacket must be explicitly set for go-sql-driver/mysql v1.3.
21+
// Otherwise gitbase will be asked for the max_allowed_packet column and the
22+
// query will fail.
23+
// The next release should make this parameter optional for us:
24+
// https://github.com/go-sql-driver/mysql/pull/680
1825
type appConfig struct {
1926
Env string `envconfig:"ENV" default:"production"`
2027
Host string `envconfig:"HOST" default:"0.0.0.0"`
2128
Port int `envconfig:"PORT" default:"8080"`
2229
ServerURL string `envconfig:"SERVER_URL"`
30+
DBConn string `envconfig:"DB_CONNECTION" default:"root@tcp(localhost:3306)/none?maxAllowedPacket=4194304"`
2331
}
2432

2533
func main() {
@@ -33,11 +41,18 @@ func main() {
3341
// logger
3442
logger := service.NewLogger(conf.Env)
3543

44+
// database
45+
db, err := sql.Open("mysql", conf.DBConn)
46+
if err != nil {
47+
logger.Fatalf("error opening the database: %s", err)
48+
}
49+
defer db.Close()
50+
3651
static := handler.NewStatic("build", conf.ServerURL)
3752

3853
// start the router
39-
router := server.Router(logger, static, version)
54+
router := server.Router(logger, static, version, db)
4055
logger.Infof("listening on %s:%d", conf.Host, conf.Port)
41-
err := http.ListenAndServe(fmt.Sprintf("%s:%d", conf.Host, conf.Port), router)
56+
err = http.ListenAndServe(fmt.Sprintf("%s:%d", conf.Host, conf.Port), router)
4257
logger.Fatal(err)
4358
}

server/handler/query.go

+88
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,88 @@
1+
package handler
2+
3+
import (
4+
"database/sql"
5+
"encoding/json"
6+
"fmt"
7+
"io/ioutil"
8+
"net/http"
9+
10+
"github.com/src-d/gitbase-playground/server/serializer"
11+
)
12+
13+
type queryRequest struct {
14+
Query string `json:"query"`
15+
Limit int `json:"limit"`
16+
}
17+
18+
// genericVals returns a slice of interface{}, each one a pointer to a NullString
19+
func genericVals(nColumns int) []interface{} {
20+
columnVals := make([]sql.NullString, nColumns)
21+
columnValsPtr := make([]interface{}, nColumns)
22+
23+
for i := range columnVals {
24+
columnValsPtr[i] = &columnVals[i]
25+
}
26+
27+
return columnValsPtr
28+
}
29+
30+
// Query returns a function that forwards an SQL query to gitbase and returns
31+
// the rows as JSON
32+
func Query(db *sql.DB) RequestProcessFunc {
33+
return func(r *http.Request) (*serializer.Response, error) {
34+
var queryRequest queryRequest
35+
body, err := ioutil.ReadAll(r.Body)
36+
defer r.Body.Close()
37+
if err == nil {
38+
err = json.Unmarshal(body, &queryRequest)
39+
}
40+
41+
if err != nil {
42+
return nil, err
43+
}
44+
45+
// TODO (carlosms) this only works if the query does not end in ;
46+
// and does not have a limit. It will also fail for queries like
47+
// DESCRIBE TABLE
48+
query := fmt.Sprintf("%s LIMIT %d", queryRequest.Query, queryRequest.Limit)
49+
rows, err := db.Query(query)
50+
if err != nil {
51+
return nil, serializer.NewHTTPError(http.StatusBadRequest, err.Error())
52+
}
53+
defer rows.Close()
54+
55+
columnNames, _ := rows.Columns()
56+
nColumns := len(columnNames)
57+
columnValsPtr := genericVals(nColumns)
58+
59+
tableData := make([]map[string]string, 0)
60+
61+
for rows.Next() {
62+
if err := rows.Scan(columnValsPtr...); err != nil {
63+
return nil, err
64+
}
65+
66+
colData := make(map[string]string)
67+
68+
for i, val := range columnValsPtr {
69+
var st string
70+
sqlSt, _ := val.(*sql.NullString)
71+
72+
if sqlSt.Valid {
73+
st = sqlSt.String
74+
}
75+
76+
colData[columnNames[i]] = st
77+
}
78+
79+
tableData = append(tableData, colData)
80+
}
81+
82+
if err := rows.Err(); err != nil {
83+
return nil, err
84+
}
85+
86+
return serializer.NewQueryResponse(tableData, columnNames), nil
87+
}
88+
}

server/router.go

+4
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
package server
22

33
import (
4+
"database/sql"
45
"net/http"
56

67
"github.com/src-d/gitbase-playground/server/handler"
@@ -17,6 +18,7 @@ func Router(
1718
logger *logrus.Logger,
1819
static *handler.Static,
1920
version string,
21+
db *sql.DB,
2022
) http.Handler {
2123

2224
// cors options
@@ -33,6 +35,8 @@ func Router(
3335
r.Use(cors.New(corsOptions).Handler)
3436
r.Use(lg.RequestLogger(logger))
3537

38+
r.Post("/query", handler.APIHandlerFunc(handler.Query(db)))
39+
3640
r.Get("/version", handler.APIHandlerFunc(handler.Version(version)))
3741

3842
r.Get("/static/*", static.ServeHTTP)

server/serializer/serializers.go

+15-4
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ type HTTPError interface {
1515
type Response struct {
1616
Status int `json:"status"`
1717
Data interface{} `json:"data,omitempty"`
18+
Meta interface{} `json:"meta,omitempty"`
1819
Errors []HTTPError `json:"errors,omitempty"`
1920
}
2021

@@ -47,16 +48,17 @@ func NewHTTPError(statusCode int, msg ...string) HTTPError {
4748
return httpError{Status: statusCode, Title: strings.Join(msg, " ")}
4849
}
4950

50-
func newResponse(c interface{}) *Response {
51-
if c == nil {
51+
func newResponse(data interface{}, meta interface{}) *Response {
52+
if data == nil {
5253
return &Response{
5354
Status: http.StatusNoContent,
5455
}
5556
}
5657

5758
return &Response{
5859
Status: http.StatusOK,
59-
Data: c,
60+
Data: data,
61+
Meta: meta,
6062
}
6163
}
6264

@@ -71,5 +73,14 @@ type versionResponse struct {
7173

7274
// NewVersionResponse returns a Response with current version of the server
7375
func NewVersionResponse(version string) *Response {
74-
return newResponse(versionResponse{version})
76+
return newResponse(versionResponse{version}, nil)
77+
}
78+
79+
type queryMetaResponse struct {
80+
Headers []string `json:"headers"`
81+
}
82+
83+
// NewQueryResponse returns a Response with table headers and row contents
84+
func NewQueryResponse(rows []map[string]string, columnNames []string) *Response {
85+
return newResponse(rows, queryMetaResponse{columnNames})
7586
}

0 commit comments

Comments
 (0)