mirror of
https://github.com/featurebasedb/featurebase.git
synced 2026-10-07 19:37:51 +00:00
Merge pull request #811 from benbjohnson/623-field-http-api
Field HTTP API
This commit is contained in:
commit
f6ce36c02c
4 changed files with 356 additions and 0 deletions
100
frame.go
100
frame.go
|
|
@ -419,6 +419,58 @@ func (f *Frame) Field(name string) *Field {
|
|||
return nil
|
||||
}
|
||||
|
||||
// CreateField creates a new field on the schema.
|
||||
func (f *Frame) CreateField(field *Field) error {
|
||||
f.mu.Lock()
|
||||
defer f.mu.Unlock()
|
||||
|
||||
// Ensure frame supports fields.
|
||||
if f.rangeEnabled {
|
||||
return ErrFrameFieldsNotAllowed
|
||||
}
|
||||
|
||||
// Copy schema and append field.
|
||||
schema := f.schema.Clone()
|
||||
if err := schema.AddField(field); err != nil {
|
||||
return err
|
||||
}
|
||||
f.schema = schema
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// DeleteField deletes an existing field on the schema.
|
||||
func (f *Frame) DeleteField(name string) error {
|
||||
f.mu.Lock()
|
||||
defer f.mu.Unlock()
|
||||
|
||||
// Ensure frame supports fields.
|
||||
if f.rangeEnabled {
|
||||
return ErrFrameFieldsNotAllowed
|
||||
}
|
||||
|
||||
// Copy schema and remove field.
|
||||
schema := f.schema.Clone()
|
||||
if err := schema.DeleteField(name); err != nil {
|
||||
return err
|
||||
}
|
||||
f.schema = schema
|
||||
|
||||
// Remove views.
|
||||
viewName := ViewFieldPrefix + name
|
||||
if view := f.views[viewName]; view != nil {
|
||||
delete(f.views, viewName)
|
||||
|
||||
if err := view.Close(); err != nil {
|
||||
return err
|
||||
} else if err := os.RemoveAll(view.Path()); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// TimeQuantum returns the time quantum for the frame.
|
||||
func (f *Frame) TimeQuantum() TimeQuantum {
|
||||
f.mu.Lock()
|
||||
|
|
@ -849,6 +901,54 @@ type FrameSchema struct {
|
|||
Fields []*Field
|
||||
}
|
||||
|
||||
// Clone returns a copy of s.
|
||||
func (s *FrameSchema) Clone() *FrameSchema {
|
||||
other := &FrameSchema{Fields: make([]*Field, len(s.Fields))}
|
||||
copy(other.Fields, s.Fields)
|
||||
return other
|
||||
}
|
||||
|
||||
// HasField returns true if a field exists on the schema.
|
||||
func (s *FrameSchema) HasField(name string) bool {
|
||||
for _, f := range s.Fields {
|
||||
if f.Name == name {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// AddField adds a single field to the schema.
|
||||
func (s *FrameSchema) AddField(field *Field) error {
|
||||
if err := ValidateField(field); err != nil {
|
||||
return err
|
||||
} else if s.HasField(field.Name) {
|
||||
return ErrFieldExists
|
||||
}
|
||||
|
||||
// Add field to list.
|
||||
s.Fields = append(s.Fields, field)
|
||||
|
||||
// Sort fields by name.
|
||||
sort.Slice(s.Fields, func(i, j int) bool {
|
||||
return s.Fields[i].Name < s.Fields[j].Name
|
||||
})
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// DeleteField removes a single field from the schema.
|
||||
func (s *FrameSchema) DeleteField(name string) error {
|
||||
for i, field := range s.Fields {
|
||||
if field.Name == name {
|
||||
copy(s.Fields[i:], s.Fields[i+1:])
|
||||
s.Fields, s.Fields[len(s.Fields)-1] = s.Fields[:len(s.Fields)-1], nil
|
||||
return nil
|
||||
}
|
||||
}
|
||||
return ErrFieldNotFound
|
||||
}
|
||||
|
||||
func encodeFrameSchema(schema *FrameSchema) *internal.FrameSchema {
|
||||
if schema == nil {
|
||||
return nil
|
||||
|
|
|
|||
76
handler.go
76
handler.go
|
|
@ -117,6 +117,8 @@ func NewRouter(handler *Handler) *mux.Router {
|
|||
router.HandleFunc("/index/{index}/frame/{frame}/attr/diff", handler.handlePostFrameAttrDiff).Methods("POST")
|
||||
router.HandleFunc("/index/{index}/frame/{frame}/restore", handler.handlePostFrameRestore).Methods("POST")
|
||||
router.HandleFunc("/index/{index}/frame/{frame}/time-quantum", handler.handlePatchFrameTimeQuantum).Methods("PATCH")
|
||||
router.HandleFunc("/index/{index}/frame/{frame}/field/{field}", handler.handlePostFrameField).Methods("POST")
|
||||
router.HandleFunc("/index/{index}/frame/{frame}/field/{field}", handler.handleDeleteFrameField).Methods("DELETE")
|
||||
router.HandleFunc("/index/{index}/frame/{frame}/views", handler.handleGetFrameViews).Methods("GET")
|
||||
router.HandleFunc("/index/{index}/frame/{frame}/view/{view}", handler.handleDeleteView).Methods("DELETE")
|
||||
router.HandleFunc("/index/{index}/input/{input-definition}", handler.handlePostInput).Methods("POST")
|
||||
|
|
@ -769,6 +771,80 @@ type patchFrameTimeQuantumRequest struct {
|
|||
|
||||
type patchFrameTimeQuantumResponse struct{}
|
||||
|
||||
// handlePostFrameField handles POST /frame/field request.
|
||||
func (h *Handler) handlePostFrameField(w http.ResponseWriter, r *http.Request) {
|
||||
indexName := mux.Vars(r)["index"]
|
||||
frameName := mux.Vars(r)["frame"]
|
||||
fieldName := mux.Vars(r)["field"]
|
||||
|
||||
// Decode request.
|
||||
var req postFrameFieldRequest
|
||||
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
||||
http.Error(w, err.Error(), http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
// Retrieve frame by name.
|
||||
f := h.Holder.Frame(indexName, frameName)
|
||||
if f == nil {
|
||||
http.Error(w, ErrFrameNotFound.Error(), http.StatusNotFound)
|
||||
return
|
||||
}
|
||||
|
||||
// Create new field.
|
||||
if err := f.CreateField(&Field{
|
||||
Name: fieldName,
|
||||
Type: req.Type,
|
||||
Min: req.Min,
|
||||
Max: req.Max,
|
||||
}); err != nil {
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
// Encode response.
|
||||
if err := json.NewEncoder(w).Encode(postFrameFieldResponse{}); err != nil {
|
||||
h.logger().Printf("response encoding error: %s", err)
|
||||
}
|
||||
}
|
||||
|
||||
type postFrameFieldRequest struct {
|
||||
Type string `json:"type,omitempty"`
|
||||
Min int64 `json:"min,omitempty"`
|
||||
Max int64 `json:"max,omitempty"`
|
||||
}
|
||||
|
||||
type postFrameFieldResponse struct{}
|
||||
|
||||
// handleDeleteFrameField handles DELETE /frame/field request.
|
||||
func (h *Handler) handleDeleteFrameField(w http.ResponseWriter, r *http.Request) {
|
||||
indexName := mux.Vars(r)["index"]
|
||||
frameName := mux.Vars(r)["frame"]
|
||||
fieldName := mux.Vars(r)["field"]
|
||||
|
||||
// Retrieve frame by name.
|
||||
f := h.Holder.Frame(indexName, frameName)
|
||||
if f == nil {
|
||||
http.Error(w, ErrFrameNotFound.Error(), http.StatusNotFound)
|
||||
return
|
||||
}
|
||||
|
||||
// Delete field.
|
||||
if err := f.DeleteField(fieldName); err != nil {
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
// Encode response.
|
||||
if err := json.NewEncoder(w).Encode(deleteFrameFieldResponse{}); err != nil {
|
||||
h.logger().Printf("response encoding error: %s", err)
|
||||
}
|
||||
}
|
||||
|
||||
type deleteFrameFieldRequest struct{}
|
||||
|
||||
type deleteFrameFieldResponse struct{}
|
||||
|
||||
// handleGetFrameViews handles GET /frame/views request.
|
||||
func (h *Handler) handleGetFrameViews(w http.ResponseWriter, r *http.Request) {
|
||||
indexName := mux.Vars(r)["index"]
|
||||
|
|
|
|||
179
handler_test.go
179
handler_test.go
|
|
@ -19,6 +19,7 @@ import (
|
|||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
|
|
@ -859,6 +860,176 @@ func TestHandler_Frame_AttrStore_Diff(t *testing.T) {
|
|||
}
|
||||
}
|
||||
|
||||
// Ensure the handler can create a new field on an existing frame.
|
||||
func TestHandler_Frame_AddField(t *testing.T) {
|
||||
hldr := test.MustOpenHolder()
|
||||
defer hldr.Close()
|
||||
|
||||
s := test.NewServer()
|
||||
s.Handler.Holder = hldr.Holder
|
||||
defer s.Close()
|
||||
|
||||
t.Run("OK", func(t *testing.T) {
|
||||
idx := hldr.MustCreateIndexIfNotExists("i", pilosa.IndexOptions{})
|
||||
f, err := idx.CreateFrameIfNotExists("f", pilosa.FrameOptions{RangeEnabled: true})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
resp, err := http.Post(
|
||||
s.URL+"/index/i/frame/f/field/x",
|
||||
"application/json",
|
||||
strings.NewReader(`{"type":"int","min":100,"max":200}`),
|
||||
)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
} else if err := resp.Body.Close(); err != nil {
|
||||
t.Fatal(err)
|
||||
} else if resp.StatusCode != http.StatusOK {
|
||||
t.Fatalf("unexpected status code: %d", resp.StatusCode)
|
||||
}
|
||||
|
||||
if field := f.Field("x"); !reflect.DeepEqual(field, &pilosa.Field{Name: "x", Type: "int", Min: 100, Max: 200}) {
|
||||
t.Fatalf("unexpected field: %#v", field)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("ErrInvalidFieldType", func(t *testing.T) {
|
||||
idx := hldr.MustCreateIndexIfNotExists("i", pilosa.IndexOptions{})
|
||||
if _, err := idx.CreateFrameIfNotExists("f", pilosa.FrameOptions{RangeEnabled: true}); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
resp, err := http.Post(
|
||||
s.URL+"/index/i/frame/f/field/x",
|
||||
"application/json",
|
||||
strings.NewReader(`{"type":"bad_type","min":100,"max":200}`),
|
||||
)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
} else if body := MustReadAll(resp.Body); string(body) != `invalid field type`+"\n" {
|
||||
t.Fatalf("unexpected body: %q", body)
|
||||
} else if err := resp.Body.Close(); err != nil {
|
||||
t.Fatal(err)
|
||||
} else if resp.StatusCode != http.StatusInternalServerError {
|
||||
t.Fatalf("unexpected status code: %d", resp.StatusCode)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("ErrInvalidFieldRange", func(t *testing.T) {
|
||||
idx := hldr.MustCreateIndexIfNotExists("i", pilosa.IndexOptions{})
|
||||
if _, err := idx.CreateFrameIfNotExists("f", pilosa.FrameOptions{RangeEnabled: true}); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
resp, err := http.Post(
|
||||
s.URL+"/index/i/frame/f/field/x",
|
||||
"application/json",
|
||||
strings.NewReader(`{"type":"int","min":200,"max":100}`),
|
||||
)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
} else if body := MustReadAll(resp.Body); string(body) != `invalid field range`+"\n" {
|
||||
t.Fatalf("unexpected body: %q", body)
|
||||
} else if err := resp.Body.Close(); err != nil {
|
||||
t.Fatal(err)
|
||||
} else if resp.StatusCode != http.StatusInternalServerError {
|
||||
t.Fatalf("unexpected status code: %d", resp.StatusCode)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("ErrFieldAlreadyExists", func(t *testing.T) {
|
||||
idx := hldr.MustCreateIndexIfNotExists("i", pilosa.IndexOptions{})
|
||||
if _, err := idx.CreateFrameIfNotExists("f", pilosa.FrameOptions{
|
||||
RangeEnabled: true,
|
||||
Fields: []*pilosa.Field{{Name: "x", Type: pilosa.FieldTypeInt, Min: 0, Max: 100}},
|
||||
}); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
resp, err := http.Post(
|
||||
s.URL+"/index/i/frame/f/field/x",
|
||||
"application/json",
|
||||
strings.NewReader(`{"type":"int","min":0,"max":100}`),
|
||||
)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
} else if body := MustReadAll(resp.Body); string(body) != `field already exists`+"\n" {
|
||||
t.Fatalf("unexpected body: %q", body)
|
||||
} else if err := resp.Body.Close(); err != nil {
|
||||
t.Fatal(err)
|
||||
} else if resp.StatusCode != http.StatusInternalServerError {
|
||||
t.Fatalf("unexpected status code: %d", resp.StatusCode)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// Ensure the handler can delete existing fields.
|
||||
func TestHandler_Frame_DeleteField(t *testing.T) {
|
||||
hldr := test.MustOpenHolder()
|
||||
defer hldr.Close()
|
||||
|
||||
s := test.NewServer()
|
||||
s.Handler.Holder = hldr.Holder
|
||||
defer s.Close()
|
||||
|
||||
t.Run("OK", func(t *testing.T) {
|
||||
idx := hldr.MustCreateIndexIfNotExists("i", pilosa.IndexOptions{})
|
||||
f, err := idx.CreateFrameIfNotExists("f", pilosa.FrameOptions{RangeEnabled: true})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
} else if err := f.CreateField(&pilosa.Field{Name: "x", Type: pilosa.FieldTypeInt, Min: 0, Max: 100}); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
req, err := http.NewRequest("DELETE", s.URL+"/index/i/frame/f/field/x", nil)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
resp, err := http.DefaultClient.Do(req)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
} else if err := resp.Body.Close(); err != nil {
|
||||
t.Fatal(err)
|
||||
} else if resp.StatusCode != http.StatusOK {
|
||||
t.Fatalf("unexpected status code: %d", resp.StatusCode)
|
||||
}
|
||||
|
||||
if field := f.Field("x"); field != nil {
|
||||
t.Fatalf("expected nil field, got: %#v", field)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("ErrFieldNotFound", func(t *testing.T) {
|
||||
idx := hldr.MustCreateIndexIfNotExists("i", pilosa.IndexOptions{})
|
||||
f, err := idx.CreateFrameIfNotExists("f", pilosa.FrameOptions{RangeEnabled: true})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
} else if err := f.CreateField(&pilosa.Field{Name: "x", Type: pilosa.FieldTypeInt, Min: 0, Max: 100}); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
req, err := http.NewRequest("DELETE", s.URL+"/index/i/frame/f/field/y", nil)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
resp, err := http.DefaultClient.Do(req)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
} else if body, err := ioutil.ReadAll(resp.Body); err != nil {
|
||||
t.Fatal(err)
|
||||
} else if strings.TrimSpace(string(body)) != `field not found` {
|
||||
t.Fatalf("unexpected body: %q", body)
|
||||
} else if err := resp.Body.Close(); err != nil {
|
||||
t.Fatal(err)
|
||||
} else if resp.StatusCode != http.StatusInternalServerError {
|
||||
t.Fatalf("unexpected status code: %d", resp.StatusCode)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// Ensure the handler can backup a fragment and then restore it.
|
||||
func TestHandler_Fragment_BackupRestore(t *testing.T) {
|
||||
hldr := test.MustOpenHolder()
|
||||
|
|
@ -1533,3 +1704,11 @@ func TestHandler_DeleteView(t *testing.T) {
|
|||
t.Fatal("expected nil view")
|
||||
}
|
||||
}
|
||||
|
||||
func MustReadAll(r io.Reader) []byte {
|
||||
buf, err := ioutil.ReadAll(r)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
return buf
|
||||
}
|
||||
|
|
|
|||
|
|
@ -49,6 +49,7 @@ var (
|
|||
ErrInputDefinitionNotFound = errors.New("input-definition not found")
|
||||
|
||||
ErrFieldNotFound = errors.New("field not found")
|
||||
ErrFieldExists = errors.New("field already exists")
|
||||
ErrFieldNameRequired = errors.New("field name required")
|
||||
ErrInvalidFieldType = errors.New("invalid field type")
|
||||
ErrInvalidFieldRange = errors.New("invalid field range")
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue