From 2789d477e26225a50b56cc160974bd26bb822e5f Mon Sep 17 00:00:00 2001 From: Erik Arvidsson Date: Fri, 17 Jul 2015 10:17:09 -0700 Subject: [PATCH] Add an RPC call to server to get the dataset root And use that in the heatmap ui --- clients/explore/noms_store.js | 27 ++++--- clients/explore/package.json | 5 +- clients/pitchmap/ui/main.js | 10 +-- clients/server/server.go | 76 +++++++++++++++----- clients/server/server_test.go | 129 ++++++++++++++++++++++++++++++++++ 5 files changed, 208 insertions(+), 39 deletions(-) create mode 100644 clients/server/server_test.go diff --git a/clients/explore/noms_store.js b/clients/explore/noms_store.js index 1fadf38082..198d955858 100644 --- a/clients/explore/noms_store.js +++ b/clients/explore/noms_store.js @@ -8,18 +8,22 @@ var nomsPort = "8000"; var nomsServer = location.protocol + '//' + host + ":" + nomsPort; var rpc = { + dataset: nomsServer + '/dataset', get: nomsServer + '/get', - root: nomsServer + '/root' -} + root: nomsServer + '/root', +}; // TODO: Use whatwg-fetch function fetch(url) { - return new Promise(function(fulfill) { + return new Promise((resolve, reject) => { var xhr = new XMLHttpRequest(); - xhr.addEventListener('load', function(e) { - fulfill(e.target.responseText); - }); - xhr.open("get", url, true); + xhr.onload = (e) => { + resolve(e.target.responseText); + }; + xhr.onerror = (e) => { + reject(e.target.statusText); + }; + xhr.open('get', url, true); xhr.send(); }); } @@ -32,7 +36,12 @@ function getRoot() { return fetch(rpc.root); } +function getDataset(id) { + return fetch(rpc.get + '?id=' + id) +} + module.exports = { - getRoot: getRoot, - getChunk: getChunk + getChunk, + getDataset, + getRoot, }; diff --git a/clients/explore/package.json b/clients/explore/package.json index 849959ce92..65ea2fcc0f 100644 --- a/clients/explore/package.json +++ b/clients/explore/package.json @@ -5,12 +5,13 @@ "immutable": "^3.7.4" }, "devDependencies": { + "babelify": "^6.1.3", "browserify": "^6.2.0", "uglify-js": "~2.4.15", "watchify": "^2.1.1" }, "scripts": { - "start": "watchify -o explore.js -v -d main.js", - "build": "NODE_ENV=production browserify main.js | uglifyjs -cm > explore.js" + "start": "watchify -o explore.js -t babelify -v -d main.js", + "build": "NODE_ENV=production browserify main.js -t babelify | uglifyjs -cm > explore.js" } } diff --git a/clients/pitchmap/ui/main.js b/clients/pitchmap/ui/main.js index 131767e830..8b16035e89 100644 --- a/clients/pitchmap/ui/main.js +++ b/clients/pitchmap/ui/main.js @@ -6,20 +6,12 @@ var Immutable = require('immutable'); var React = require('react'); var Map = require('./map.js'); -store.getRoot().then(function(s) { +store.getDataset('mlb/heatmap').then(function(s) { return decode.readValue(s, store.getChunk); -}).then(function(v) { - return getDatasetRoot(v, 'mlb/heatmap'); }).then(getPitchers).then(renderPitchersList).catch(function(err) { console.error(err); }); -function getDatasetRoot(root, id) { - return root.first().get('value').find(function(map) { - return map.get('id') === id; - }).get('root'); -} - function getPitchers(datasetRoot) { return datasetRoot.first().get('value') } diff --git a/clients/server/server.go b/clients/server/server.go index 4712bbf617..7265ad4524 100644 --- a/clients/server/server.go +++ b/clients/server/server.go @@ -7,53 +7,91 @@ import ( "net/http" "github.com/attic-labs/noms/chunks" + "github.com/attic-labs/noms/datas" + "github.com/attic-labs/noms/dataset/mgmt" "github.com/attic-labs/noms/ref" ) var ( - port *string = flag.String("port", "8000", "") - cs chunks.ChunkStore + port = flag.String("port", "8000", "") ) -func handler(w http.ResponseWriter, r *http.Request) { +type server struct { + chunks.ChunkStore +} + +func (s server) handle(w http.ResponseWriter, r *http.Request) { w.Header().Add("Access-Control-Allow-Origin", "*") switch r.URL.Path[1:] { case "root": + cs := s.ChunkStore w.Header().Add("content-type", "text/plain") fmt.Fprintf(w, "%v", cs.Root().String()) case "get": - hashString := r.URL.Query()["ref"][0] - ref, err := ref.Parse(hashString) - if err != nil { - http.Error(w, fmt.Sprintf("Parse error: %v", err), http.StatusBadRequest) - return + if refs, ok := r.URL.Query()["ref"]; ok { + s.handleGetRef(w, refs[0]) + } else { + http.Error(w, "Missing query param ref", http.StatusBadRequest) } - - reader, err := cs.Get(ref) - if err != nil { - http.Error(w, fmt.Sprintf("Fetch error: %v", err), http.StatusNotFound) - return + case "dataset": + if ids, ok := r.URL.Query()["id"]; ok { + s.handleGetDataset(w, ids[0]) + } else { + http.Error(w, "Missing query param id", http.StatusBadRequest) } - - w.Header().Add("content-type", "application/octet-stream") - w.Header().Add("cache-control", "max-age=31536000") // 1 year - io.Copy(w, reader) default: http.Error(w, fmt.Sprintf("Unrecognized: %v", r.URL.Path[1:]), http.StatusBadRequest) } } +func (s server) handleGetRef(w http.ResponseWriter, hashString string) { + cs := s.ChunkStore + ref, err := ref.Parse(hashString) + if err != nil { + http.Error(w, fmt.Sprintf("Parse error: %v", err), http.StatusBadRequest) + return + } + + reader, err := cs.Get(ref) + if err != nil { + // TODO: Maybe we should not expose the internal path? + http.Error(w, fmt.Sprintf("Fetch error: %v", err), http.StatusNotFound) + return + } + + if reader == nil { + http.Error(w, fmt.Sprintf("No such ref: %v", hashString), http.StatusNotFound) + return + } + + w.Header().Add("content-type", "application/octet-stream") + w.Header().Add("cache-control", "max-age=31536000") // 1 year + io.Copy(w, reader) +} + +func (s server) handleGetDataset(w http.ResponseWriter, id string) { + cs := s.ChunkStore + rootDataStore := datas.NewDataStore(cs, cs.(chunks.RootTracker)) + dataset := mgmt.GetDatasetRoot(mgmt.GetDatasets(rootDataStore), id) + if dataset == nil { + http.Error(w, fmt.Sprintf("Dataset not found: %s", id), http.StatusNotFound) + return + } + w.Header().Add("content-type", "text/plain") + fmt.Fprintf(w, "%s", dataset.Ref()) +} + func main() { flags := chunks.NewFlags() flag.Parse() - cs = flags.CreateStore() + cs := flags.CreateStore() if cs == nil { flag.Usage() return } - http.HandleFunc("/", handler) + http.HandleFunc("/", cs.(server).handle) http.ListenAndServe(fmt.Sprintf(":%s", *port), nil) } diff --git a/clients/server/server_test.go b/clients/server/server_test.go new file mode 100644 index 0000000000..c6286d3317 --- /dev/null +++ b/clients/server/server_test.go @@ -0,0 +1,129 @@ +package main + +import ( + "net/http" + "net/http/httptest" + "testing" + + "github.com/attic-labs/noms/chunks" + "github.com/attic-labs/noms/datas" + "github.com/attic-labs/noms/dataset" + "github.com/attic-labs/noms/ref" + "github.com/attic-labs/noms/types" + "github.com/stretchr/testify/assert" +) + +var datasetID = "testdataset" + +func createTestStore() chunks.ChunkStore { + ms := &chunks.MemoryStore{} + datasetDs := dataset.NewDataset(datas.NewDataStore(ms, ms), datasetID) + datasetRoot := types.NewString("Root value for " + datasetID) + datasetDs = datasetDs.Commit(datas.NewRootSet().Insert( + datas.NewRoot().SetParents( + types.NewSet()).SetValue(datasetRoot))) + return ms +} + +func TestBadRequest(t *testing.T) { + assert := assert.New(t) + + req, _ := http.NewRequest("GET", "/bad", nil) + w := httptest.NewRecorder() + + ms := &chunks.MemoryStore{} + s := server{ms} + s.handle(w, req) + assert.Equal(w.Code, http.StatusBadRequest) +} + +func TestRoot(t *testing.T) { + assert := assert.New(t) + + req, _ := http.NewRequest("GET", "/root", nil) + w := httptest.NewRecorder() + ms := createTestStore() + s := server{ms} + s.handle(w, req) + assert.Equal(w.Code, http.StatusOK) + ref, err := ref.Parse(w.Body.String()) + assert.NoError(err) + assert.Equal(ms.Root(), ref) +} + +func TestGetRef(t *testing.T) { + assert := assert.New(t) + + ms := createTestStore() + rootRef := ms.Root().String() + + req, _ := http.NewRequest("GET", "/get?ref="+rootRef, nil) + w := httptest.NewRecorder() + s := server{ms} + s.handle(w, req) + assert.Equal(w.Code, http.StatusOK) + assert.Equal(`j {"set":[{"ref":"sha1-b432c2dd6d7b6e7e163cab2517d1e6221d5d595c"}]} +`, w.Body.String()) +} + +func TestGetInvalidRef(t *testing.T) { + assert := assert.New(t) + + ms := createTestStore() + rootRef := "sha1-xxx" + + req, _ := http.NewRequest("GET", "/get?ref="+rootRef, nil) + w := httptest.NewRecorder() + s := server{ms} + s.handle(w, req) + assert.Equal(w.Code, http.StatusBadRequest) +} + +func TestGetNonExistingRef(t *testing.T) { + assert := assert.New(t) + + ms := createTestStore() + ref := "sha1-1111111111111111111111111111111111111111" + + req, _ := http.NewRequest("GET", "/get?ref="+ref, nil) + w := httptest.NewRecorder() + s := server{ms} + s.handle(w, req) + assert.Equal(w.Code, http.StatusNotFound) +} + +func TestGetDataset(t *testing.T) { + assert := assert.New(t) + + ms := createTestStore() + + req, _ := http.NewRequest("GET", "/dataset?id="+datasetID, nil) + w := httptest.NewRecorder() + s := server{ms} + s.handle(w, req) + assert.Equal(w.Code, http.StatusOK) +} + +func TestGetDatasetMissingParam(t *testing.T) { + assert := assert.New(t) + + ms := createTestStore() + + req, _ := http.NewRequest("GET", "/dataset", nil) + w := httptest.NewRecorder() + s := server{ms} + s.handle(w, req) + assert.Equal(w.Code, http.StatusBadRequest) +} + +func TestGetDatasetNotFound(t *testing.T) { + assert := assert.New(t) + + ms := createTestStore() + + req, _ := http.NewRequest("GET", "/dataset?id=notfound", nil) + w := httptest.NewRecorder() + s := server{ms} + s.handle(w, req) + assert.Equal(w.Code, http.StatusNotFound) +}