From 830a8805c9e06920da0f021a77a66bd25bae94d5 Mon Sep 17 00:00:00 2001 From: Matthew Dillon <mrdillon@alaska.edu> Date: Wed, 15 Oct 2014 13:01:11 -0800 Subject: [PATCH 1/9] Species Read - Order of ops: router/routes.go router/api.go models/species_test.go models/species.go models/client.go datastore/migrations/addspecies.sql datastore/migrations/dropspecies.sql datastore/species_test.go datastore/species.go datastore/datastore.go api/species_test.go api/species.go api/handler.go --- api/handler.go | 2 + api/species.go | 22 ++++++ api/species_test.go | 34 +++++++++ datastore/datastore.go | 13 ++-- .../migrations/00003_AddSpecies_down.sql | 5 ++ datastore/migrations/00003_AddSpecies_up.sql | 15 ++++ datastore/species.go | 22 ++++++ datastore/species_test.go | 37 ++++++++++ models/client.go | 6 +- models/species.go | 69 +++++++++++++++++++ models/species_test.go | 39 +++++++++++ router/api.go | 4 ++ router/routes.go | 2 + 13 files changed, 263 insertions(+), 7 deletions(-) create mode 100644 api/species.go create mode 100644 api/species_test.go create mode 100644 datastore/migrations/00003_AddSpecies_down.sql create mode 100644 datastore/migrations/00003_AddSpecies_up.sql create mode 100644 datastore/species.go create mode 100644 datastore/species_test.go create mode 100644 models/species.go create mode 100644 models/species_test.go diff --git a/api/handler.go b/api/handler.go index a01506b..384740a 100644 --- a/api/handler.go +++ b/api/handler.go @@ -29,6 +29,8 @@ func Handler() *mux.Router { m.Get(router.UpdateGenus).Handler(handler(serveUpdateGenus)) m.Get(router.DeleteGenus).Handler(handler(serveDeleteGenus)) + m.Get(router.Species).Handler(handler(serveSpecies)) + return m } diff --git a/api/species.go b/api/species.go new file mode 100644 index 0000000..c8e127c --- /dev/null +++ b/api/species.go @@ -0,0 +1,22 @@ +package api + +import ( + "net/http" + "strconv" + + "github.com/gorilla/mux" +) + +func serveSpecies(w http.ResponseWriter, r *http.Request) error { + id, err := strconv.ParseInt(mux.Vars(r)["Id"], 10, 0) + if err != nil { + return err + } + + species, err := store.Species.Get(id) + if err != nil { + return err + } + + return writeJSON(w, species) +} diff --git a/api/species_test.go b/api/species_test.go new file mode 100644 index 0000000..5d1bf87 --- /dev/null +++ b/api/species_test.go @@ -0,0 +1,34 @@ +package api + +import ( + "testing" + + "github.com/thermokarst/bactdb/models" +) + +func TestSpecies_Get(t *testing.T) { + setup() + + want := &models.Species{Id: 1, GenusId: 1, SpeciesName: "Test Species"} + + calledGet := false + store.Species.(*models.MockSpeciesService).Get_ = func(id int64) (*models.Species, error) { + if id != want.Id { + t.Errorf("wanted request for species %d but got %d", want.Id, id) + } + calledGet = true + return want, nil + } + + _, err := apiClient.Species.Get(want.Id) + if err != nil { + t.Fatal(err) + } + + // if !calledGet { + // t.Error("!calledGet") + // } + // if !normalizeDeepEqual(want, got) { + // t.Errorf("got species %+v but wanted species %+v", got, want) + // } +} diff --git a/datastore/datastore.go b/datastore/datastore.go index 43faafd..1919e3a 100644 --- a/datastore/datastore.go +++ b/datastore/datastore.go @@ -9,9 +9,10 @@ import ( // A datastore access point (in PostgreSQL) type Datastore struct { - Users models.UsersService - Genera models.GeneraService - dbh modl.SqlExecutor + Users models.UsersService + Genera models.GeneraService + Species models.SpeciesService + dbh modl.SqlExecutor } var ( @@ -29,12 +30,14 @@ func NewDatastore(dbh modl.SqlExecutor) *Datastore { d := &Datastore{dbh: dbh} d.Users = &usersStore{d} d.Genera = &generaStore{d} + d.Species = &speciesStore{d} return d } func NewMockDatastore() *Datastore { return &Datastore{ - Users: &models.MockUsersService{}, - Genera: &models.MockGeneraService{}, + Users: &models.MockUsersService{}, + Genera: &models.MockGeneraService{}, + Species: &models.MockSpeciesService{}, } } diff --git a/datastore/migrations/00003_AddSpecies_down.sql b/datastore/migrations/00003_AddSpecies_down.sql new file mode 100644 index 0000000..3ce7db5 --- /dev/null +++ b/datastore/migrations/00003_AddSpecies_down.sql @@ -0,0 +1,5 @@ +-- bactdb +-- Matthew R Dillon + +DROP TABLE species; + diff --git a/datastore/migrations/00003_AddSpecies_up.sql b/datastore/migrations/00003_AddSpecies_up.sql new file mode 100644 index 0000000..d290de1 --- /dev/null +++ b/datastore/migrations/00003_AddSpecies_up.sql @@ -0,0 +1,15 @@ +-- bactdb +-- Matthew Dillon + +CREATE TABLE species ( + id BIGSERIAL NOT NULL, + genusid BIGINT, + speciesname CHARACTER VARYING(100), + + createdat TIMESTAMP WITH TIME ZONE, + updatedat TIMESTAMP WITH TIME ZONE, + deletedat TIMESTAMP WITH TIME ZONE, + + CONSTRAINT species_pkey PRIMARY KEY (id), + FOREIGN KEY (genusid) REFERENCES genera(id) +); diff --git a/datastore/species.go b/datastore/species.go new file mode 100644 index 0000000..a7f3c17 --- /dev/null +++ b/datastore/species.go @@ -0,0 +1,22 @@ +package datastore + +import "github.com/thermokarst/bactdb/models" + +func init() { + DB.AddTableWithName(models.Species{}, "species").SetKeys(true, "Id") +} + +type speciesStore struct { + *Datastore +} + +func (s *speciesStore) Get(id int64) (*models.Species, error) { + var species []*models.Species + if err := s.dbh.Select(&species, `SELECT * FROM species WHERE id=$1;`, id); err != nil { + return nil, err + } + if len(species) == 0 { + return nil, models.ErrSpeciesNotFound + } + return species[0], nil +} diff --git a/datastore/species_test.go b/datastore/species_test.go new file mode 100644 index 0000000..915d522 --- /dev/null +++ b/datastore/species_test.go @@ -0,0 +1,37 @@ +package datastore + +import ( + "reflect" + "testing" + + "github.com/thermokarst/bactdb/models" +) + +func TestSpeciesStore_Get_db(t *testing.T) { + tx, _ := DB.Begin() + defer tx.Rollback() + + // Test on a clean database + tx.Exec(`DELETE FROM species;`) + + wantGenus := &models.Genus{GenusName: "Test Genus"} + if err := tx.Insert(wantGenus); err != nil { + t.Fatal(err) + } + + want := &models.Species{Id: 1, GenusId: wantGenus.Id, SpeciesName: "Test Species"} + if err := tx.Insert(want); err != nil { + t.Fatal(err) + } + + d := NewDatastore(tx) + species, err := d.Species.Get(1) + if err != nil { + t.Fatal(err) + } + + normalizeTime(&want.CreatedAt, &want.UpdatedAt, &want.DeletedAt) + if !reflect.DeepEqual(species, want) { + t.Errorf("got species %+v, want %+v", species, want) + } +} diff --git a/models/client.go b/models/client.go index bb321e7..b94f3f1 100644 --- a/models/client.go +++ b/models/client.go @@ -16,8 +16,9 @@ import ( // A Client communicates with bactdb's HTTP API. type Client struct { - Users UsersService - Genera GeneraService + Users UsersService + Genera GeneraService + Species SpeciesService // BaseURL for HTTP requests to bactdb's API. BaseURL *url.URL @@ -47,6 +48,7 @@ func NewClient(httpClient *http.Client) *Client { } c.Users = &usersService{c} c.Genera = &generaService{c} + c.Species = &speciesService{c} return c } diff --git a/models/species.go b/models/species.go new file mode 100644 index 0000000..336c474 --- /dev/null +++ b/models/species.go @@ -0,0 +1,69 @@ +package models + +import ( + "errors" + "strconv" + "time" + + "github.com/thermokarst/bactdb/router" +) + +// A Species is a high-level classifier in bactdb. +type Species struct { + Id int64 `json:"id,omitempty"` + GenusId int64 `json:"genus_id"` + SpeciesName string `json:"species_name"` + CreatedAt time.Time `json:"created_at"` + UpdatedAt time.Time `json:"updated_at"` + DeletedAt time.Time `json:"deleted_at"` +} + +// SpeciesService interacts with the species-related endpoints in bactdb's API. +type SpeciesService interface { + // Get a species + Get(id int64) (*Species, error) +} + +var ( + ErrSpeciesNotFound = errors.New("species not found") +) + +type speciesService struct { + client *Client +} + +func (s *speciesService) Get(id int64) (*Species, error) { + // Pass in key value pairs as strings, sp that the gorilla mux URL generation is happy + strId := strconv.FormatInt(id, 10) + + url, err := s.client.url(router.Species, map[string]string{"Id": strId}, nil) + if err != nil { + return nil, err + } + + req, err := s.client.NewRequest("GET", url.String(), nil) + if err != nil { + return nil, err + } + + var species *Species + _, err = s.client.Do(req, &species) + if err != nil { + return nil, err + } + + return species, nil +} + +type MockSpeciesService struct { + Get_ func(id int64) (*Species, error) +} + +var _ SpeciesService = &MockSpeciesService{} + +func (s *MockSpeciesService) Get(id int64) (*Species, error) { + if s.Get_ == nil { + return nil, nil + } + return s.Get_(id) +} diff --git a/models/species_test.go b/models/species_test.go new file mode 100644 index 0000000..e080179 --- /dev/null +++ b/models/species_test.go @@ -0,0 +1,39 @@ +package models + +import ( + "net/http" + "reflect" + "testing" + + "github.com/thermokarst/bactdb/router" +) + +func TestSpeciesService_Get(t *testing.T) { + setup() + defer teardown() + + want := &Species{Id: 1, GenusId: 1, SpeciesName: "Test Species"} + + var called bool + mux.HandleFunc(urlPath(t, router.Species, map[string]string{"Id": "1"}), func(w http.ResponseWriter, r *http.Request) { + called = true + testMethod(t, r, "GET") + + writeJSON(w, want) + }) + + species, err := client.Species.Get(1) + if err != nil { + t.Errorf("Species.Get returned error: %v", err) + } + + if !called { + t.Fatal("!called") + } + + normalizeTime(&want.CreatedAt, &want.UpdatedAt, &want.DeletedAt) + + if !reflect.DeepEqual(species, want) { + t.Errorf("Species.Get returned %+v, want %+v", species, want) + } +} diff --git a/router/api.go b/router/api.go index 1df34ee..229bddc 100644 --- a/router/api.go +++ b/router/api.go @@ -16,5 +16,9 @@ func API() *mux.Router { m.Path("/genera/{Id:.+}").Methods("GET").Name(Genus) m.Path("/genera/{Id:.+}").Methods("PUT").Name(UpdateGenus) m.Path("/genera/{Id:.+}").Methods("DELETE").Name(DeleteGenus) + + // Species + m.Path("/species/{Id:.+}").Methods("GET").Name(Species) + return m } diff --git a/router/routes.go b/router/routes.go index e3bb7bb..9c4ed37 100644 --- a/router/routes.go +++ b/router/routes.go @@ -10,4 +10,6 @@ const ( Genera = "genera" UpdateGenus = "genus:update" DeleteGenus = "genus:delete" + + Species = "species" ) From ed2ba266544498e39cd0fb241b1e555cfaf8e8a2 Mon Sep 17 00:00:00 2001 From: Matthew Dillon <mrdillon@alaska.edu> Date: Wed, 15 Oct 2014 14:11:17 -0800 Subject: [PATCH 2/9] Removed commented portion --- api/species_test.go | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/api/species_test.go b/api/species_test.go index 5d1bf87..407a952 100644 --- a/api/species_test.go +++ b/api/species_test.go @@ -20,15 +20,15 @@ func TestSpecies_Get(t *testing.T) { return want, nil } - _, err := apiClient.Species.Get(want.Id) + got, err := apiClient.Species.Get(want.Id) if err != nil { t.Fatal(err) } - // if !calledGet { - // t.Error("!calledGet") - // } - // if !normalizeDeepEqual(want, got) { - // t.Errorf("got species %+v but wanted species %+v", got, want) - // } + if !calledGet { + t.Error("!calledGet") + } + if !normalizeDeepEqual(want, got) { + t.Errorf("got species %+v but wanted species %+v", got, want) + } } From 7fe5566edfd22068c1b0394c130f8e4622080cd2 Mon Sep 17 00:00:00 2001 From: Matthew Dillon <mrdillon@alaska.edu> Date: Wed, 15 Oct 2014 16:43:09 -0800 Subject: [PATCH 3/9] Create species, cleanup schema. - create species - species genus_id not null --- api/handler.go | 1 + api/species.go | 20 +++++++++++ api/species_test.go | 27 +++++++++++++++ datastore/migrations/00003_AddSpecies_up.sql | 2 +- datastore/species.go | 7 ++++ datastore/species_test.go | 28 +++++++++++++++ models/species.go | 33 +++++++++++++++++- models/species_test.go | 36 ++++++++++++++++++++ router/api.go | 1 + router/routes.go | 3 +- 10 files changed, 155 insertions(+), 3 deletions(-) diff --git a/api/handler.go b/api/handler.go index 384740a..65bbc79 100644 --- a/api/handler.go +++ b/api/handler.go @@ -30,6 +30,7 @@ func Handler() *mux.Router { m.Get(router.DeleteGenus).Handler(handler(serveDeleteGenus)) m.Get(router.Species).Handler(handler(serveSpecies)) + m.Get(router.CreateSpecies).Handler(handler(serveCreateSpecies)) return m } diff --git a/api/species.go b/api/species.go index c8e127c..b8bae18 100644 --- a/api/species.go +++ b/api/species.go @@ -1,10 +1,12 @@ package api import ( + "encoding/json" "net/http" "strconv" "github.com/gorilla/mux" + "github.com/thermokarst/bactdb/models" ) func serveSpecies(w http.ResponseWriter, r *http.Request) error { @@ -20,3 +22,21 @@ func serveSpecies(w http.ResponseWriter, r *http.Request) error { return writeJSON(w, species) } + +func serveCreateSpecies(w http.ResponseWriter, r *http.Request) error { + var species models.Species + err := json.NewDecoder(r.Body).Decode(&species) + if err != nil { + return err + } + + created, err := store.Species.Create(&species) + if err != nil { + return err + } + if created { + w.WriteHeader(http.StatusCreated) + } + + return writeJSON(w, species) +} diff --git a/api/species_test.go b/api/species_test.go index 407a952..24fd557 100644 --- a/api/species_test.go +++ b/api/species_test.go @@ -32,3 +32,30 @@ func TestSpecies_Get(t *testing.T) { t.Errorf("got species %+v but wanted species %+v", got, want) } } + +func TestSpecies_Create(t *testing.T) { + setup() + + want := &models.Species{Id: 1, GenusId: 1, SpeciesName: "Test Species"} + + calledPost := false + store.Species.(*models.MockSpeciesService).Create_ = func(species *models.Species) (bool, error) { + if !normalizeDeepEqual(want, species) { + t.Errorf("wanted request for species %d but got %d", want, species) + } + calledPost = true + return true, nil + } + + success, err := apiClient.Species.Create(want) + if err != nil { + t.Fatal(err) + } + + if !calledPost { + t.Error("!calledPost") + } + if !success { + t.Error("!success") + } +} diff --git a/datastore/migrations/00003_AddSpecies_up.sql b/datastore/migrations/00003_AddSpecies_up.sql index d290de1..aff073b 100644 --- a/datastore/migrations/00003_AddSpecies_up.sql +++ b/datastore/migrations/00003_AddSpecies_up.sql @@ -3,7 +3,7 @@ CREATE TABLE species ( id BIGSERIAL NOT NULL, - genusid BIGINT, + genusid BIGINT NOT NULL, speciesname CHARACTER VARYING(100), createdat TIMESTAMP WITH TIME ZONE, diff --git a/datastore/species.go b/datastore/species.go index a7f3c17..f7e6dee 100644 --- a/datastore/species.go +++ b/datastore/species.go @@ -20,3 +20,10 @@ func (s *speciesStore) Get(id int64) (*models.Species, error) { } return species[0], nil } + +func (s *speciesStore) Create(species *models.Species) (bool, error) { + if err := s.dbh.Insert(species); err != nil { + return false, err + } + return true, nil +} diff --git a/datastore/species_test.go b/datastore/species_test.go index 915d522..db81db6 100644 --- a/datastore/species_test.go +++ b/datastore/species_test.go @@ -35,3 +35,31 @@ func TestSpeciesStore_Get_db(t *testing.T) { t.Errorf("got species %+v, want %+v", species, want) } } + +func TestSpeciesStore_Create_db(t *testing.T) { + tx, _ := DB.Begin() + defer tx.Rollback() + + // Test on a clean database + tx.Exec(`DELETE FROM species;`) + + genus := &models.Genus{} + if err := tx.Insert(genus); err != nil { + t.Fatal(err) + } + + species := &models.Species{Id: 1, GenusId: genus.Id, SpeciesName: "Test Species"} + + d := NewDatastore(tx) + created, err := d.Species.Create(species) + if err != nil { + t.Fatal(err) + } + + if !created { + t.Error("!created") + } + if species.Id == 0 { + t.Error("want nonzero species.Id after submitting") + } +} diff --git a/models/species.go b/models/species.go index 336c474..3437d8e 100644 --- a/models/species.go +++ b/models/species.go @@ -2,6 +2,7 @@ package models import ( "errors" + "net/http" "strconv" "time" @@ -22,6 +23,9 @@ type Species struct { type SpeciesService interface { // Get a species Get(id int64) (*Species, error) + + // Create a species record + Create(species *Species) (bool, error) } var ( @@ -55,8 +59,28 @@ func (s *speciesService) Get(id int64) (*Species, error) { return species, nil } +func (s *speciesService) Create(species *Species) (bool, error) { + url, err := s.client.url(router.CreateSpecies, nil, nil) + if err != nil { + return false, err + } + + req, err := s.client.NewRequest("POST", url.String(), species) + if err != nil { + return false, err + } + + resp, err := s.client.Do(req, &species) + if err != nil { + return false, err + } + + return resp.StatusCode == http.StatusCreated, nil +} + type MockSpeciesService struct { - Get_ func(id int64) (*Species, error) + Get_ func(id int64) (*Species, error) + Create_ func(species *Species) (bool, error) } var _ SpeciesService = &MockSpeciesService{} @@ -67,3 +91,10 @@ func (s *MockSpeciesService) Get(id int64) (*Species, error) { } return s.Get_(id) } + +func (s *MockSpeciesService) Create(species *Species) (bool, error) { + if s.Create_ == nil { + return false, nil + } + return s.Create_(species) +} diff --git a/models/species_test.go b/models/species_test.go index e080179..338d13b 100644 --- a/models/species_test.go +++ b/models/species_test.go @@ -37,3 +37,39 @@ func TestSpeciesService_Get(t *testing.T) { t.Errorf("Species.Get returned %+v, want %+v", species, want) } } + +func TestSpeciesService_Create(t *testing.T) { + setup() + defer teardown() + + want := &Species{Id: 1, GenusId: 1, SpeciesName: "Test Species"} + + var called bool + mux.HandleFunc(urlPath(t, router.CreateSpecies, nil), func(w http.ResponseWriter, r *http.Request) { + called = true + testMethod(t, r, "POST") + testBody(t, r, `{"id":1,"genus_id":1,"species_name":"Test Species","created_at":"0001-01-01T00:00:00Z","updated_at":"0001-01-01T00:00:00Z","deleted_at":"0001-01-01T00:00:00Z"}`+"\n") + + w.WriteHeader(http.StatusCreated) + writeJSON(w, want) + }) + + species := &Species{Id: 1, GenusId: 1, SpeciesName: "Test Species"} + created, err := client.Species.Create(species) + if err != nil { + t.Errorf("Species.Create returned error: %v", err) + } + + if !created { + t.Error("!created") + } + + if !called { + t.Fatal("!called") + } + + normalizeTime(&want.CreatedAt, &want.UpdatedAt, &want.DeletedAt) + if !reflect.DeepEqual(species, want) { + t.Errorf("Species.Create returned %+v, want %+v", species, want) + } +} diff --git a/router/api.go b/router/api.go index 229bddc..8ae8b02 100644 --- a/router/api.go +++ b/router/api.go @@ -18,6 +18,7 @@ func API() *mux.Router { m.Path("/genera/{Id:.+}").Methods("DELETE").Name(DeleteGenus) // Species + m.Path("/species").Methods("POST").Name(CreateSpecies) m.Path("/species/{Id:.+}").Methods("GET").Name(Species) return m diff --git a/router/routes.go b/router/routes.go index 9c4ed37..8b2bd70 100644 --- a/router/routes.go +++ b/router/routes.go @@ -11,5 +11,6 @@ const ( UpdateGenus = "genus:update" DeleteGenus = "genus:delete" - Species = "species" + Species = "species" + CreateSpecies = "species:create" ) From 7e74d672ba8f441424a414314b7a1612ba9dbc3b Mon Sep 17 00:00:00 2001 From: Matthew Dillon <mrdillon@alaska.edu> Date: Thu, 16 Oct 2014 09:20:25 -0800 Subject: [PATCH 4/9] Using struct attrs to match desired db schema --- datastore/migrations/00001_AddUsers_up.sql | 6 +++--- datastore/migrations/00002_AddGenera_up.sql | 10 +++++----- datastore/migrations/00003_AddSpecies_up.sql | 12 ++++++------ models/genera.go | 8 ++++---- models/species.go | 10 +++++----- models/users.go | 8 ++++---- 6 files changed, 27 insertions(+), 27 deletions(-) diff --git a/datastore/migrations/00001_AddUsers_up.sql b/datastore/migrations/00001_AddUsers_up.sql index 9dd8907..4ef446a 100644 --- a/datastore/migrations/00001_AddUsers_up.sql +++ b/datastore/migrations/00001_AddUsers_up.sql @@ -5,9 +5,9 @@ CREATE TABLE users ( id BIGSERIAL NOT NULL, username CHARACTER VARYING(100), - createdat TIMESTAMP WITH TIME ZONE, - updatedat TIMESTAMP WITH TIME ZONE, - deletedat TIMESTAMP WITH TIME ZONE, + created_at TIMESTAMP WITH TIME ZONE, + updated_at TIMESTAMP WITH TIME ZONE, + deleted_at TIMESTAMP WITH TIME ZONE, CONSTRAINT users_pkey PRIMARY KEY (id) ); diff --git a/datastore/migrations/00002_AddGenera_up.sql b/datastore/migrations/00002_AddGenera_up.sql index 3fa4d79..e9ea156 100644 --- a/datastore/migrations/00002_AddGenera_up.sql +++ b/datastore/migrations/00002_AddGenera_up.sql @@ -3,11 +3,11 @@ CREATE TABLE genera ( id BIGSERIAL NOT NULL, - genusname CHARACTER VARYING(100), + genus_name CHARACTER VARYING(100), - createdat TIMESTAMP WITH TIME ZONE, - updatedat TIMESTAMP WITH TIME ZONE, - deletedat TIMESTAMP WITH TIME ZONE, + created_at TIMESTAMP WITH TIME ZONE, + updated_at TIMESTAMP WITH TIME ZONE, + deleted_at TIMESTAMP WITH TIME ZONE, CONSTRAINT genus_pkey PRIMARY KEY (id) ); @@ -15,5 +15,5 @@ CREATE TABLE genera ( CREATE UNIQUE INDEX genusname_idx ON genera USING btree - (genusname COLLATE pg_catalog."default"); + (genus_name COLLATE pg_catalog."default"); diff --git a/datastore/migrations/00003_AddSpecies_up.sql b/datastore/migrations/00003_AddSpecies_up.sql index aff073b..88a192a 100644 --- a/datastore/migrations/00003_AddSpecies_up.sql +++ b/datastore/migrations/00003_AddSpecies_up.sql @@ -3,13 +3,13 @@ CREATE TABLE species ( id BIGSERIAL NOT NULL, - genusid BIGINT NOT NULL, - speciesname CHARACTER VARYING(100), + genus_id BIGINT NOT NULL, + species_name CHARACTER VARYING(100), - createdat TIMESTAMP WITH TIME ZONE, - updatedat TIMESTAMP WITH TIME ZONE, - deletedat TIMESTAMP WITH TIME ZONE, + created_at TIMESTAMP WITH TIME ZONE, + updated_at TIMESTAMP WITH TIME ZONE, + deleted_at TIMESTAMP WITH TIME ZONE, CONSTRAINT species_pkey PRIMARY KEY (id), - FOREIGN KEY (genusid) REFERENCES genera(id) + FOREIGN KEY (genus_id) REFERENCES genera(id) ); diff --git a/models/genera.go b/models/genera.go index fb87694..60cc2b0 100644 --- a/models/genera.go +++ b/models/genera.go @@ -12,10 +12,10 @@ import ( // A Genus is a high-level classifier in bactdb. type Genus struct { Id int64 `json:"id,omitempty"` - GenusName string `json:"genus_name"` - CreatedAt time.Time `json:"created_at"` - UpdatedAt time.Time `json:"updated_at"` - DeletedAt time.Time `json:"deleted_at"` + GenusName string `db:"genus_name" json:"genus_name"` + CreatedAt time.Time `db:"created_at" json:"created_at"` + UpdatedAt time.Time `db:"updated_at" json:"updated_at"` + DeletedAt time.Time `db:"deleted_at" json:"deleted_at"` } // GeneraService interacts with the genus-related endpoints in bactdb's API. diff --git a/models/species.go b/models/species.go index 3437d8e..dbd696d 100644 --- a/models/species.go +++ b/models/species.go @@ -12,11 +12,11 @@ import ( // A Species is a high-level classifier in bactdb. type Species struct { Id int64 `json:"id,omitempty"` - GenusId int64 `json:"genus_id"` - SpeciesName string `json:"species_name"` - CreatedAt time.Time `json:"created_at"` - UpdatedAt time.Time `json:"updated_at"` - DeletedAt time.Time `json:"deleted_at"` + GenusId int64 `db:"genus_id" json:"genus_id"` + SpeciesName string `db:"species_name" json:"species_name"` + CreatedAt time.Time `db:"created_at" json:"created_at"` + UpdatedAt time.Time `db:"updated_at" json:"updated_at"` + DeletedAt time.Time `db:"deleted_at" json:"deleted_at"` } // SpeciesService interacts with the species-related endpoints in bactdb's API. diff --git a/models/users.go b/models/users.go index e398295..d7a526d 100644 --- a/models/users.go +++ b/models/users.go @@ -12,10 +12,10 @@ import ( // A User is a person that has administrative access to bactdb. type User struct { Id int64 `json:"id,omitempty"` - UserName string `sql:"size:100" json:"user_name"` - CreatedAt time.Time `json:"created_at"` - UpdatedAt time.Time `json:"updated_at"` - DeletedAt time.Time `json:"deleted_at"` + UserName string `json:"user_name"` + CreatedAt time.Time `db:"created_at" json:"created_at"` + UpdatedAt time.Time `db:"updated_at" json:"updated_at"` + DeletedAt time.Time `db:"deleted_at" json:"deleted_at"` } // UsersService interacts with the user-related endpoints in bactdb's API. From b045ded9cd97b1b47b868b2047a695c2c443fc78 Mon Sep 17 00:00:00 2001 From: Matthew Dillon <mrdillon@alaska.edu> Date: Thu, 16 Oct 2014 09:41:01 -0800 Subject: [PATCH 5/9] Importing base schema --- datastore/migrations/00004_AddStrain_down.sql | 5 +++++ datastore/migrations/00004_AddStrain_up.sql | 20 +++++++++++++++++++ .../00005_AddObservationTypes_down.sql | 5 +++++ .../00005_AddObservationTypes_up.sql | 14 +++++++++++++ .../migrations/00006_AddObservations_down.sql | 5 +++++ .../migrations/00006_AddObservations_up.sql | 16 +++++++++++++++ .../00007_AddStrainsObservations_down.sql | 5 +++++ .../00007_AddStrainsObservations_up.sql | 17 ++++++++++++++++ .../00008_AddText_Measurements_down.sql | 5 +++++ .../00008_AddText_Measurements_up.sql | 14 +++++++++++++ .../migrations/00009_AddUnit_Types_down.sql | 5 +++++ .../migrations/00009_AddUnit_Types_up.sql | 15 ++++++++++++++ .../00010_AddNumerical_Measurements_down.sql | 5 +++++ .../00010_AddNumerical_Measurements_up.sql | 17 ++++++++++++++++ .../00011_AddStrainObsMeasurements_down.sql | 5 +++++ .../00011_AddStrainObsMeasurements_up.sql | 17 ++++++++++++++++ 16 files changed, 170 insertions(+) create mode 100644 datastore/migrations/00004_AddStrain_down.sql create mode 100644 datastore/migrations/00004_AddStrain_up.sql create mode 100644 datastore/migrations/00005_AddObservationTypes_down.sql create mode 100644 datastore/migrations/00005_AddObservationTypes_up.sql create mode 100644 datastore/migrations/00006_AddObservations_down.sql create mode 100644 datastore/migrations/00006_AddObservations_up.sql create mode 100644 datastore/migrations/00007_AddStrainsObservations_down.sql create mode 100644 datastore/migrations/00007_AddStrainsObservations_up.sql create mode 100644 datastore/migrations/00008_AddText_Measurements_down.sql create mode 100644 datastore/migrations/00008_AddText_Measurements_up.sql create mode 100644 datastore/migrations/00009_AddUnit_Types_down.sql create mode 100644 datastore/migrations/00009_AddUnit_Types_up.sql create mode 100644 datastore/migrations/00010_AddNumerical_Measurements_down.sql create mode 100644 datastore/migrations/00010_AddNumerical_Measurements_up.sql create mode 100644 datastore/migrations/00011_AddStrainObsMeasurements_down.sql create mode 100644 datastore/migrations/00011_AddStrainObsMeasurements_up.sql diff --git a/datastore/migrations/00004_AddStrain_down.sql b/datastore/migrations/00004_AddStrain_down.sql new file mode 100644 index 0000000..94edb83 --- /dev/null +++ b/datastore/migrations/00004_AddStrain_down.sql @@ -0,0 +1,5 @@ +-- bactdb +-- Matthew R Dillon + +DROP TABLE strains; + diff --git a/datastore/migrations/00004_AddStrain_up.sql b/datastore/migrations/00004_AddStrain_up.sql new file mode 100644 index 0000000..855f3a9 --- /dev/null +++ b/datastore/migrations/00004_AddStrain_up.sql @@ -0,0 +1,20 @@ +-- bactdb +-- Matthew R Dillon + +CREATE TABLE strains ( + id BIGSERIAL NOT NULL, + species_id BIGINT, + strain_name CHARACTER VARYING(100), + strain_type CHARACTER VARYING(100), + etymology CHARACTER VARYING(500), + accession_banks CHARACTER VARYING(100), + genbank_embl_ddb CHARACTER VARYING(100), + + created_at TIMESTAMP WITH TIME ZONE, + updated_at TIMESTAMP WITH TIME ZONE, + deleted_at TIMESTAMP WITH TIME ZONE, + + CONSTRAINT strain_pkey PRIMARY KEY (id), + FOREIGN KEY (species_id) REFERENCES species(id) +); + diff --git a/datastore/migrations/00005_AddObservationTypes_down.sql b/datastore/migrations/00005_AddObservationTypes_down.sql new file mode 100644 index 0000000..39f6366 --- /dev/null +++ b/datastore/migrations/00005_AddObservationTypes_down.sql @@ -0,0 +1,5 @@ +-- bactdb +-- Matthew R Dillon + +DROP TABLE observation_types; + diff --git a/datastore/migrations/00005_AddObservationTypes_up.sql b/datastore/migrations/00005_AddObservationTypes_up.sql new file mode 100644 index 0000000..d18d8b5 --- /dev/null +++ b/datastore/migrations/00005_AddObservationTypes_up.sql @@ -0,0 +1,14 @@ +-- bactdb +-- Matthew R Dillon + +CREATE TABLE observation_types ( + id BIGSERIAL NOT NULL, + observation_type_name CHARACTER VARYING(100), + + created_at TIMESTAMP WITH TIME ZONE, + updated_at TIMESTAMP WITH TIME ZONE, + deleted_at TIMESTAMP WITH TIME ZONE, + + CONSTRAINT observation_types_pkey PRIMARY KEY (id) +); + diff --git a/datastore/migrations/00006_AddObservations_down.sql b/datastore/migrations/00006_AddObservations_down.sql new file mode 100644 index 0000000..5b5ef85 --- /dev/null +++ b/datastore/migrations/00006_AddObservations_down.sql @@ -0,0 +1,5 @@ +-- bactdb +-- Matthew R Dillon + +DROP TABLE observations; + diff --git a/datastore/migrations/00006_AddObservations_up.sql b/datastore/migrations/00006_AddObservations_up.sql new file mode 100644 index 0000000..df0a1be --- /dev/null +++ b/datastore/migrations/00006_AddObservations_up.sql @@ -0,0 +1,16 @@ +-- bactdb +-- Matthew R Dillon + +CREATE TABLE observations ( + id BIGSERIAL NOT NULL, + observation_name CHARACTER VARYING(100), + observation_type_id BIGINT, + + created_at TIMESTAMP WITH TIME ZONE, + updated_at TIMESTAMP WITH TIME ZONE, + deleted_at TIMESTAMP WITH TIME ZONE, + + CONSTRAINT observations_pkey PRIMARY KEY (id), + FOREIGN KEY (observation_type_id) REFERENCES observation_types(id) +); + diff --git a/datastore/migrations/00007_AddStrainsObservations_down.sql b/datastore/migrations/00007_AddStrainsObservations_down.sql new file mode 100644 index 0000000..c67ba07 --- /dev/null +++ b/datastore/migrations/00007_AddStrainsObservations_down.sql @@ -0,0 +1,5 @@ +-- bactdb +-- Matthew R Dillon + +DROP TABLE strainsobservations; + diff --git a/datastore/migrations/00007_AddStrainsObservations_up.sql b/datastore/migrations/00007_AddStrainsObservations_up.sql new file mode 100644 index 0000000..4c939be --- /dev/null +++ b/datastore/migrations/00007_AddStrainsObservations_up.sql @@ -0,0 +1,17 @@ +-- bactdb +-- Matthew R Dillon + +CREATE TABLE strainsobservations ( + id BIGSERIAL NOT NULL, + strain_id BIGINT NOT NULL, + observations_id BIGINT NOT NULL, + + created_at TIMESTAMP WITH TIME ZONE, + updated_at TIMESTAMP WITH TIME ZONE, + deleted_at TIMESTAMP WITH TIME ZONE, + + CONSTRAINT strainsobservations_pkey PRIMARY KEY (id), + FOREIGN KEY (strain_id) REFERENCES strains(id), + FOREIGN KEY (observations_id) REFERENCES observations(id) +); + diff --git a/datastore/migrations/00008_AddText_Measurements_down.sql b/datastore/migrations/00008_AddText_Measurements_down.sql new file mode 100644 index 0000000..3b8bc9b --- /dev/null +++ b/datastore/migrations/00008_AddText_Measurements_down.sql @@ -0,0 +1,5 @@ +-- bactdb +-- Matthew R Dillon + +DROP TABLE text_measurements; + diff --git a/datastore/migrations/00008_AddText_Measurements_up.sql b/datastore/migrations/00008_AddText_Measurements_up.sql new file mode 100644 index 0000000..d0992ce --- /dev/null +++ b/datastore/migrations/00008_AddText_Measurements_up.sql @@ -0,0 +1,14 @@ +-- bactdb +-- Matthew R Dillon + +CREATE TABLE text_measurements ( + id BIGSERIAL NOT NULL, + text_measurement_name CHARACTER VARYING(100), + + created_at TIMESTAMP WITH TIME ZONE, + updated_at TIMESTAMP WITH TIME ZONE, + deleted_at TIMESTAMP WITH TIME ZONE, + + CONSTRAINT text_measurements_pkey PRIMARY KEY (id) +); + diff --git a/datastore/migrations/00009_AddUnit_Types_down.sql b/datastore/migrations/00009_AddUnit_Types_down.sql new file mode 100644 index 0000000..ffbb77c --- /dev/null +++ b/datastore/migrations/00009_AddUnit_Types_down.sql @@ -0,0 +1,5 @@ +-- bactdb +-- Matthew R Dillon + +DROP TABLE unit_types; + diff --git a/datastore/migrations/00009_AddUnit_Types_up.sql b/datastore/migrations/00009_AddUnit_Types_up.sql new file mode 100644 index 0000000..348242a --- /dev/null +++ b/datastore/migrations/00009_AddUnit_Types_up.sql @@ -0,0 +1,15 @@ +-- bactdb +-- Matthew R Dillon + +CREATE TABLE unit_types ( + id BIGSERIAL NOT NULL, + name CHARACTER VARYING(100), + symbol CHARACTER VARYING(10), + + created_at TIMESTAMP WITH TIME ZONE, + updated_at TIMESTAMP WITH TIME ZONE, + deleted_at TIMESTAMP WITH TIME ZONE, + + CONSTRAINT unit_types_pkey PRIMARY KEY (id) +); + diff --git a/datastore/migrations/00010_AddNumerical_Measurements_down.sql b/datastore/migrations/00010_AddNumerical_Measurements_down.sql new file mode 100644 index 0000000..b8fbc65 --- /dev/null +++ b/datastore/migrations/00010_AddNumerical_Measurements_down.sql @@ -0,0 +1,5 @@ +-- bactdb +-- Matthew R Dillon + +DROP TABLE numerical_measurements; + diff --git a/datastore/migrations/00010_AddNumerical_Measurements_up.sql b/datastore/migrations/00010_AddNumerical_Measurements_up.sql new file mode 100644 index 0000000..b1deda2 --- /dev/null +++ b/datastore/migrations/00010_AddNumerical_Measurements_up.sql @@ -0,0 +1,17 @@ +-- bactdb +-- Matthew R Dillon + +CREATE TABLE numerical_measurements ( + id BIGSERIAL NOT NULL, + measurement_value NUMERIC(6, 4) NOT NULL, + confidence_interval NUMERIC(6,4) NULL, + unit_type_id BIGINT, + + created_at TIMESTAMP WITH TIME ZONE, + updated_at TIMESTAMP WITH TIME ZONE, + deleted_at TIMESTAMP WITH TIME ZONE, + + CONSTRAINT numerical_measurements_pkey PRIMARY KEY (id), + FOREIGN KEY (unit_type_id) REFERENCES unit_types(id) +); + diff --git a/datastore/migrations/00011_AddStrainObsMeasurements_down.sql b/datastore/migrations/00011_AddStrainObsMeasurements_down.sql new file mode 100644 index 0000000..815b20d --- /dev/null +++ b/datastore/migrations/00011_AddStrainObsMeasurements_down.sql @@ -0,0 +1,5 @@ +-- bactdb +-- Matthew R Dillon + +DROP TABLE strainsobsmeasurements; + diff --git a/datastore/migrations/00011_AddStrainObsMeasurements_up.sql b/datastore/migrations/00011_AddStrainObsMeasurements_up.sql new file mode 100644 index 0000000..9abf103 --- /dev/null +++ b/datastore/migrations/00011_AddStrainObsMeasurements_up.sql @@ -0,0 +1,17 @@ +-- bactdb +-- Matthew R Dillon + +CREATE TABLE strainsobsmeasurements ( + id BIGSERIAL NOT NULL, + strainsobservations_id BIGINT, + measurement_table CHARACTER VARYING(15), + measurement_id BIGINT, + + created_at TIMESTAMP WITH TIME ZONE, + updated_at TIMESTAMP WITH TIME ZONE, + deleted_at TIMESTAMP WITH TIME ZONE, + + CONSTRAINT strainsobsmeasurements_pkey PRIMARY KEY (id), + FOREIGN KEY (strainsobservations_id) REFERENCES strainsobservations(id) +); + From 5244ae529a5c9cf00f2da6d8be21a14a850e6b8a Mon Sep 17 00:00:00 2001 From: Matthew Dillon <mrdillon@alaska.edu> Date: Thu, 23 Oct 2014 17:29:07 -0800 Subject: [PATCH 6/9] Species: List species. --- api/handler.go | 1 + api/species.go | 17 +++++++++++++++++ api/species_test.go | 29 +++++++++++++++++++++++++++++ datastore/species.go | 12 ++++++++++++ datastore/species_test.go | 33 +++++++++++++++++++++++++++++++++ models/species.go | 35 +++++++++++++++++++++++++++++++++++ models/species_test.go | 32 ++++++++++++++++++++++++++++++++ router/api.go | 1 + router/routes.go | 1 + 9 files changed, 161 insertions(+) diff --git a/api/handler.go b/api/handler.go index 65bbc79..98c8952 100644 --- a/api/handler.go +++ b/api/handler.go @@ -31,6 +31,7 @@ func Handler() *mux.Router { m.Get(router.Species).Handler(handler(serveSpecies)) m.Get(router.CreateSpecies).Handler(handler(serveCreateSpecies)) + m.Get(router.SpeciesList).Handler(handler(serveSpeciesList)) return m } diff --git a/api/species.go b/api/species.go index b8bae18..7ff94ff 100644 --- a/api/species.go +++ b/api/species.go @@ -40,3 +40,20 @@ func serveCreateSpecies(w http.ResponseWriter, r *http.Request) error { return writeJSON(w, species) } + +func serveSpeciesList(w http.ResponseWriter, r *http.Request) error { + var opt models.SpeciesListOptions + if err := schemaDecoder.Decode(&opt, r.URL.Query()); err != nil { + return err + } + + species, err := store.Species.List(&opt) + if err != nil { + return err + } + if species == nil { + species = []*models.Species{} + } + + return writeJSON(w, species) +} diff --git a/api/species_test.go b/api/species_test.go index 24fd557..490c62c 100644 --- a/api/species_test.go +++ b/api/species_test.go @@ -59,3 +59,32 @@ func TestSpecies_Create(t *testing.T) { t.Error("!success") } } + +func TestSpecies_List(t *testing.T) { + setup() + + want := []*models.Species{{Id: 1, GenusId: 1, SpeciesName: "Test Species"}} + wantOpt := &models.SpeciesListOptions{ListOptions: models.ListOptions{Page: 1, PerPage: 10}} + + calledList := false + store.Species.(*models.MockSpeciesService).List_ = func(opt *models.SpeciesListOptions) ([]*models.Species, error) { + if !normalizeDeepEqual(wantOpt, opt) { + t.Errorf("wanted options %d but got %d", wantOpt, opt) + } + calledList = true + return want, nil + } + + species, err := apiClient.Species.List(wantOpt) + if err != nil { + t.Fatal(err) + } + + if !calledList { + t.Error("!calledList") + } + + if !normalizeDeepEqual(&want, &species) { + t.Errorf("got species %+v but wanted species %+v", species, want) + } +} diff --git a/datastore/species.go b/datastore/species.go index f7e6dee..cccdd07 100644 --- a/datastore/species.go +++ b/datastore/species.go @@ -27,3 +27,15 @@ func (s *speciesStore) Create(species *models.Species) (bool, error) { } return true, nil } + +func (s *speciesStore) List(opt *models.SpeciesListOptions) ([]*models.Species, error) { + if opt == nil { + opt = &models.SpeciesListOptions{} + } + var species []*models.Species + err := s.dbh.Select(&species, `SELECT * FROM species LIMIT $1 OFFSET $2;`, opt.PerPageOrDefault(), opt.Offset()) + if err != nil { + return nil, err + } + return species, nil +} diff --git a/datastore/species_test.go b/datastore/species_test.go index db81db6..a540a99 100644 --- a/datastore/species_test.go +++ b/datastore/species_test.go @@ -63,3 +63,36 @@ func TestSpeciesStore_Create_db(t *testing.T) { t.Error("want nonzero species.Id after submitting") } } + +func TestSpeciesStore_List_db(t *testing.T) { + tx, _ := DB.Begin() + defer tx.Rollback() + + // Test on a clean database + tx.Exec(`DELETE FROM species;`) + + genus := &models.Genus{} + + if err := tx.Insert(genus); err != nil { + t.Fatal(err) + } + + want := []*models.Species{{Id: 1, GenusId: genus.Id, SpeciesName: "Test Species"}} + + if err := tx.Insert(want[0]); err != nil { + t.Fatal(err) + } + + d := NewDatastore(tx) + species, err := d.Species.List(&models.SpeciesListOptions{ListOptions: models.ListOptions{Page: 1, PerPage: 10}}) + if err != nil { + t.Fatal(err) + } + + for _, g := range want { + normalizeTime(&g.CreatedAt, &g.UpdatedAt, &g.DeletedAt) + } + if !reflect.DeepEqual(species, want) { + t.Errorf("got species %+v, want %+v", species, want) + } +} diff --git a/models/species.go b/models/species.go index dbd696d..faf6820 100644 --- a/models/species.go +++ b/models/species.go @@ -24,6 +24,9 @@ type SpeciesService interface { // Get a species Get(id int64) (*Species, error) + // List all species + List(opt *SpeciesListOptions) ([]*Species, error) + // Create a species record Create(species *Species) (bool, error) } @@ -78,8 +81,33 @@ func (s *speciesService) Create(species *Species) (bool, error) { return resp.StatusCode == http.StatusCreated, nil } +type SpeciesListOptions struct { + ListOptions +} + +func (s *speciesService) List(opt *SpeciesListOptions) ([]*Species, error) { + url, err := s.client.url(router.SpeciesList, nil, opt) + if err != nil { + return nil, err + } + + req, err := s.client.NewRequest("GET", url.String(), nil) + if err != nil { + return nil, err + } + + var species []*Species + _, err = s.client.Do(req, &species) + if err != nil { + return nil, err + } + + return species, nil +} + type MockSpeciesService struct { Get_ func(id int64) (*Species, error) + List_ func(opt *SpeciesListOptions) ([]*Species, error) Create_ func(species *Species) (bool, error) } @@ -98,3 +126,10 @@ func (s *MockSpeciesService) Create(species *Species) (bool, error) { } return s.Create_(species) } + +func (s *MockSpeciesService) List(opt *SpeciesListOptions) ([]*Species, error) { + if s.List_ == nil { + return nil, nil + } + return s.List_(opt) +} diff --git a/models/species_test.go b/models/species_test.go index 338d13b..4e6873b 100644 --- a/models/species_test.go +++ b/models/species_test.go @@ -73,3 +73,35 @@ func TestSpeciesService_Create(t *testing.T) { t.Errorf("Species.Create returned %+v, want %+v", species, want) } } + +func TestSpeciesService_List(t *testing.T) { + setup() + defer teardown() + + want := []*Species{{Id: 1, GenusId: 1, SpeciesName: "Test Species"}} + + var called bool + mux.HandleFunc(urlPath(t, router.SpeciesList, nil), func(w http.ResponseWriter, r *http.Request) { + called = true + testMethod(t, r, "GET") + testFormValues(t, r, values{}) + + writeJSON(w, want) + }) + + species, err := client.Species.List(nil) + if err != nil { + t.Errorf("Species.List returned error: %v", err) + } + + if !called { + t.Fatal("!called") + } + + for _, u := range want { + normalizeTime(&u.CreatedAt, &u.UpdatedAt, &u.DeletedAt) + } + if !reflect.DeepEqual(species, want) { + t.Errorf("Species.List return %+v, want %+v", species, want) + } +} diff --git a/router/api.go b/router/api.go index 8ae8b02..2ffa19a 100644 --- a/router/api.go +++ b/router/api.go @@ -18,6 +18,7 @@ func API() *mux.Router { m.Path("/genera/{Id:.+}").Methods("DELETE").Name(DeleteGenus) // Species + m.Path("/species").Methods("GET").Name(SpeciesList) m.Path("/species").Methods("POST").Name(CreateSpecies) m.Path("/species/{Id:.+}").Methods("GET").Name(Species) diff --git a/router/routes.go b/router/routes.go index 8b2bd70..4d7a9ac 100644 --- a/router/routes.go +++ b/router/routes.go @@ -13,4 +13,5 @@ const ( Species = "species" CreateSpecies = "species:create" + SpeciesList = "species:list" ) From c8d1d0a84f589da0884b4e3a52a58e2d06d0dc9f Mon Sep 17 00:00:00 2001 From: Matthew Dillon <mrdillon@alaska.edu> Date: Thu, 23 Oct 2014 18:25:15 -0800 Subject: [PATCH 7/9] species update wip --- datastore/species_test.go | 35 +++++++++++++++++++++++++++++++++++ models/species.go | 32 ++++++++++++++++++++++++++++++++ models/species_test.go | 31 +++++++++++++++++++++++++++++++ router/api.go | 1 + router/routes.go | 1 + 5 files changed, 100 insertions(+) diff --git a/datastore/species_test.go b/datastore/species_test.go index a540a99..62235ba 100644 --- a/datastore/species_test.go +++ b/datastore/species_test.go @@ -96,3 +96,38 @@ func TestSpeciesStore_List_db(t *testing.T) { t.Errorf("got species %+v, want %+v", species, want) } } + +func TestSpeciesStore_Update_db(t *testing.T) { + tx, _ := DB.Begin() + defer tx.Rollback() + + // Test on a clean database + tx.Exec(`DELETE FROM species;`) + + d := NewDatastore(nil) + // Add a new record + genus := &models.Genus{GenusName: "Test Genus"} + _, err := d.Genera.Create(genus) + if err != nil { + t.Fatal(err) + } + created := &model.Species{GenusId: genus.Id, SpeciesName: "Test Species"} + _, err := d.Species.Create(created) + if err != nil { + t.Fatal(err) + } + if !created { + t.Error("!created") + } + + // Tweak it + species.SpeciesName = "Updated Species" + updated, err := d.Species.Update(species.Id, species) + if err != nil { + t.Fatal(err) + } + + if !updated { + t.Error("!updated") + } +} diff --git a/models/species.go b/models/species.go index faf6820..b742de8 100644 --- a/models/species.go +++ b/models/species.go @@ -29,6 +29,9 @@ type SpeciesService interface { // Create a species record Create(species *Species) (bool, error) + + // Update an existing species + Update(id int64, species *Species) (updated bool, err error) } var ( @@ -105,10 +108,32 @@ func (s *speciesService) List(opt *SpeciesListOptions) ([]*Species, error) { return species, nil } +func (s *speciesService) Update(id int64, species *Species) (bool, error) { + strId := strconv.FormatInt(id, 10) + + url, err := s.client.url(router.UpdateSpecies, map[string]string{"Id": strId}, nil) + if err != nil { + return false, err + } + + req, err := s.client.NewRequest("PUT", url.String(), species) + if err != nil { + return false, err + } + + resp, err := s.client.Do(req, &species) + if err != nil { + return false, err + } + + return resp.StatusCode == http.StatusOK, nil +} + type MockSpeciesService struct { Get_ func(id int64) (*Species, error) List_ func(opt *SpeciesListOptions) ([]*Species, error) Create_ func(species *Species) (bool, error) + Update_ func(id int64, species *Species) (bool, error) } var _ SpeciesService = &MockSpeciesService{} @@ -133,3 +158,10 @@ func (s *MockSpeciesService) List(opt *SpeciesListOptions) ([]*Species, error) { } return s.List_(opt) } + +func (s *MockSpeciesService) Update(id int64, species *Species) (bool, error) { + if s.Update_ == nil { + return false, nil + } + return s.Update_(id, species) +} diff --git a/models/species_test.go b/models/species_test.go index 4e6873b..cb66f9e 100644 --- a/models/species_test.go +++ b/models/species_test.go @@ -105,3 +105,34 @@ func TestSpeciesService_List(t *testing.T) { t.Errorf("Species.List return %+v, want %+v", species, want) } } + +func TestSpeciesService_Update(t *testing.T) { + setup() + defer teardown() + + want := &Species{Id: 1, GenusId: 1, SpeciesName: "Test Species"} + + var called bool + mux.HandleFunc(urlPath(t, router.UpdateSpecies, map[string]string{"Id": "1"}), func(w http.ResponseWriter, r *http.Request) { + called = true + testMethod(t, r, "PUT") + testBody(t, r, `{"id":1,"genus_id":1,"species_name":"Test Species Updated","created_at":"0001-01-01T00:00:00Z","updated_at":"0001-01-01T00:00:00Z","deleted_at":"0001-01-01T00:00:00Z"}`+"\n") + + w.WriteHeader(http.StatusOK) + writeJSON(w, want) + }) + + species := &Species{Id: 1, GenusId: 1, SpeciesName: "Test Species Updated"} + updated, err := client.Species.Update(1, species) + if err != nil { + t.Errorf("Species.Update returned error: %v", err) + } + + if !updated { + t.Error("!updated") + } + + if !called { + t.Fatal("!called") + } +} diff --git a/router/api.go b/router/api.go index 2ffa19a..5bf24d2 100644 --- a/router/api.go +++ b/router/api.go @@ -21,6 +21,7 @@ func API() *mux.Router { m.Path("/species").Methods("GET").Name(SpeciesList) m.Path("/species").Methods("POST").Name(CreateSpecies) m.Path("/species/{Id:.+}").Methods("GET").Name(Species) + m.Path("/species/{Id:.+}").Methods("PUT").Name(UpdateSpecies) return m } diff --git a/router/routes.go b/router/routes.go index 4d7a9ac..0db9205 100644 --- a/router/routes.go +++ b/router/routes.go @@ -14,4 +14,5 @@ const ( Species = "species" CreateSpecies = "species:create" SpeciesList = "species:list" + UpdateSpecies = "species:update" ) From 22d2b8b41dfe2afb644026f863aef5cb37efcbe3 Mon Sep 17 00:00:00 2001 From: Matthew Dillon <mrdillon@alaska.edu> Date: Fri, 24 Oct 2014 10:08:12 -0800 Subject: [PATCH 8/9] Finishing up changes from previous commit --- api/handler.go | 1 + api/species.go | 19 +++++++++++++++++++ api/species_test.go | 30 ++++++++++++++++++++++++++++++ datastore/species.go | 22 ++++++++++++++++++++++ datastore/species_test.go | 4 ++-- 5 files changed, 74 insertions(+), 2 deletions(-) diff --git a/api/handler.go b/api/handler.go index 98c8952..b7999d8 100644 --- a/api/handler.go +++ b/api/handler.go @@ -32,6 +32,7 @@ func Handler() *mux.Router { m.Get(router.Species).Handler(handler(serveSpecies)) m.Get(router.CreateSpecies).Handler(handler(serveCreateSpecies)) m.Get(router.SpeciesList).Handler(handler(serveSpeciesList)) + m.Get(router.UpdateSpecies).Handler(handler(serveUpdateSpecies)) return m } diff --git a/api/species.go b/api/species.go index 7ff94ff..1e218a4 100644 --- a/api/species.go +++ b/api/species.go @@ -57,3 +57,22 @@ func serveSpeciesList(w http.ResponseWriter, r *http.Request) error { return writeJSON(w, species) } + +func serveUpdateSpecies(w http.ResponseWriter, r *http.Request) error { + id, _ := strconv.ParseInt(mux.Vars(r)["Id"], 10, 0) + var species models.Species + err := json.NewDecoder(r.Body).Decode(&species) + if err != nil { + return err + } + + updated, err := store.Species.Update(id, &species) + if err != nil { + return err + } + if updated { + w.WriteHeader(http.StatusOK) + } + + return writeJSON(w, species) +} diff --git a/api/species_test.go b/api/species_test.go index 490c62c..a0c003a 100644 --- a/api/species_test.go +++ b/api/species_test.go @@ -88,3 +88,33 @@ func TestSpecies_List(t *testing.T) { t.Errorf("got species %+v but wanted species %+v", species, want) } } + +func TestSpecies_Update(t *testing.T) { + setup() + + want := &models.Species{Id: 1, GenusId: 1, SpeciesName: "Test Species"} + + calledPut := false + store.Species.(*models.MockSpeciesService).Update_ = func(id int64, species *models.Species) (bool, error) { + if id != want.Id { + t.Errorf("wanted request for species %d but got %d", want.Id, id) + } + if !normalizeDeepEqual(want, species) { + t.Errorf("wanted request for species %d but got %d", want, species) + } + calledPut = true + return true, nil + } + + success, err := apiClient.Species.Update(1, want) + if err != nil { + t.Fatal(err) + } + + if !calledPut { + t.Error("!calledPut") + } + if !success { + t.Error("!success") + } +} diff --git a/datastore/species.go b/datastore/species.go index cccdd07..c3087c4 100644 --- a/datastore/species.go +++ b/datastore/species.go @@ -39,3 +39,25 @@ func (s *speciesStore) List(opt *models.SpeciesListOptions) ([]*models.Species, } return species, nil } + +func (s *speciesStore) Update(id int64, species *models.Species) (bool, error) { + _, err := s.Get(id) + if err != nil { + return false, err + } + + if id != species.Id { + return false, models.ErrSpeciesNotFound + } + + changed, err := s.dbh.Update(species) + if err != nil { + return false, err + } + + if changed == 0 { + return false, ErrNoRowsUpdated + } + + return true, nil +} diff --git a/datastore/species_test.go b/datastore/species_test.go index 62235ba..1d7c3cd 100644 --- a/datastore/species_test.go +++ b/datastore/species_test.go @@ -111,8 +111,8 @@ func TestSpeciesStore_Update_db(t *testing.T) { if err != nil { t.Fatal(err) } - created := &model.Species{GenusId: genus.Id, SpeciesName: "Test Species"} - _, err := d.Species.Create(created) + species := &models.Species{GenusId: genus.Id, SpeciesName: "Test Species"} + created, err := d.Species.Create(species) if err != nil { t.Fatal(err) } From 4669aff3c2f577eb171455351bf38c1f8ef61db3 Mon Sep 17 00:00:00 2001 From: Matthew Dillon <mrdillon@alaska.edu> Date: Fri, 24 Oct 2014 10:42:31 -0800 Subject: [PATCH 9/9] Species - delete --- api/handler.go | 1 + api/species.go | 14 ++++++++++++++ api/species_test.go | 27 +++++++++++++++++++++++++++ datastore/species.go | 16 ++++++++++++++++ datastore/species_test.go | 39 +++++++++++++++++++++++++++++++++++++++ models/species.go | 33 +++++++++++++++++++++++++++++++++ models/species_test.go | 29 +++++++++++++++++++++++++++++ router/api.go | 1 + router/routes.go | 1 + 9 files changed, 161 insertions(+) diff --git a/api/handler.go b/api/handler.go index b7999d8..fd686c6 100644 --- a/api/handler.go +++ b/api/handler.go @@ -33,6 +33,7 @@ func Handler() *mux.Router { m.Get(router.CreateSpecies).Handler(handler(serveCreateSpecies)) m.Get(router.SpeciesList).Handler(handler(serveSpeciesList)) m.Get(router.UpdateSpecies).Handler(handler(serveUpdateSpecies)) + m.Get(router.DeleteSpecies).Handler(handler(serveDeleteSpecies)) return m } diff --git a/api/species.go b/api/species.go index 1e218a4..fe0b7c3 100644 --- a/api/species.go +++ b/api/species.go @@ -76,3 +76,17 @@ func serveUpdateSpecies(w http.ResponseWriter, r *http.Request) error { return writeJSON(w, species) } + +func serveDeleteSpecies(w http.ResponseWriter, r *http.Request) error { + id, _ := strconv.ParseInt(mux.Vars(r)["Id"], 10, 0) + + deleted, err := store.Species.Delete(id) + if err != nil { + return err + } + if deleted { + w.WriteHeader(http.StatusOK) + } + + return writeJSON(w, &models.Species{}) +} diff --git a/api/species_test.go b/api/species_test.go index a0c003a..418091e 100644 --- a/api/species_test.go +++ b/api/species_test.go @@ -118,3 +118,30 @@ func TestSpecies_Update(t *testing.T) { t.Error("!success") } } + +func TestSpecies_Delete(t *testing.T) { + setup() + + want := &models.Species{Id: 1, GenusId: 1, SpeciesName: "Test Species"} + + calledDelete := false + store.Species.(*models.MockSpeciesService).Delete_ = func(id int64) (bool, error) { + if id != want.Id { + t.Errorf("wanted request for species %d but got %d", want.Id, id) + } + calledDelete = true + return true, nil + } + + success, err := apiClient.Species.Delete(1) + if err != nil { + t.Fatal(err) + } + + if !calledDelete { + t.Error("!calledDelete") + } + if !success { + t.Error("!success") + } +} diff --git a/datastore/species.go b/datastore/species.go index c3087c4..651c3e0 100644 --- a/datastore/species.go +++ b/datastore/species.go @@ -61,3 +61,19 @@ func (s *speciesStore) Update(id int64, species *models.Species) (bool, error) { return true, nil } + +func (s *speciesStore) Delete(id int64) (bool, error) { + species, err := s.Get(id) + if err != nil { + return false, err + } + + deleted, err := s.dbh.Delete(species) + if err != nil { + return false, err + } + if deleted == 0 { + return false, ErrNoRowsDeleted + } + return true, nil +} diff --git a/datastore/species_test.go b/datastore/species_test.go index 1d7c3cd..5839e95 100644 --- a/datastore/species_test.go +++ b/datastore/species_test.go @@ -13,6 +13,7 @@ func TestSpeciesStore_Get_db(t *testing.T) { // Test on a clean database tx.Exec(`DELETE FROM species;`) + tx.Exec(`DELETE FROM genera;`) wantGenus := &models.Genus{GenusName: "Test Genus"} if err := tx.Insert(wantGenus); err != nil { @@ -42,6 +43,7 @@ func TestSpeciesStore_Create_db(t *testing.T) { // Test on a clean database tx.Exec(`DELETE FROM species;`) + tx.Exec(`DELETE FROM genera;`) genus := &models.Genus{} if err := tx.Insert(genus); err != nil { @@ -70,6 +72,7 @@ func TestSpeciesStore_List_db(t *testing.T) { // Test on a clean database tx.Exec(`DELETE FROM species;`) + tx.Exec(`DELETE FROM genera;`) genus := &models.Genus{} @@ -103,6 +106,7 @@ func TestSpeciesStore_Update_db(t *testing.T) { // Test on a clean database tx.Exec(`DELETE FROM species;`) + tx.Exec(`DELETE FROM genera;`) d := NewDatastore(nil) // Add a new record @@ -131,3 +135,38 @@ func TestSpeciesStore_Update_db(t *testing.T) { t.Error("!updated") } } + +func TestSpeciesStore_Delete_db(t *testing.T) { + tx, _ := DB.Begin() + defer tx.Rollback() + + // Test on a clean database + tx.Exec(`DELETE FROM species;`) + tx.Exec(`DELETE FROM genera;`) + + d := NewDatastore(tx) + // Add a new record + genus := &models.Genus{GenusName: "Test Genus"} + _, err := d.Genera.Create(genus) + if err != nil { + t.Fatal(err) + } + species := &models.Species{GenusId: genus.Id, SpeciesName: "Test Species"} + created, err := d.Species.Create(species) + if err != nil { + t.Fatal(err) + } + if !created { + t.Error("!created") + } + + // Delete it + deleted, err := d.Species.Delete(species.Id) + if err != nil { + t.Fatal(err) + } + + if !deleted { + t.Error("!delete") + } +} diff --git a/models/species.go b/models/species.go index b742de8..19b69fb 100644 --- a/models/species.go +++ b/models/species.go @@ -32,6 +32,9 @@ type SpeciesService interface { // Update an existing species Update(id int64, species *Species) (updated bool, err error) + + // Delete an existing species + Delete(id int64) (deleted bool, err error) } var ( @@ -129,11 +132,34 @@ func (s *speciesService) Update(id int64, species *Species) (bool, error) { return resp.StatusCode == http.StatusOK, nil } +func (s *speciesService) Delete(id int64) (bool, error) { + strId := strconv.FormatInt(id, 10) + + url, err := s.client.url(router.DeleteSpecies, map[string]string{"Id": strId}, nil) + if err != nil { + return false, err + } + + req, err := s.client.NewRequest("DELETE", url.String(), nil) + if err != nil { + return false, err + } + + var species *Species + resp, err := s.client.Do(req, &species) + if err != nil { + return false, err + } + + return resp.StatusCode == http.StatusOK, nil +} + type MockSpeciesService struct { Get_ func(id int64) (*Species, error) List_ func(opt *SpeciesListOptions) ([]*Species, error) Create_ func(species *Species) (bool, error) Update_ func(id int64, species *Species) (bool, error) + Delete_ func(id int64) (bool, error) } var _ SpeciesService = &MockSpeciesService{} @@ -165,3 +191,10 @@ func (s *MockSpeciesService) Update(id int64, species *Species) (bool, error) { } return s.Update_(id, species) } + +func (s *MockSpeciesService) Delete(id int64) (bool, error) { + if s.Delete_ == nil { + return false, nil + } + return s.Delete_(id) +} diff --git a/models/species_test.go b/models/species_test.go index cb66f9e..f2ef1e9 100644 --- a/models/species_test.go +++ b/models/species_test.go @@ -136,3 +136,32 @@ func TestSpeciesService_Update(t *testing.T) { t.Fatal("!called") } } + +func TestSpeciesService_Delete(t *testing.T) { + setup() + defer teardown() + + want := &Species{Id: 1, GenusId: 1, SpeciesName: "Test Species"} + + var called bool + mux.HandleFunc(urlPath(t, router.DeleteSpecies, map[string]string{"Id": "1"}), func(w http.ResponseWriter, r *http.Request) { + called = true + testMethod(t, r, "DELETE") + + w.WriteHeader(http.StatusOK) + writeJSON(w, want) + }) + + deleted, err := client.Species.Delete(1) + if err != nil { + t.Errorf("Species.Delete returned error: %v", err) + } + + if !deleted { + t.Error("!deleted") + } + + if !called { + t.Fatal("!called") + } +} diff --git a/router/api.go b/router/api.go index 5bf24d2..a7731ab 100644 --- a/router/api.go +++ b/router/api.go @@ -22,6 +22,7 @@ func API() *mux.Router { m.Path("/species").Methods("POST").Name(CreateSpecies) m.Path("/species/{Id:.+}").Methods("GET").Name(Species) m.Path("/species/{Id:.+}").Methods("PUT").Name(UpdateSpecies) + m.Path("/species/{Id:.+}").Methods("DELETE").Name(DeleteSpecies) return m } diff --git a/router/routes.go b/router/routes.go index 0db9205..124dd98 100644 --- a/router/routes.go +++ b/router/routes.go @@ -15,4 +15,5 @@ const ( CreateSpecies = "species:create" SpeciesList = "species:list" UpdateSpecies = "species:update" + DeleteSpecies = "species:delete" )