Ember data: convey genera/species relationships
This commit is contained in:
		
							parent
							
								
									52b21b24d8
								
							
						
					
					
						commit
						1dbfb3bc54
					
				
					 7 changed files with 62 additions and 20 deletions
				
			
		|  | @ -8,7 +8,7 @@ import ( | ||||||
| ) | ) | ||||||
| 
 | 
 | ||||||
| func init() { | func init() { | ||||||
| 	DB.AddTableWithName(models.Genus{}, "genera").SetKeys(true, "Id") | 	DB.AddTableWithName(models.GenusBase{}, "genera").SetKeys(true, "Id") | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| type generaStore struct { | type generaStore struct { | ||||||
|  | @ -17,7 +17,8 @@ type generaStore struct { | ||||||
| 
 | 
 | ||||||
| func (s *generaStore) Get(id int64) (*models.Genus, error) { | func (s *generaStore) Get(id int64) (*models.Genus, error) { | ||||||
| 	var genus models.Genus | 	var genus models.Genus | ||||||
| 	if err := s.dbh.SelectOne(&genus, `SELECT * FROM genera WHERE id=$1;`, id); err != nil { | 	err := s.dbh.SelectOne(&genus, `SELECT g.*, array_agg(s.id) AS species FROM genera g LEFT OUTER JOIN species s ON s.genus_id=g.id WHERE g.id=$1 GROUP BY g.id;`, id) | ||||||
|  | 	if err != nil { | ||||||
| 		return nil, err | 		return nil, err | ||||||
| 	} | 	} | ||||||
| 	if &genus == nil { | 	if &genus == nil { | ||||||
|  | @ -30,11 +31,14 @@ func (s *generaStore) Create(genus *models.Genus) (bool, error) { | ||||||
| 	currentTime := time.Now() | 	currentTime := time.Now() | ||||||
| 	genus.CreatedAt = currentTime | 	genus.CreatedAt = currentTime | ||||||
| 	genus.UpdatedAt = currentTime | 	genus.UpdatedAt = currentTime | ||||||
| 	if err := s.dbh.Insert(genus); err != nil { | 	// Ugly --- extract embedded struct | ||||||
|  | 	base := genus.GenusBase | ||||||
|  | 	if err := s.dbh.Insert(base); err != nil { | ||||||
| 		if strings.Contains(err.Error(), `violates unique constraint "genus_idx"`) { | 		if strings.Contains(err.Error(), `violates unique constraint "genus_idx"`) { | ||||||
| 			return false, err | 			return false, err | ||||||
| 		} | 		} | ||||||
| 	} | 	} | ||||||
|  | 	genus.Id = base.Id | ||||||
| 	return true, nil | 	return true, nil | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
|  | @ -43,7 +47,7 @@ func (s *generaStore) List(opt *models.GenusListOptions) ([]*models.Genus, error | ||||||
| 		opt = &models.GenusListOptions{} | 		opt = &models.GenusListOptions{} | ||||||
| 	} | 	} | ||||||
| 	var genera []*models.Genus | 	var genera []*models.Genus | ||||||
| 	err := s.dbh.Select(&genera, `SELECT * FROM genera LIMIT $1 OFFSET $2;`, opt.PerPageOrDefault(), opt.Offset()) | 	err := s.dbh.Select(&genera, `SELECT g.*, array_agg(s.id) AS species FROM genera g LEFT OUTER JOIN species s ON s.genus_id=g.id GROUP BY g.id LIMIT $1 OFFSET $2;`, opt.PerPageOrDefault(), opt.Offset()) | ||||||
| 	if err != nil { | 	if err != nil { | ||||||
| 		return nil, err | 		return nil, err | ||||||
| 	} | 	} | ||||||
|  | @ -61,7 +65,8 @@ func (s *generaStore) Update(id int64, genus *models.Genus) (bool, error) { | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
| 	genus.UpdatedAt = time.Now() | 	genus.UpdatedAt = time.Now() | ||||||
| 	changed, err := s.dbh.Update(genus) | 
 | ||||||
|  | 	changed, err := s.dbh.Update(genus.GenusBase) | ||||||
| 	if err != nil { | 	if err != nil { | ||||||
| 		return false, err | 		return false, err | ||||||
| 	} | 	} | ||||||
|  | @ -79,7 +84,7 @@ func (s *generaStore) Delete(id int64) (bool, error) { | ||||||
| 		return false, err | 		return false, err | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
| 	deleted, err := s.dbh.Delete(genus) | 	deleted, err := s.dbh.Delete(genus.GenusBase) | ||||||
| 	if err != nil { | 	if err != nil { | ||||||
| 		return false, err | 		return false, err | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
|  | @ -12,15 +12,15 @@ func insertGenus(t *testing.T, tx *modl.Transaction) *models.Genus { | ||||||
| 	// Test on a clean database | 	// Test on a clean database | ||||||
| 	tx.Exec(`DELETE FROM genera;`) | 	tx.Exec(`DELETE FROM genera;`) | ||||||
| 
 | 
 | ||||||
| 	genus := newGenus() | 	g := newGenus() | ||||||
| 	if err := tx.Insert(genus); err != nil { | 	if err := tx.Insert(g); err != nil { | ||||||
| 		t.Fatal(err) | 		t.Fatal(err) | ||||||
| 	} | 	} | ||||||
| 	return genus | 	return &models.Genus{g, []int64(nil)} | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| func newGenus() *models.Genus { | func newGenus() *models.GenusBase { | ||||||
| 	return &models.Genus{GenusName: "Test Genus"} | 	return &models.GenusBase{GenusName: "Test Genus"} | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| func TestGeneraStore_Get_db(t *testing.T) { | func TestGeneraStore_Get_db(t *testing.T) { | ||||||
|  | @ -46,10 +46,11 @@ func TestGeneraStore_Create_db(t *testing.T) { | ||||||
| 	tx, _ := DB.Begin() | 	tx, _ := DB.Begin() | ||||||
| 	defer tx.Rollback() | 	defer tx.Rollback() | ||||||
| 
 | 
 | ||||||
| 	genus := newGenus() | 	base_genus := newGenus() | ||||||
|  | 	genus := models.Genus{base_genus, []int64(nil)} | ||||||
| 
 | 
 | ||||||
| 	d := NewDatastore(tx) | 	d := NewDatastore(tx) | ||||||
| 	created, err := d.Genera.Create(genus) | 	created, err := d.Genera.Create(&genus) | ||||||
| 	if err != nil { | 	if err != nil { | ||||||
| 		t.Fatal(err) | 		t.Fatal(err) | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
|  | @ -11,7 +11,7 @@ import ( | ||||||
| ) | ) | ||||||
| 
 | 
 | ||||||
| // A Genus is a high-level classifier in bactdb. | // A Genus is a high-level classifier in bactdb. | ||||||
| type Genus struct { | type GenusBase struct { | ||||||
| 	Id        int64     `json:"id,omitempty"` | 	Id        int64     `json:"id,omitempty"` | ||||||
| 	GenusName string    `db:"genus_name" json:"genusName"` | 	GenusName string    `db:"genus_name" json:"genusName"` | ||||||
| 	CreatedAt time.Time `db:"created_at" json:"createdAt"` | 	CreatedAt time.Time `db:"created_at" json:"createdAt"` | ||||||
|  | @ -19,6 +19,11 @@ type Genus struct { | ||||||
| 	DeletedAt NullTime  `db:"deleted_at" json:"deletedAt"` | 	DeletedAt NullTime  `db:"deleted_at" json:"deletedAt"` | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
|  | type Genus struct { | ||||||
|  | 	*GenusBase | ||||||
|  | 	Species NullSliceInt64 `db:"species" json:"species"` | ||||||
|  | } | ||||||
|  | 
 | ||||||
| type GenusJSON struct { | type GenusJSON struct { | ||||||
| 	Genus *Genus `json:"genus"` | 	Genus *Genus `json:"genus"` | ||||||
| } | } | ||||||
|  | @ -31,8 +36,12 @@ func (m *Genus) String() string { | ||||||
| 	return fmt.Sprintf("%v", *m) | 	return fmt.Sprintf("%v", *m) | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
|  | func (m *GenusBase) String() string { | ||||||
|  | 	return fmt.Sprintf("%v", *m) | ||||||
|  | } | ||||||
|  | 
 | ||||||
| func NewGenus() *Genus { | func NewGenus() *Genus { | ||||||
| 	return &Genus{GenusName: "Test Genus"} | 	return &Genus{&GenusBase{GenusName: "Test Genus"}, make([]int64, 0)} | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| // GeneraService interacts with the genus-related endpoints in bactdb's API. | // GeneraService interacts with the genus-related endpoints in bactdb's API. | ||||||
|  |  | ||||||
|  | @ -54,7 +54,7 @@ func TestGeneraService_Create(t *testing.T) { | ||||||
| 	mux.HandleFunc(urlPath(t, router.CreateGenus, nil), func(w http.ResponseWriter, r *http.Request) { | 	mux.HandleFunc(urlPath(t, router.CreateGenus, nil), func(w http.ResponseWriter, r *http.Request) { | ||||||
| 		called = true | 		called = true | ||||||
| 		testMethod(t, r, "POST") | 		testMethod(t, r, "POST") | ||||||
| 		testBody(t, r, `{"genus":{"id":1,"genusName":"Test Genus","createdAt":"0001-01-01T00:00:00Z","updatedAt":"0001-01-01T00:00:00Z","deletedAt":null}}`+"\n") | 		testBody(t, r, `{"genus":{"id":1,"genusName":"Test Genus","createdAt":"0001-01-01T00:00:00Z","updatedAt":"0001-01-01T00:00:00Z","deletedAt":null,"species":[]}}`+"\n") | ||||||
| 
 | 
 | ||||||
| 		w.WriteHeader(http.StatusCreated) | 		w.WriteHeader(http.StatusCreated) | ||||||
| 		writeJSON(w, want) | 		writeJSON(w, want) | ||||||
|  | @ -124,7 +124,7 @@ func TestGeneraService_Update(t *testing.T) { | ||||||
| 	mux.HandleFunc(urlPath(t, router.UpdateGenus, map[string]string{"Id": "1"}), func(w http.ResponseWriter, r *http.Request) { | 	mux.HandleFunc(urlPath(t, router.UpdateGenus, map[string]string{"Id": "1"}), func(w http.ResponseWriter, r *http.Request) { | ||||||
| 		called = true | 		called = true | ||||||
| 		testMethod(t, r, "PUT") | 		testMethod(t, r, "PUT") | ||||||
| 		testBody(t, r, `{"genus":{"id":1,"genusName":"Test Genus Updated","createdAt":"0001-01-01T00:00:00Z","updatedAt":"0001-01-01T00:00:00Z","deletedAt":null}}`+"\n") | 		testBody(t, r, `{"genus":{"id":1,"genusName":"Test Genus Updated","createdAt":"0001-01-01T00:00:00Z","updatedAt":"0001-01-01T00:00:00Z","deletedAt":null,"species":[]}}`+"\n") | ||||||
| 
 | 
 | ||||||
| 		w.WriteHeader(http.StatusOK) | 		w.WriteHeader(http.StatusOK) | ||||||
| 		writeJSON(w, want) | 		writeJSON(w, want) | ||||||
|  |  | ||||||
|  | @ -13,7 +13,7 @@ import ( | ||||||
| // A Species is a high-level classifier in bactdb. | // A Species is a high-level classifier in bactdb. | ||||||
| type Species struct { | type Species struct { | ||||||
| 	Id          int64     `json:"id,omitempty"` | 	Id          int64     `json:"id,omitempty"` | ||||||
| 	GenusId     int64     `db:"genus_id" json:"genusId"` | 	GenusId     int64     `db:"genus_id" json:"genus"` | ||||||
| 	SpeciesName string    `db:"species_name" json:"speciesName"` | 	SpeciesName string    `db:"species_name" json:"speciesName"` | ||||||
| 	CreatedAt   time.Time `db:"created_at" json:"createdAt"` | 	CreatedAt   time.Time `db:"created_at" json:"createdAt"` | ||||||
| 	UpdatedAt   time.Time `db:"updated_at" json:"updatedAt"` | 	UpdatedAt   time.Time `db:"updated_at" json:"updatedAt"` | ||||||
|  |  | ||||||
|  | @ -55,7 +55,7 @@ func TestSpeciesService_Create(t *testing.T) { | ||||||
| 	mux.HandleFunc(urlPath(t, router.CreateSpecies, nil), func(w http.ResponseWriter, r *http.Request) { | 	mux.HandleFunc(urlPath(t, router.CreateSpecies, nil), func(w http.ResponseWriter, r *http.Request) { | ||||||
| 		called = true | 		called = true | ||||||
| 		testMethod(t, r, "POST") | 		testMethod(t, r, "POST") | ||||||
| 		testBody(t, r, `{"species":{"id":1,"genusId":1,"speciesName":"Test Species","createdAt":"0001-01-01T00:00:00Z","updatedAt":"0001-01-01T00:00:00Z","deletedAt":null}}`+"\n") | 		testBody(t, r, `{"species":{"id":1,"genus":1,"speciesName":"Test Species","createdAt":"0001-01-01T00:00:00Z","updatedAt":"0001-01-01T00:00:00Z","deletedAt":null}}`+"\n") | ||||||
| 
 | 
 | ||||||
| 		w.WriteHeader(http.StatusCreated) | 		w.WriteHeader(http.StatusCreated) | ||||||
| 		writeJSON(w, want) | 		writeJSON(w, want) | ||||||
|  | @ -124,7 +124,7 @@ func TestSpeciesService_Update(t *testing.T) { | ||||||
| 	mux.HandleFunc(urlPath(t, router.UpdateSpecies, map[string]string{"Id": "1"}), func(w http.ResponseWriter, r *http.Request) { | 	mux.HandleFunc(urlPath(t, router.UpdateSpecies, map[string]string{"Id": "1"}), func(w http.ResponseWriter, r *http.Request) { | ||||||
| 		called = true | 		called = true | ||||||
| 		testMethod(t, r, "PUT") | 		testMethod(t, r, "PUT") | ||||||
| 		testBody(t, r, `{"species":{"id":1,"genusId":1,"speciesName":"Test Species Updated","createdAt":"0001-01-01T00:00:00Z","updatedAt":"0001-01-01T00:00:00Z","deletedAt":null}}`+"\n") | 		testBody(t, r, `{"species":{"id":1,"genus":1,"speciesName":"Test Species Updated","createdAt":"0001-01-01T00:00:00Z","updatedAt":"0001-01-01T00:00:00Z","deletedAt":null}}`+"\n") | ||||||
| 
 | 
 | ||||||
| 		w.WriteHeader(http.StatusOK) | 		w.WriteHeader(http.StatusOK) | ||||||
| 		writeJSON(w, want) | 		writeJSON(w, want) | ||||||
|  |  | ||||||
|  | @ -4,6 +4,9 @@ import ( | ||||||
| 	"bytes" | 	"bytes" | ||||||
| 	"database/sql" | 	"database/sql" | ||||||
| 	"encoding/json" | 	"encoding/json" | ||||||
|  | 	"errors" | ||||||
|  | 	"strconv" | ||||||
|  | 	"strings" | ||||||
| 	"time" | 	"time" | ||||||
| 
 | 
 | ||||||
| 	"github.com/lib/pq" | 	"github.com/lib/pq" | ||||||
|  | @ -129,3 +132,27 @@ func (t *NullTime) UnmarshalJSON(b []byte) error { | ||||||
| 	t.Valid = true | 	t.Valid = true | ||||||
| 	return err | 	return err | ||||||
| } | } | ||||||
|  | 
 | ||||||
|  | type NullSliceInt64 []int64 | ||||||
|  | 
 | ||||||
|  | func (i *NullSliceInt64) Scan(src interface{}) error { | ||||||
|  | 	asBytes, ok := src.([]byte) | ||||||
|  | 	if !ok { | ||||||
|  | 		return errors.New("Scan source was not []byte") | ||||||
|  | 	} | ||||||
|  | 	asString := string(asBytes) | ||||||
|  | 	(*i) = strToIntSlice(asString) | ||||||
|  | 	return nil | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | func strToIntSlice(s string) []int64 { | ||||||
|  | 	r := strings.Trim(s, "{}") | ||||||
|  | 	a := []int64(nil) | ||||||
|  | 	if r != "NULL" { | ||||||
|  | 		for _, t := range strings.Split(r, ",") { | ||||||
|  | 			i, _ := strconv.ParseInt(t, 10, 64) | ||||||
|  | 			a = append(a, i) | ||||||
|  | 		} | ||||||
|  | 	} | ||||||
|  | 	return a | ||||||
|  | } | ||||||
|  |  | ||||||
		Reference in a new issue
	
	 Matthew Dillon
						Matthew Dillon