diff --git a/README.md b/README.md index a012e04..6a7ccdf 100644 --- a/README.md +++ b/README.md @@ -1 +1,5 @@ # issuehunter-api + +## Configuration + +* `PORT` HTTP server port, default: 8080 diff --git a/TODO.md b/TODO.md new file mode 100644 index 0000000..1c39efe --- /dev/null +++ b/TODO.md @@ -0,0 +1,4 @@ +https://stevenwhite.com/building-a-rest-service-with-golang-2/ +https://codegangsta.gitbooks.io/building-web-apps-with-go/content/deployment/index.html + +https://derekchiang.com/posts/reusable-and-type-safe-options-for-go-apis/ diff --git a/db/schema.sql b/db/schema.sql new file mode 100644 index 0000000..7125db9 --- /dev/null +++ b/db/schema.sql @@ -0,0 +1,53 @@ +DROP TABLE IF EXISTS campaigns; + +CREATE TABLE IF NOT EXISTS campaigns( + uid VARCHAR(255) NOT NULL PRIMARY KEY, + rewarded BOOLEAN, + total NUMERIC(100), -- uint256 + created_by VARCHAR(255), + pre_reward_period_expires_at TIMESTAMP, + reward_period_expires_at TIMESTAMP, + resolved_by VARCHAR(255), + patch_verifier VARCHAR(255), + tip_per_mille INT, + tips_amount NUMERIC(100), + repo_url VARCHAR(255), + created_at TIMESTAMP NOT NULL, + updated_at TIMESTAMP NOT NULL, + created_at_block_id VARCHAR(255) NOT NULL, + updated_at_block_id VARCHAR(255) NOT NULL +); + +CREATE INDEX campaigns_total_idx ON campaigns (total); +CREATE INDEX campaigns_created_at_idx ON campaigns (created_at); +CREATE INDEX campaigns_updated_at_idx ON campaigns (updated_at); + +DROP TABLE IF EXISTS funds; + +CREATE TABLE IF NOT EXISTS funds( + funder_address VARCHAR(255), + amount NUMERIC(100), -- uint256 + created_at TIMESTAMP NOT NULL, + updated_at TIMESTAMP NOT NULL, + created_at_block_id VARCHAR(255) NOT NULL, + updated_at_block_id VARCHAR(255) NOT NULL +); + +CREATE INDEX funder_address_idx ON funds (funder_address); +CREATE INDEX funds_created_at_idx ON funds (created_at); +CREATE INDEX funds_updated_at_idx ON funds (updated_at); + +DROP TABLE IF EXISTS patches; + +CREATE TABLE IF NOT EXISTS patches( + author_address VARCHAR(255), + ref VARCHAR(255), + created_at TIMESTAMP NOT NULL, + updated_at TIMESTAMP NOT NULL, + created_at_block_id VARCHAR(255) NOT NULL, + updated_at_block_id VARCHAR(255) NOT NULL +); + +CREATE INDEX author_address_idx ON funds (author_address); +CREATE INDEX patches_created_at_idx ON patches (created_at); +CREATE INDEX patches_updated_at_idx ON patches (updated_at); diff --git a/server.go b/server.go new file mode 100644 index 0000000..eb3d4bf --- /dev/null +++ b/server.go @@ -0,0 +1,89 @@ +package main + +import ( + "database/sql" + "fmt" + "log" + "net/http" + "os" + "time" + + "github.com/julienschmidt/httprouter" + _ "github.com/lib/pq" +) + +// Logger is a cool type +type Logger struct { + handler http.Handler +} + +func (l *Logger) ServeHTTP(w http.ResponseWriter, req *http.Request) { + start := time.Now() + + l.handler.ServeHTTP(w, req) + + log.Printf("- %s %s (%s)", req.Method, req.URL.Path, time.Since(start).String()) +} + +func serveIndex(w http.ResponseWriter, req *http.Request, _ httprouter.Params) { + http.ServeFile(w, req, "static/index.html") +} + +// HelloWorld is a cool function +func HelloWorld(w http.ResponseWriter, r *http.Request, _ httprouter.Params) { + // Simply write some test data for now + fmt.Fprintln(w, "Hello World!") +} + +// App is a cool function +func App() http.Handler { + router := httprouter.New() + db := NewDB() + + // Add a handler on /hello + router.GET("/hello", HelloWorld) + + router.GET("/campaigns", CampaignsIndex(db)) + + // Serve static assets + router.GET("/", serveIndex) + router.ServeFiles("/stylesheets/*filepath", http.Dir("static/stylesheets")) + + return &Logger{router} +} + +func CampaignsIndex(db *sql.DB) func(http.ResponseWriter, *http.Request, httprouter.Params) { + return func(rw http.ResponseWriter, r *http.Request, _ httprouter.Params) { + var title, author string + err := db.QueryRow("SELECT title, author FROM campaigns").Scan(&title, &author) + if err != nil && err == sql.ErrNoRows { + fmt.Fprintf(rw, "No campaigns found") + return + } + if err != nil { + panic(err) + } + + fmt.Fprintf(rw, "The first campaign is '%s' by '%s'", title, author) + } +} + +func NewDB() *sql.DB { + db, err := sql.Open("postgres", "postgres://gcappellotto@localhost/issuehunter?sslmode=disable") + if err != nil { + panic(err) + } + + return db +} + +func main() { + port := os.Getenv("PORT") + if port == "" { + port = "8080" + } + + // Fire up the server + fmt.Println("Server listening on port " + port) + http.ListenAndServe(":"+port, App()) +} diff --git a/server_test.go b/server_test.go new file mode 100644 index 0000000..cb5aa41 --- /dev/null +++ b/server_test.go @@ -0,0 +1,50 @@ +package main + +import ( + "io/ioutil" + "net/http" + "net/http/httptest" + "testing" +) + +func Test_HelloWorld(t *testing.T) { + req, err := http.NewRequest("GET", "http://example.com/foo", nil) + if err != nil { + t.Fatal(err) + } + + res := httptest.NewRecorder() + HelloWorld(res, req, nil) + + exp := "Hello World!\n" + act := res.Body.String() + if exp != act { + t.Fatalf("Expected '%s' got '%s'", exp, act) + } +} + +func Test_App(t *testing.T) { + ts := httptest.NewServer(App()) + defer ts.Close() + + res, err := http.Get(ts.URL) + if err != nil { + t.Fatal(err) + } + + body, err := ioutil.ReadAll(res.Body) + res.Body.Close() + + if err != nil { + t.Fatal(err) + } + + exp, err := ioutil.ReadFile("static/index.html") + if err != nil { + t.Fatal(err) + } + + if string(exp) != string(body) { + t.Fatalf("Expected '%s' got '%s'", exp, body) + } +} diff --git a/static/index.html b/static/index.html new file mode 100644 index 0000000..84b1a8e --- /dev/null +++ b/static/index.html @@ -0,0 +1,15 @@ + + +
+ + +