From ee6179a28896e14ee132986d451bfb8ec4e06aca Mon Sep 17 00:00:00 2001 From: connorwalsh Date: Sat, 17 Feb 2018 14:53:46 -0500 Subject: [PATCH 1/4] [cw|#11] begin working on poet crud --- server/types/poet.go | 4 ++-- server/types/user.go | 4 +--- 2 files changed, 3 insertions(+), 5 deletions(-) diff --git a/server/types/poet.go b/server/types/poet.go index d6f4cb4..7dafcd4 100644 --- a/server/types/poet.go +++ b/server/types/poet.go @@ -12,7 +12,7 @@ import ( type Poet struct { Id string - God string // the writer of the poet (user) + Designer string // the writer of the poet (user) BirthDate time.Time // so we can show years active DeathDate time.Time // this should be set to null for currently active poets Name string @@ -34,7 +34,7 @@ func (p *Poet) Validate() error { func (*Poet) CreateTable(db *sql.DB) error { mkTableStmt := `CREATE TABLE IF NOT EXISTS poets ( id UUID NOT NULL UNIQUE, - god UUID REFERENCES users NOT NULL, + designer UUID REFERENCES users NOT NULL, birthDate TIMESTAMP WITH TIME ZONE NOT NULL, deathDate TIMESTAMP WITH TIME ZONE, name VARCHAR(255) NOT NULL UNIQUE, diff --git a/server/types/user.go b/server/types/user.go index 54dd60b..fd6e2c9 100644 --- a/server/types/user.go +++ b/server/types/user.go @@ -137,9 +137,7 @@ func (u *User) Read(db *sql.DB) error { } func (u *User) Update(db *sql.DB) error { - // var ( - // err error - // ) + // TODO return nil } From 142d018ba234699dd22f2dc013fd15b5e1865ee8 Mon Sep 17 00:00:00 2001 From: connorwalsh Date: Sat, 17 Feb 2018 17:36:02 -0500 Subject: [PATCH 2/4] [cw|#11] add json tags to poet struct, implement create and read, include basic test coverage --- server/types/main_test.go | 4 ++ server/types/poet.go | 128 ++++++++++++++++++++++++++++++++++---- server/types/poet_test.go | 127 +++++++++++++++++++++++++++++++++++++ server/types/user.go | 4 +- server/types/user_test.go | 9 +-- 5 files changed, 254 insertions(+), 18 deletions(-) create mode 100644 server/types/poet_test.go diff --git a/server/types/main_test.go b/server/types/main_test.go index c51f0e7..d4de63e 100644 --- a/server/types/main_test.go +++ b/server/types/main_test.go @@ -93,3 +93,7 @@ func teardown() error { func TestUserSuite(t *testing.T) { suite.Run(t, &UserTestSuite{db: testDB}) } + +func TestPoetSuite(t *testing.T) { + suite.Run(t, &PoetTestSuite{db: testDB}) +} diff --git a/server/types/poet.go b/server/types/poet.go index 7dafcd4..6b090b2 100644 --- a/server/types/poet.go +++ b/server/types/poet.go @@ -2,7 +2,11 @@ package types import ( "database/sql" + "fmt" "time" + + "github.com/connorwalsh/new-yorken-poesry-magazine/server/utils" + _ "github.com/lib/pq" ) // Notes about poet executables: @@ -11,18 +15,27 @@ import ( // executables will be stored on the filesystem in a safe dir with the path /some/path/bin// type Poet struct { - Id string - Designer string // the writer of the poet (user) - BirthDate time.Time // so we can show years active - DeathDate time.Time // this should be set to null for currently active poets - Name string - Description string - ExecPath string // or possibly a Path, this is the path to the source code + Id string `json:"id"` + Designer string `json:"designer"` // the writer of the poet (user) + BirthDate time.Time `json:"birthDate"` // so we can show years active + DeathDate time.Time `json:"deathDate"` // this should be set to null for currently active poets + Name string `json:"name"` + Description string `json:"description"` + ExecPath string `json:"execPath"` // or possibly a Path, this is the path to the source code // TODO additional statistics: specifically, it would be cool to see the success rate // of a particular poet along with the timeline of how their poems have been recieved + + // what if we also had a poet obituary for when poets are "retired" } -func (p *Poet) Validate() error { +func (p *Poet) Validate(action string) error { + // make sure id, if not an empty string, is a uuid + if !utils.IsValidUUIDV4(p.Id) && p.Id != "" { + return fmt.Errorf("User Id must be a valid uuid, given %s", p.Id) + } + + // TODO ensure that only the user namking the create and delete request can perform + // those actions! return nil } @@ -31,14 +44,22 @@ func (p *Poet) Validate() error { db methods */ -func (*Poet) CreateTable(db *sql.DB) error { +// package level globals for storing prepared sql statements +var ( + poetCreateStmt *sql.Stmt + poetReadStmt *sql.Stmt + poetReadAllStmt *sql.Stmt + poetDeleteStmt *sql.Stmt +) + +func CreatePoetsTable(db *sql.DB) error { mkTableStmt := `CREATE TABLE IF NOT EXISTS poets ( id UUID NOT NULL UNIQUE, designer UUID REFERENCES users NOT NULL, birthDate TIMESTAMP WITH TIME ZONE NOT NULL, - deathDate TIMESTAMP WITH TIME ZONE, + deathDate TIMESTAMP WITH TIME ZONE NOT NULL, name VARCHAR(255) NOT NULL UNIQUE, - description TEXT, + description TEXT NOT NULL, execPath VARCHAR(255) NOT NULL UNIQUE, PRIMARY KEY (id) )` @@ -50,3 +71,88 @@ func (*Poet) CreateTable(db *sql.DB) error { return nil } + +// TODO persist files to the filesystem for poet execs +func (p *Poet) Create(id string, db *sql.DB) error { + var ( + err error + ) + + // assign id + p.Id = id + + // set birthday + p.BirthDate = time.Now().Truncate(time.Millisecond) + + // prepare statement if not already done so. + if poetCreateStmt == nil { + // create statement + stmt := `INSERT INTO poets ( + id, designer, name, birthDate, deathDate, description, execPath + ) VALUES ($1, $2, $3, $4, $5, $6, $7)` + poetCreateStmt, err = db.Prepare(stmt) + if err != nil { + return err + } + } + + _, err = poetCreateStmt.Exec( + p.Id, + p.Designer, + p.Name, + p.BirthDate, + p.DeathDate, + p.Description, + p.ExecPath, + ) + if err != nil { + return err + } + + return nil +} + +func (p *Poet) Read(db *sql.DB) error { + var ( + err error + ) + + // prepare statement if not already done so. + if poetReadStmt == nil { + // read statement + stmt := `SELECT id, designer, name, birthDate, deathDate, description + FROM poets WHERE id = $1` + poetReadStmt, err = db.Prepare(stmt) + if err != nil { + return err + } + } + + // make sure user Id is actually populated + + // run prepared query over arguments + err = poetReadStmt. + QueryRow(p.Id). + Scan(&p.Id, &p.Designer, &p.Name, &p.BirthDate, &p.DeathDate, &p.Description) + switch { + case err == sql.ErrNoRows: + return fmt.Errorf("No poet with id %s", p.Id) + case err != nil: + return err + } + + return nil +} + +func (p *Poet) Delete(db *sql.DB) error { + + return nil +} + +func ReadPoets(db *sql.DB) ([]*Poet, error) { + var ( + poets []*Poet = []*Poet{} + ) + + return poets, nil +} diff --git a/server/types/poet_test.go b/server/types/poet_test.go new file mode 100644 index 0000000..8850cad --- /dev/null +++ b/server/types/poet_test.go @@ -0,0 +1,127 @@ +package types + +import ( + "database/sql" + "path" + "time" + + uuid "github.com/satori/go.uuid" + "github.com/stretchr/testify/suite" +) + +type PoetTestSuite struct { + suite.Suite + db *sql.DB +} + +// run before all tests in this suite begin +func (s *PoetTestSuite) SetupSuite() { + // create users table (referenced by poets) + err := CreateUsersTable(s.db) + if err != nil { + panic(err) + } + + // create poets table + err = CreatePoetsTable(s.db) + if err != nil { + panic(err) + } +} + +// run after all tests in this suite have complete +func (s *PoetTestSuite) TearDownSuite() { + _, err := s.db.Exec(`DROP TABLE IF EXISTS poets CASCADE`) + if err != nil { + panic(err) + } +} + +// run specific setups before specific tests +func (s *PoetTestSuite) BeforeTest(suiteName, testName string) { + var ( + err error + ) + + switch testName { + // drop users table before create table test to see if it works. + case "TestCreateTable": + _, err = s.db.Exec(`DROP TABLE IF EXISTS poets CASCADE`) + if err != nil { + panic(err) + } + } +} + +func (s *PoetTestSuite) TestCreateTable() { + err := CreatePoetsTable(testDB) + s.NoError(err) +} + +func (s *PoetTestSuite) TestCreatePoet() { + userId := uuid.NewV4().String() + poetId := uuid.NewV4().String() + + // create user + user := &User{Username: "3jane", Password: "pwd", Email: "3j4n3@tessier.gov"} + err := user.Create(userId, s.db) + s.NoError(err) + + // create poet + poet := &Poet{ + Designer: userId, + Name: "wintermute", + Description: "mutator of the immutable", + ExecPath: path.Join("/poets/", poetId), + } + + err = poet.Create(poetId, s.db) + s.NoError(err) +} + +func (s *PoetTestSuite) TestReadPoet() { + userId := uuid.NewV4().String() + poetId := uuid.NewV4().String() + + // create user + user := &User{Username: "hamilton", Password: "pwd", Email: "ijk@quaternion.idk"} + err := user.Create(userId, s.db) + s.NoError(err) + + // create poet + poet := &Poet{ + Designer: userId, + Name: "Chum of Chance", + Description: "explorer of some other dimensionality", + ExecPath: path.Join("/poets/", poetId), + } + + err = poet.Create(poetId, s.db) + s.NoError(err) + + expectedPoet := poet + expectedPoet.ExecPath = "" // this should not be public info + + // read poet + poet = &Poet{Id: poetId} + err = poet.Read(s.db) + s.NoError(err) + + // since there isa problem with the postgres and golang time formats w.r.t. + // timezones, we will just compoare the formtted times here and nillify the + // times int he structs -__- + expectedBirthDate := expectedPoet.BirthDate.Format(time.RFC3339) + expectedDeathDate := expectedPoet.DeathDate.Format(time.RFC3339) + birthDate := poet.BirthDate.Format(time.RFC3339) + deathDate := poet.DeathDate.Format(time.RFC3339) + + s.EqualValues(expectedBirthDate, birthDate) + s.EqualValues(expectedDeathDate, deathDate) + + expectedPoet.BirthDate = time.Time{} + expectedPoet.DeathDate = time.Time{} + poet.BirthDate = time.Time{} + poet.DeathDate = time.Time{} + + s.EqualValues(expectedPoet, poet) +} diff --git a/server/types/user.go b/server/types/user.go index fd6e2c9..bbcba8e 100644 --- a/server/types/user.go +++ b/server/types/user.go @@ -57,7 +57,9 @@ var ( userDeleteStmt *sql.Stmt ) -func (*User) CreateTable(db *sql.DB) error { +// TODO refactor this so that is doesn't need a reciever +// aka CreateUsersTable(...) +func CreateUsersTable(db *sql.DB) error { mkTableStmt := `CREATE TABLE IF NOT EXISTS users ( id UUID NOT NULL UNIQUE, username VARCHAR(255) NOT NULL UNIQUE, diff --git a/server/types/user_test.go b/server/types/user_test.go index 4feb912..16879f5 100644 --- a/server/types/user_test.go +++ b/server/types/user_test.go @@ -15,8 +15,7 @@ type UserTestSuite struct { // run before all tests in this suite begin func (s *UserTestSuite) SetupSuite() { // create users table - user := &User{} - err := user.CreateTable(s.db) + err := CreateUsersTable(s.db) if err != nil { panic(err) } @@ -48,7 +47,7 @@ func (s *UserTestSuite) BeforeTest(suiteName, testName string) { if err != nil { panic(err) } - err := (&User{}).CreateTable(testDB) + err := CreateUsersTable(testDB) if err != nil { panic(err) } @@ -56,9 +55,7 @@ func (s *UserTestSuite) BeforeTest(suiteName, testName string) { } func (s *UserTestSuite) TestCreateTable() { - user := &User{} - - err := user.CreateTable(testDB) + err := CreateUsersTable(testDB) s.NoError(err) } From 66574eb545be7edfa141cc8de689f5d73b964547 Mon Sep 17 00:00:00 2001 From: connorwalsh Date: Sat, 17 Feb 2018 18:09:13 -0500 Subject: [PATCH 3/4] [cw|#11] implement read all poets --- server/types/poet.go | 53 +++++++++++++++++++++++++++-- server/types/poet_test.go | 70 ++++++++++++++++++++++++++++++++++++++- server/types/user.go | 3 ++ 3 files changed, 123 insertions(+), 3 deletions(-) diff --git a/server/types/poet.go b/server/types/poet.go index 6b090b2..6e82f0a 100644 --- a/server/types/poet.go +++ b/server/types/poet.go @@ -120,7 +120,7 @@ func (p *Poet) Read(db *sql.DB) error { // prepare statement if not already done so. if poetReadStmt == nil { // read statement - stmt := `SELECT id, designer, name, birthDate, deathDate, description + stmt := `SELECT id, designer, name, birthDate, deathDate, description, execPath FROM poets WHERE id = $1` poetReadStmt, err = db.Prepare(stmt) if err != nil { @@ -133,7 +133,15 @@ func (p *Poet) Read(db *sql.DB) error { // run prepared query over arguments err = poetReadStmt. QueryRow(p.Id). - Scan(&p.Id, &p.Designer, &p.Name, &p.BirthDate, &p.DeathDate, &p.Description) + Scan( + &p.Id, + &p.Designer, + &p.Name, + &p.BirthDate, + &p.DeathDate, + &p.Description, + &p.ExecPath, + ) switch { case err == sql.ErrNoRows: return fmt.Errorf("No poet with id %s", p.Id) @@ -152,7 +160,48 @@ func (p *Poet) Delete(db *sql.DB) error { func ReadPoets(db *sql.DB) ([]*Poet, error) { var ( poets []*Poet = []*Poet{} + err error ) + // prepare statement if not already done so. + if poetReadAllStmt == nil { + // readAll statement + // TODO pagination + stmt := `SELECT id, designer, name, birthDate, deathDate, description, execPath + FROM poets` + poetReadAllStmt, err = db.Prepare(stmt) + if err != nil { + return poets, nil + } + } + + rows, err := poetReadAllStmt.Query() + if err != nil { + return poets, err + } + + defer rows.Close() + for rows.Next() { + poet := &Poet{} + err = rows.Scan( + &poet.Id, + &poet.Designer, + &poet.Name, + &poet.BirthDate, + &poet.DeathDate, + &poet.Description, + &poet.ExecPath, + ) + if err != nil { + return poets, err + } + + // append scanned user into list of all users + poets = append(poets, poet) + } + if err := rows.Err(); err != nil { + return poets, err + } + return poets, nil } diff --git a/server/types/poet_test.go b/server/types/poet_test.go index 8850cad..eee8cf3 100644 --- a/server/types/poet_test.go +++ b/server/types/poet_test.go @@ -50,6 +50,17 @@ func (s *PoetTestSuite) BeforeTest(suiteName, testName string) { if err != nil { panic(err) } + case "TestReadAllPoets": + _, err = s.db.Exec(`DROP TABLE IF EXISTS poets CASCADE`) + if err != nil { + panic(err) + } + + // create poets table + err = CreatePoetsTable(s.db) + if err != nil { + panic(err) + } } } @@ -100,7 +111,6 @@ func (s *PoetTestSuite) TestReadPoet() { s.NoError(err) expectedPoet := poet - expectedPoet.ExecPath = "" // this should not be public info // read poet poet = &Poet{Id: poetId} @@ -125,3 +135,61 @@ func (s *PoetTestSuite) TestReadPoet() { s.EqualValues(expectedPoet, poet) } + +func (s *PoetTestSuite) TestReadAllPoets() { + poetIds := []string{uuid.NewV4().String(), uuid.NewV4().String(), uuid.NewV4().String()} + userId := uuid.NewV4().String() + + // create user + user := &User{Username: "cat-eyed-boy", Password: "pwd", Email: "qt@spooky.jp"} + err := user.Create(userId, s.db) + s.NoError(err) + + // create poets + poets := []*Poet{ + { + Designer: userId, + Name: "ghostA", + Description: "haunts shoes", + ExecPath: path.Join("/poets/", poetIds[0]), + }, + { + Designer: userId, + Name: "ghostB", + Description: "haunts shoe stores", + ExecPath: path.Join("/poets/", poetIds[1]), + }, + { + Designer: userId, + Name: "ghostC", + Description: "isn't a ghost", + ExecPath: path.Join("/poets/", poetIds[2]), + }, + } + + for i := 0; i < len(poetIds); i++ { + err = poets[i].Create(poetIds[i], s.db) + s.NoError(err) + } + + resultPoets, err := ReadPoets(s.db) + s.NoError(err) + for j := 0; j < len(resultPoets); j++ { + // compare formatted string times (since postgres and go have different formats -___-) + s.EqualValues( + poets[j].BirthDate.Format(time.RFC3339), + resultPoets[j].BirthDate.Format(time.RFC3339), + ) + s.EqualValues( + poets[j].DeathDate.Format(time.RFC3339), + resultPoets[j].DeathDate.Format(time.RFC3339), + ) + + resultPoets[j].BirthDate = time.Time{} + resultPoets[j].DeathDate = time.Time{} + poets[j].BirthDate = time.Time{} + poets[j].DeathDate = time.Time{} + } + + s.EqualValues(poets, resultPoets) +} diff --git a/server/types/user.go b/server/types/user.go index bbcba8e..decbdef 100644 --- a/server/types/user.go +++ b/server/types/user.go @@ -135,6 +135,9 @@ func (u *User) Read(db *sql.DB) error { return err } + // TODO ensure that we only allow reading of passwords if the user making the + // request is the user being read. + return nil } From 7eda56ec431f3e7cb3c347436a6ada0e839e682b Mon Sep 17 00:00:00 2001 From: connorwalsh Date: Sat, 17 Feb 2018 19:33:59 -0500 Subject: [PATCH 4/4] [cw|#11] add comment w.r.t. deletes --- server/types/poet.go | 2 ++ 1 file changed, 2 insertions(+) diff --git a/server/types/poet.go b/server/types/poet.go index 6e82f0a..986bafb 100644 --- a/server/types/poet.go +++ b/server/types/poet.go @@ -152,6 +152,8 @@ func (p *Poet) Read(db *sql.DB) error { return nil } +// delete should keep meta about poets in the system along with their poems, but +// should remove all files from the file system and assign a death date. func (p *Poet) Delete(db *sql.DB) error { return nil