commit 9d0ea6071aa7d65cd679d64b2b39e19b08a37187 Author: beorc-gar Date: Sat Sep 19 13:20:25 2020 -0400 adding project to github diff --git a/api/account/member.go b/api/account/member.go new file mode 100644 index 0000000..9e4d761 --- /dev/null +++ b/api/account/member.go @@ -0,0 +1,184 @@ +package account + +import ( + "errors" + "regexp" + + "../database" + "github.com/twinj/uuid" +) + +type Account struct { + Member Member `json:"member"` + User User `json:"user"` + Sso []Sso `json:"sso"` +} + +type Member struct { + Id string `json:"id" db:"id"` + Name string `json:"name" db:"name"` +} + +type User struct { + Id string `json:"id" db:"id"` + Username string `json:"username" db:"username"` + Password string `json:"password" db:"password"` +} + +type Sso struct { + Id string `json:"id" db:"id"` + Token string `json:"token" db:"token"` + SsoTypeId string `json:"ssoTypeId" db:"ssoTypeId"` +} + +type SsoType struct { + Id string `json:"id" db:"id"` + Source string `json:"source" db:"source"` +} + +func Login(account *Account) error { + selectStatement := "SELECT BIN_TO_UUID(`member`.`id`) AS memberId " + + ", `member`.`name` " + + ", COALESCE(BIN_TO_UUID(`user`.`id`), '') AS userId " + + ", COALESCE(`user`.`username`, '') AS username " + + ", COALESCE(`user`.`password`, '') AS password " + + ", COALESCE(BIN_TO_UUID(`sso`.`id`), '') AS ssoId " + + ", COALESCE(`sso`.`token`, '') AS token " + + ", COALESCE(BIN_TO_UUID(`sso`.`ssoTypeId`), '') AS ssoTypeId " + + "FROM `member` " + + "LEFT JOIN `user` ON `member`.`id` = `user`.`memberId` " + + "LEFT JOIN `sso` ON `member`.`id` = `sso`.`memberId` " + + whereClause := "" + vars := make([]interface{}, 2) + errorMessage := "" + + if &account.User != nil && &account.User.Username != nil && &account.User.Password != nil && account.User.Password != "" { + whereClause = "WHERE LOWER(`user`.`username`) = LOWER(?) " + + "AND `user`.`password` = ?" + + vars[0] = account.User.Username + vars[1] = account.User.Password + + errorMessage = "invalid username or password" + account.Sso = make([]Sso, 1) + account.Sso[0] = Sso{} + } else if &account.Sso != nil && len(account.Sso) == 1 && account.Sso[0].Token != "" && &account.Sso[0].SsoTypeId != nil && account.Sso[0].SsoTypeId != "" { + whereClause = "WHERE `sso`.`token` = ? " + + "AND `sso`.`ssoTypeId` = ?" + + vars[0] = account.Sso[0].Token + vars[1] = account.Sso[0].SsoTypeId + errorMessage = "invalid sso credentials" + } else { + return errors.New("account must have either a user or an sso login") + } + + result, err := database.DB.Query(selectStatement+whereClause, vars...) + + if err != nil { + return err + } else if !result.Next() { + return errors.New(errorMessage) + } + + err = result.Scan(&account.Member.Id, &account.Member.Name, &account.User.Id, &account.User.Username, &account.User.Password, &account.Sso[0].Id, &account.Sso[0].Token, &account.Sso[0].SsoTypeId) + return err +} + +func Create(account *Account) error { + if account == nil || &account.Member == nil || account.Member.Name == "" { + return errors.New("account needs to have a name") + } else if &account.User != nil && &account.User.Username != nil && &account.User.Password != nil && account.User.Password != "" { + matched, err := regexp.MatchString("(?:[a-z0-9!#$%&'*+/=?^_`{|}~-]+(?:\\.[a-z0-9!#$%&'*+/=?^_`{|}~-]+)*|\"(?:[\x01-\x08\x0b\x0c\x0e-\x1f\x21\x23-\x5b\x5d-\x7f]|\\[\x01-\x09\x0b\x0c\x0e-\x7f])*\")@(?:(?:[a-z0-9](?:[a-z0-9-]*[a-z0-9])?\\.)+[a-z0-9](?:[a-z0-9-]*[a-z0-9])?|\\[(?:(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\\.){3}(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?|[a-z0-9-]*[a-z0-9]:(?:[\x01-\x08\x0b\x0c\x0e-\x1f\x21-\x5a\x53-\x7f]|\\[\x01-\x09\x0b\x0c\x0e-\x7f])+)\\])", account.User.Username) + if err == nil && matched { + return createViaUser(account) + } else { + return errors.New("username must be an email address") + } + } else if &account.Sso != nil && len(account.Sso) == 1 && account.Sso[0].Token != "" && &account.Sso[0].SsoTypeId != nil && account.Sso[0].SsoTypeId != "" { + return createViaSso(account) + } else { + return errors.New("account must have either a user or an sso login") + } +} + +func createViaUser(account *Account) error { + account.Member.Id = uuid.NewV4().String() + account.User.Id = uuid.NewV4().String() + + exists, err := database.Exists("user", "username", account.User.Username) + if err != nil { + return err + } else if exists { + return errors.New("username unavailable") + } + + err = database.Insert("member", account.Member, nil, nil) + if err != nil { + return err + } + + err = database.Insert("user", account.User, []string{"memberId"}, []interface{}{account.Member.Id}) + + return err +} + +func createViaSso(account *Account) error { + account.Member.Id = uuid.NewV4().String() + account.Sso[0].Id = uuid.NewV4().String() + + result, err := database.DB.Query("SELECT BIN_TO_UUID(`id`) AS id, `source` FROM `ssoType` WHERE `id` = UUID_TO_BIN(?);", account.Sso[0].SsoTypeId) + + if err != nil { + return err + } else if !result.Next() { + return errors.New("no sso type with this id") + } + + result, err = database.DB.Query("SELECT COUNT(`id`) AS c FROM `sso` WHERE `token` = ? AND `ssoTypeId` = UUID_TO_BIN(?);", account.Sso[0].Token, account.Sso[0].SsoTypeId) + if err != nil { + return err + } else if !result.Next() { + return errors.New("query produced no results") + } + + var c int + err = result.Scan(&c) + + if err != nil { + return err + } else if c != 0 { + return errors.New("this sso already exists") + } + + err = database.Insert("member", account.Member, nil, nil) + if err != nil { + return err + } + + err = database.Insert("sso", account.Sso[0], []string{"memberId"}, []interface{}{account.Member.Id}) + + return err +} + +func SsoTypes() ([]SsoType, error) { + result, err := database.DB.Query("SELECT BIN_TO_UUID(`id`) AS id, `source` FROM `ssoType`") + if err != nil { + return nil, err + } + + ssoTypes := make([]SsoType,) + for result.Next() { + + } + + var c int + err = result.Scan(&c) + + if err != nil { + return nil, err + } else if c != 0 { + return errors.New("this sso already exists") + } +} diff --git a/api/database/connection.go b/api/database/connection.go new file mode 100644 index 0000000..086b49b --- /dev/null +++ b/api/database/connection.go @@ -0,0 +1,113 @@ +package database + +import ( + "database/sql" + "errors" + "reflect" + "strings" +) + +var DB *sql.DB + +func Insert(table string, object interface{}, additionalColumns []string, additionalValues []interface{}) error { + return upsert(table, object, false, additionalColumns, additionalValues) +} + +func Update(table string, object interface{}, additionalColumns []string, additionalValues []interface{}) error { + return upsert(table, object, true, additionalColumns, additionalValues) +} + +func upsert(table string, object interface{}, isUpdate bool, additionalColumns []string, additionalValues []interface{}) error { + statement := "INSERT INTO `" + table + "` (" // col1, col2 + values := ") VALUES (" // ?, ? + v := reflect.ValueOf(object) + + if isUpdate { + statement = "UPDATE `" + table + "` SET" + values = "" + } + + for i := 0; i < v.Type().NumField(); i++ { + if v.Field(i).CanInterface() { + additionalColumns = append(additionalColumns, getColumn(v.Type().Field(i))) + additionalValues = append(additionalValues, v.Field(i).Interface()) + } + } + + for i := 0; i < len(additionalColumns); i++ { + if i > 0 { + if !isUpdate { + statement += ", " + } + values += ", " + } + column := additionalColumns[i] + value := "?" + + if column == "id" || strings.HasSuffix(column, "Id") { + value = "UUID_TO_BIN(?)" + } + + if isUpdate { + values += "`" + column + "` = " + value + + if column == "id" { + additionalValues = append(additionalValues, additionalValues[i]) + } + } else { + statement += "`" + column + "`" + values += value + } + + } + if isUpdate { + values += " WHERE `id` = UUID_TO_BIN(?);" + } else { + values += ");" + } + + rows, err := DB.Exec(statement+values, additionalValues...) + if err != nil { + return err + } + + n, err := rows.RowsAffected() + if err != nil { + return err + } else if n != 1 { + operation := "insert to" + if isUpdate { + operation = "update" + } + return errors.New("failed to " + operation + " " + table) + } + + return nil +} + +func getColumn(field reflect.StructField) string { + column, ok := field.Tag.Lookup("db") + if !ok { + column = field.Name + } + return column +} + +func Exists(table string, column string, value interface{}) (bool, error) { + result, err := DB.Query("SELECT COUNT(`id`) AS c FROM `"+table+"` WHERE `"+column+"` = ?;", value) + + if err != nil { + return false, err + } else if !result.Next() { + return false, errors.New("query produced no results") + } + + var c int + err = result.Scan(&c) + + if err != nil { + return false, err + } + + return c > 0, nil +} diff --git a/api/endpoint/account.go b/api/endpoint/account.go new file mode 100644 index 0000000..3a71138 --- /dev/null +++ b/api/endpoint/account.go @@ -0,0 +1,65 @@ +package endpoint + +import ( + "encoding/json" + "net/http" + + "../account" + "../util" +) + +//TODO do not return passwords and sso tokens + +func CreateAccount(w http.ResponseWriter, req *http.Request) { + var acc account.Account + + err := json.NewDecoder(req.Body).Decode(&acc) + + if err != nil { + util.ErrorResponse(err, w) + return + } + + err = account.Create(&acc) + + if err != nil { + util.ErrorResponse(err, w) + return + } + + jsonData, err := json.Marshal(acc) + + if err != nil { + util.ErrorResponse(err, w) + return + } + + w.Write(jsonData) +} + +func Login(w http.ResponseWriter, req *http.Request) { + var acc account.Account + + err := json.NewDecoder(req.Body).Decode(&acc) + + if err != nil { + util.ErrorResponse(err, w) + return + } + + err = account.Login(&acc) + + if err != nil { + util.ErrorResponse(err, w) + return + } + + jsonData, err := json.Marshal(acc) + + if err != nil { + util.ErrorResponse(err, w) + return + } + + w.Write(jsonData) +} diff --git a/api/league/league.go b/api/league/league.go new file mode 100644 index 0000000..23b07e3 --- /dev/null +++ b/api/league/league.go @@ -0,0 +1,56 @@ +package league + +import ( + "errors" + + "../database" + "github.com/twinj/uuid" +) + +type League struct { + Id string `json:"id"` + Name string `json:"name"` + Public bool `json:"public"` +} + +func createLeague(league *League) error { + if league == nil || &league.Name == nil || league.Name == "" { + return errors.New("league needs to have a name") + } + + //todo use db.exists + result, err := database.DB.Query("SELECT COUNT(`id`) AS c FROM `league` WHERE `name` = ?;", league.Name) + + if err != nil { + return err + } else if !result.Next() { + return errors.New("query produced no results") + } + + var c int + err = result.Scan(&c) + + if err != nil { + return err + } else if c != 0 { + return errors.New("league name unavailable") + } + + league.Id = uuid.NewV4().String() + if &league.Public == nil { + league.Public = true + } + + //todo use db.insert + rows, err := database.DB.Exec("INSERT INTO `league` (`id`, `name`, `public`) VALUES (UUID_TO_BIN(?), ?, ?);", league.Id, league.Name, league.Public) + if err != nil { + return err + } + + n, err := rows.RowsAffected() + if err != nil { + return err + } else if n != 1 { + return errors.New("failed to insert member") + } +} diff --git a/api/main.go b/api/main.go new file mode 100644 index 0000000..33f733b --- /dev/null +++ b/api/main.go @@ -0,0 +1,28 @@ +package main + +import ( + "database/sql" + "net/http" + + "./database" + "./endpoint" + _ "github.com/go-sql-driver/mysql" + "github.com/gorilla/mux" +) + +func main() { + //todo get connection string based on zone + db, err := sql.Open("mysql", "root:Se4Q2Lp-3587@tcp(localhost)/baseball") + if err != nil { + //todo handle it (fatal, can't continue.) + } + database.DB = db + defer database.DB.Close() + + router := mux.NewRouter() + + router.HandleFunc("/account/create", endpoint.CreateAccount).Methods("POST") + router.HandleFunc("/account/login", endpoint.Login).Methods("POST") + + http.ListenAndServe(":8080", router) +} diff --git a/api/util/log.go b/api/util/log.go new file mode 100644 index 0000000..0fe192a --- /dev/null +++ b/api/util/log.go @@ -0,0 +1,12 @@ +package util + +import ( + "net/http" + "strings" +) + +func ErrorResponse(err error, w http.ResponseWriter) { + if err != nil { + w.Write([]byte("{\"error\":\"" + strings.Replace(err.Error(), "\"", "\\\"", -1) + "\"}")) + } +} diff --git a/data/README.md b/data/README.md new file mode 100644 index 0000000..443362e --- /dev/null +++ b/data/README.md @@ -0,0 +1,13 @@ +# How to use these files + +Each `sql` script in this folder needs to be executed using the appropriate database. +Due to foreign key constraints, a proper order must be followed. +`_MASTER_.sql` has the proper order, and can be executed like so: + +```sql +use baseball; +source /path/to/_MASTER_.sql; +``` + +Which will then execute each script in order. +Then, verify that each statement was successful. \ No newline at end of file diff --git a/data/_MASTER_.sql b/data/_MASTER_.sql new file mode 100644 index 0000000..4510440 --- /dev/null +++ b/data/_MASTER_.sql @@ -0,0 +1,19 @@ +source C:\Users\Bronson\Desktop\baseballApp\data\league.sql +source C:\Users\Bronson\Desktop\baseballApp\data\member.sql +source C:\Users\Bronson\Desktop\baseballApp\data\role.sql +source C:\Users\Bronson\Desktop\baseballApp\data\leagueMember.sql +source C:\Users\Bronson\Desktop\baseballApp\data\team.sql +source C:\Users\Bronson\Desktop\baseballApp\data\position.sql +source C:\Users\Bronson\Desktop\baseballApp\data\player.sql +source C:\Users\Bronson\Desktop\baseballApp\data\teamPlayer.sql +source C:\Users\Bronson\Desktop\baseballApp\data\trade.sql +source C:\Users\Bronson\Desktop\baseballApp\data\injury.sql +source C:\Users\Bronson\Desktop\baseballApp\data\invitation.sql +source C:\Users\Bronson\Desktop\baseballApp\data\statistic.sql +source C:\Users\Bronson\Desktop\baseballApp\data\draft.sql +source C:\Users\Bronson\Desktop\baseballApp\data\draftPick.sql +source C:\Users\Bronson\Desktop\baseballApp\data\notificationTemplate.sql +source C:\Users\Bronson\Desktop\baseballApp\data\notification.sql +source C:\Users\Bronson\Desktop\baseballApp\data\user.sql +source C:\Users\Bronson\Desktop\baseballApp\data\ssoType.sql +source C:\Users\Bronson\Desktop\baseballApp\data\sso.sql \ No newline at end of file diff --git a/data/draft.sql b/data/draft.sql new file mode 100644 index 0000000..88b552e --- /dev/null +++ b/data/draft.sql @@ -0,0 +1,29 @@ +CREATE TABLE IF NOT EXISTS `draft` ( + `id` BINARY(16) PRIMARY KEY, + `leagueId` BINARY(16) NOT NULL, + `turn` BINARY(16) NOT NULL, -- this is the leagueMemberId of whose turn it is. not foreign key so if the member is removed, we can go: this person doesn't exist, so it's the next member after. + + `created` DATETIME, + `modified` DATETIME, + + FOREIGN KEY(`leagueId`) REFERENCES `league`(`id`) ON DELETE CASCADE +); + +DROP TRIGGER IF EXISTS `insertDraftTrigger`; +DELIMITER $$ +CREATE TRIGGER `insertDraftTrigger` BEFORE INSERT ON `draft` +FOR EACH ROW + BEGIN + SET NEW.`created` = IFNULL(NEW.`created`, NOW()); + END$$ +DELIMITER ; + +DROP TRIGGER IF EXISTS `updateDraftTrigger`; +DELIMITER $$ +CREATE TRIGGER `updateDraftTrigger` BEFORE UPDATE ON `draft` +FOR EACH ROW + BEGIN + SET NEW.`created` = OLD.`created`; + SET NEW.`modified` = NOW(); + END$$ +DELIMITER ; \ No newline at end of file diff --git a/data/draftPick.sql b/data/draftPick.sql new file mode 100644 index 0000000..ccd09df --- /dev/null +++ b/data/draftPick.sql @@ -0,0 +1,34 @@ +CREATE TABLE IF NOT EXISTS `draftPick` ( + `id` BINARY(16) PRIMARY KEY, + `draftId` BINARY(16) NOT NULL, + `leagueMemberId` BINARY(16) NOT NULL, + `order` INT(11) NOT NULL, + + `created` DATETIME, + `modified` DATETIME, + + UNIQUE (`draftId`, `leagueMemberId`), + UNIQUE (`leagueMemberId`, `order`), + + FOREIGN KEY(`draftId`) REFERENCES `draft`(`id`) ON DELETE CASCADE, + FOREIGN KEY(`leagueMemberId`) REFERENCES `leagueMember`(`id`) ON DELETE CASCADE +); + +DROP TRIGGER IF EXISTS `insertDraftPickTrigger`; +DELIMITER $$ +CREATE TRIGGER `insertDraftPickTrigger` BEFORE INSERT ON `draftPick` +FOR EACH ROW + BEGIN + SET NEW.`created` = IFNULL(NEW.`created`, NOW()); + END$$ +DELIMITER ; + +DROP TRIGGER IF EXISTS `updateDraftPickTrigger`; +DELIMITER $$ +CREATE TRIGGER `updateDraftPickTrigger` BEFORE UPDATE ON `draftPick` +FOR EACH ROW + BEGIN + SET NEW.`created` = OLD.`created`; + SET NEW.`modified` = NOW(); + END$$ +DELIMITER ; \ No newline at end of file diff --git a/data/injury.sql b/data/injury.sql new file mode 100644 index 0000000..c9f3bfb --- /dev/null +++ b/data/injury.sql @@ -0,0 +1,30 @@ +CREATE TABLE IF NOT EXISTS `injury` ( + `id` BINARY(16) PRIMARY KEY, + `playerId` BINARY(16) NOT NULL, + `reason` VARCHAR(1024), -- this and below are from fantasybaseballnerd.com + `notes` VARCHAR(2048), + + `created` DATETIME, + `modified` DATETIME, + + FOREIGN KEY(`playerId`) REFERENCES `player`(`id`) ON DELETE CASCADE +); + +DROP TRIGGER IF EXISTS `insertInjuryTrigger`; +DELIMITER $$ +CREATE TRIGGER `insertInjuryTrigger` BEFORE INSERT ON `injury` +FOR EACH ROW + BEGIN + SET NEW.`created` = IFNULL(NEW.`created`, NOW()); + END$$ +DELIMITER ; + +DROP TRIGGER IF EXISTS `updateInjuryTrigger`; +DELIMITER $$ +CREATE TRIGGER `updateInjuryTrigger` BEFORE UPDATE ON `injury` +FOR EACH ROW + BEGIN + SET NEW.`created` = OLD.`created`; + SET NEW.`modified` = NOW(); + END$$ +DELIMITER ; \ No newline at end of file diff --git a/data/invitation.sql b/data/invitation.sql new file mode 100644 index 0000000..a6990dc --- /dev/null +++ b/data/invitation.sql @@ -0,0 +1,33 @@ +CREATE TABLE IF NOT EXISTS `invitation` ( + `id` BINARY(16) PRIMARY KEY, + `leagueId` BINARY(16) NOT NULL, + `memberId` BINARY(16) NOT NULL, + `isRequest` BOOLEAN NOT NULL, -- true if member asked to join league, false if league invited member. + + `created` DATETIME, + `modified` DATETIME, + + UNIQUE (`leagueId`, `memberId`), + + FOREIGN KEY(`leagueId`) REFERENCES `league`(`id`) ON DELETE CASCADE, + FOREIGN KEY(`memberId`) REFERENCES `member`(`id`) ON DELETE CASCADE +); + +DROP TRIGGER IF EXISTS `insertInvitationTrigger`; +DELIMITER $$ +CREATE TRIGGER `insertInvitationTrigger` BEFORE INSERT ON `invitation` +FOR EACH ROW + BEGIN + SET NEW.`created` = IFNULL(NEW.`created`, NOW()); + END$$ +DELIMITER ; + +DROP TRIGGER IF EXISTS `updateInvitationTrigger`; +DELIMITER $$ +CREATE TRIGGER `updateInvitationTrigger` BEFORE UPDATE ON `invitation` +FOR EACH ROW + BEGIN + SET NEW.`created` = OLD.`created`; + SET NEW.`modified` = NOW(); + END$$ +DELIMITER ; \ No newline at end of file diff --git a/data/league.sql b/data/league.sql new file mode 100644 index 0000000..c907a8f --- /dev/null +++ b/data/league.sql @@ -0,0 +1,27 @@ +CREATE TABLE IF NOT EXISTS `league` ( + `id` BINARY(16) PRIMARY KEY, + `name` VARCHAR(64) NOT NULL UNIQUE, + `public` BOOLEAN NOT NULL, + + `created` DATETIME, + `modified` DATETIME +); + +DROP TRIGGER IF EXISTS `insertLeagueTrigger`; +DELIMITER $$ +CREATE TRIGGER `insertLeagueTrigger` BEFORE INSERT ON `league` +FOR EACH ROW + BEGIN + SET NEW.`created` = IFNULL(NEW.`created`, NOW()); + END$$ +DELIMITER ; + +DROP TRIGGER IF EXISTS `updateLeagueTrigger`; +DELIMITER $$ +CREATE TRIGGER `updateLeagueTrigger` BEFORE UPDATE ON `league` +FOR EACH ROW + BEGIN + SET NEW.`created` = OLD.`created`; + SET NEW.`modified` = NOW(); + END$$ +DELIMITER ; \ No newline at end of file diff --git a/data/leagueMember.sql b/data/leagueMember.sql new file mode 100644 index 0000000..01de2a3 --- /dev/null +++ b/data/leagueMember.sql @@ -0,0 +1,35 @@ +CREATE TABLE IF NOT EXISTS `leagueMember` ( + `id` BINARY(16) PRIMARY KEY, + `leagueId` BINARY(16) NOT NULL, + `memberId` BINARY(16) NOT NULL, + `nickName` VARCHAR(64), + `roleId` BINARY(16) NOT NULL, + + `created` DATETIME, + `modified` DATETIME, + + UNIQUE (`leagueId`, `memberId`), + + FOREIGN KEY(`leagueId`) REFERENCES `league`(`id`) ON DELETE CASCADE, + FOREIGN KEY(`memberId`) REFERENCES `member`(`id`) ON DELETE CASCADE, + FOREIGN KEY(`roleId`) REFERENCES `role`(`id`) +); + +DROP TRIGGER IF EXISTS `insertLeagueMemberTrigger`; +DELIMITER $$ +CREATE TRIGGER `insertLeagueMemberTrigger` BEFORE INSERT ON `leagueMember` +FOR EACH ROW + BEGIN + SET NEW.`created` = IFNULL(NEW.`created`, NOW()); + END$$ +DELIMITER ; + +DROP TRIGGER IF EXISTS `updateLeagueMemberTrigger`; +DELIMITER $$ +CREATE TRIGGER `updateLeagueMemberTrigger` BEFORE UPDATE ON `leagueMember` +FOR EACH ROW + BEGIN + SET NEW.`created` = OLD.`created`; + SET NEW.`modified` = NOW(); + END$$ +DELIMITER ; \ No newline at end of file diff --git a/data/member.sql b/data/member.sql new file mode 100644 index 0000000..58cfc2b --- /dev/null +++ b/data/member.sql @@ -0,0 +1,26 @@ +CREATE TABLE IF NOT EXISTS `member` ( + `id` BINARY(16) PRIMARY KEY, + `name` VARCHAR(64) NOT NULL, + + `created` DATETIME, + `modified` DATETIME +); + +DROP TRIGGER IF EXISTS `insertMemberTrigger`; +DELIMITER $$ +CREATE TRIGGER `insertMemberTrigger` BEFORE INSERT ON `member` +FOR EACH ROW + BEGIN + SET NEW.`created` = IFNULL(NEW.`created`, NOW()); + END$$ +DELIMITER ; + +DROP TRIGGER IF EXISTS `updateMemberTrigger`; +DELIMITER $$ +CREATE TRIGGER `updateMemberTrigger` BEFORE UPDATE ON `member` +FOR EACH ROW + BEGIN + SET NEW.`created` = OLD.`created`; + SET NEW.`modified` = NOW(); + END$$ +DELIMITER ; \ No newline at end of file diff --git a/data/notification.sql b/data/notification.sql new file mode 100644 index 0000000..29fc469 --- /dev/null +++ b/data/notification.sql @@ -0,0 +1,34 @@ +CREATE TABLE IF NOT EXISTS `notification` ( + `id` BINARY(16) PRIMARY KEY, + `templateId` BINARY(16) NOT NULL, + `leagueMemberId` BINARY(16) NOT NULL, + `referenceId` BINARY(16), -- used to link to injury, invitation, etc. + `reference` VARCHAR(128), -- describe what the referenceId is referring to + + `created` DATETIME, + `modified` DATETIME, + + UNIQUE (`templateId`, `leagueMemberId`, `referenceId`), + + FOREIGN KEY(`templateId`) REFERENCES `notificationTemplate`(`id`) ON DELETE CASCADE, + FOREIGN KEY(`leagueMemberId`) REFERENCES `leagueMember`(`id`) ON DELETE CASCADE +); + +DROP TRIGGER IF EXISTS `insertNotificationTrigger`; +DELIMITER $$ +CREATE TRIGGER `insertNotificationTrigger` BEFORE INSERT ON `notification` +FOR EACH ROW + BEGIN + SET NEW.`created` = IFNULL(NEW.`created`, NOW()); + END$$ +DELIMITER ; + +DROP TRIGGER IF EXISTS `updateNotificationTrigger`; +DELIMITER $$ +CREATE TRIGGER `updateNotificationTrigger` BEFORE UPDATE ON `notification` +FOR EACH ROW + BEGIN + SET NEW.`created` = OLD.`created`; + SET NEW.`modified` = NOW(); + END$$ +DELIMITER ; \ No newline at end of file diff --git a/data/notificationTemplate.sql b/data/notificationTemplate.sql new file mode 100644 index 0000000..7aceaf0 --- /dev/null +++ b/data/notificationTemplate.sql @@ -0,0 +1,14 @@ +CREATE TABLE IF NOT EXISTS `notificationTemplate` ( + `id` BINARY(16) PRIMARY KEY, + `code` VARCHAR(16) UNIQUE, + `title` VARCHAR(256), + `message` VARCHAR(2048) +); + +INSERT IGNORE INTO `notificationTemplate` (`id`, `code`, `title`, `message`) VALUES +(UUID_TO_BIN(UUID()), 'YOUR-TURN', "It's your turn to pick!", "Why don't you go on ahead and make the next addition to your team?"), -- referenceId will be draftPick +(UUID_TO_BIN(UUID()), 'INJURED-PLAYER', "Your player was injured.", "Unfortunately, one of your players has went and gotten himself injured."), -- referenceId will be injury +(UUID_TO_BIN(UUID()), 'INACTIVE-PLAYER', "Your player is no longer available.", "Unfortunately, one of your players has retired, or was demoted, or something."), -- referenceId will be player +(UUID_TO_BIN(UUID()), 'INACTIVE-PLAYER', "Your player is no longer available.", "Unfortunately, one of your players has retired, or was demoted, or something."), -- referenceId will be player +(UUID_TO_BIN(UUID()), 'LEAGUE-REQUEST', "Someone wants to join your league.", "You probably shouldn't leave them hanging."), -- referenceId will be invitation +(UUID_TO_BIN(UUID()), 'LEAGUE-INVITE', "Someone wants you to join their league.", "You probably shouldn't leave them hanging."); -- referenceId will be invitation diff --git a/data/player.sql b/data/player.sql new file mode 100644 index 0000000..0e6a925 --- /dev/null +++ b/data/player.sql @@ -0,0 +1,39 @@ +CREATE TABLE IF NOT EXISTS `player` ( + `id` BINARY(16) PRIMARY KEY, + `sourceId` INT(11) UNIQUE, -- player's id on fantasybaseballnerd.com + `name` VARCHAR(64) NOT NULL, -- this and below is data from fantasybaseballnerd.com + `positionId` BINARY(16) NOT NULL, + `team` VARCHAR(3), + `bats` VARCHAR(1), + `throws` VARCHAR(1), + `height` VARCHAR(5), + `weight` INT(3), + `jersey` INT(2), + `birthDate` DATE, + + `active` BOOLEAN, -- when the player isn't returned by api anymore, set to true + + `created` DATETIME, + `modified` DATETIME, + + FOREIGN KEY(`positionId`) REFERENCES `position`(`id`) +); + +DROP TRIGGER IF EXISTS `insertPlayerTrigger`; +DELIMITER $$ +CREATE TRIGGER `insertPlayerTrigger` BEFORE INSERT ON `player` +FOR EACH ROW + BEGIN + SET NEW.`created` = IFNULL(NEW.`created`, NOW()); + END$$ +DELIMITER ; + +DROP TRIGGER IF EXISTS `updatePlayerTrigger`; +DELIMITER $$ +CREATE TRIGGER `updatePlayerTrigger` BEFORE UPDATE ON `player` +FOR EACH ROW + BEGIN + SET NEW.`created` = OLD.`created`; + SET NEW.`modified` = NOW(); + END$$ +DELIMITER ; \ No newline at end of file diff --git a/data/position.sql b/data/position.sql new file mode 100644 index 0000000..956194c --- /dev/null +++ b/data/position.sql @@ -0,0 +1,21 @@ +CREATE TABLE IF NOT EXISTS `position` ( + `id` BINARY(16) PRIMARY KEY, + `code` VARCHAR(2) UNIQUE, + `name` VARCHAR(64) NOT NULL UNIQUE +); + +INSERT IGNORE INTO `position` (`id`, `code`, `name`) VALUES +(UUID_TO_BIN(UUID()), 'DH', 'designated hitter'), +(UUID_TO_BIN(UUID()), 'C', 'catcher'), +(UUID_TO_BIN(UUID()), 'P', 'pitcher'), +(UUID_TO_BIN(UUID()), 'RP', 'relief pitcher'), +(UUID_TO_BIN(UUID()), 'SP', 'starting pitcher'), +(UUID_TO_BIN(UUID()), 'IF', 'in field'), +(UUID_TO_BIN(UUID()), '1B', 'first base'), +(UUID_TO_BIN(UUID()), '2B', 'second base'), +(UUID_TO_BIN(UUID()), 'SS', 'short stop'), +(UUID_TO_BIN(UUID()), '3B', 'third base'), +(UUID_TO_BIN(UUID()), 'OF', 'out field'), +(UUID_TO_BIN(UUID()), 'LF', 'left field'), +(UUID_TO_BIN(UUID()), 'RF', 'right field'), +(UUID_TO_BIN(UUID()), 'CF', 'center field'); \ No newline at end of file diff --git a/data/role.sql b/data/role.sql new file mode 100644 index 0000000..a3f382c --- /dev/null +++ b/data/role.sql @@ -0,0 +1,11 @@ +CREATE TABLE IF NOT EXISTS `role` ( + `id` BINARY(16) PRIMARY KEY, + `name` VARCHAR(64) NOT NULL UNIQUE +); + +INSERT IGNORE INTO `role` (`id`, `name`) VALUES +(UUID_TO_BIN(UUID()), 'owner'), +(UUID_TO_BIN(UUID()), 'admin'), +(UUID_TO_BIN(UUID()), 'advanced'), -- someone who can manage their own stuff but no one else's. +(UUID_TO_BIN(UUID()), 'member'), +(UUID_TO_BIN(UUID()), 'spectator'); \ No newline at end of file diff --git a/data/sso.sql b/data/sso.sql new file mode 100644 index 0000000..c9f9ffe --- /dev/null +++ b/data/sso.sql @@ -0,0 +1,33 @@ +CREATE TABLE IF NOT EXISTS `sso` ( + `id` BINARY(16) PRIMARY KEY, + `memberId` BINARY(16) NOT NULL, + `token` VARCHAR(64) NOT NULL, + `ssoTypeId` BINARY(16) NOT NULL, + + `created` DATETIME, + `modified` DATETIME, + + UNIQUE (`token`, `ssoTypeId`), + + FOREIGN KEY(`memberId`) REFERENCES `member`(`id`) ON DELETE CASCADE, + FOREIGN KEY(`ssoTypeId`) REFERENCES `ssoType`(`id`) ON DELETE CASCADE +); + +DROP TRIGGER IF EXISTS `insertSsoTrigger`; +DELIMITER $$ +CREATE TRIGGER `insertSsoTrigger` BEFORE INSERT ON `sso` +FOR EACH ROW + BEGIN + SET NEW.`created` = IFNULL(NEW.`created`, NOW()); + END$$ +DELIMITER ; + +DROP TRIGGER IF EXISTS `updateSsoTrigger`; +DELIMITER $$ +CREATE TRIGGER `updateSsoTrigger` BEFORE UPDATE ON `sso` +FOR EACH ROW + BEGIN + SET NEW.`created` = OLD.`created`; + SET NEW.`modified` = NOW(); + END$$ +DELIMITER ; \ No newline at end of file diff --git a/data/ssoType.sql b/data/ssoType.sql new file mode 100644 index 0000000..7aaf652 --- /dev/null +++ b/data/ssoType.sql @@ -0,0 +1,8 @@ +CREATE TABLE IF NOT EXISTS `ssoType` ( + `id` BINARY(16) PRIMARY KEY, + `source` VARCHAR(32) UNIQUE +); + +INSERT IGNORE INTO `ssoType` (`id`, `source`) VALUES +(UUID_TO_BIN(UUID()), 'Google'), +(UUID_TO_BIN(UUID()), 'Facebook'); \ No newline at end of file diff --git a/data/statistic.sql b/data/statistic.sql new file mode 100644 index 0000000..5d23d6a --- /dev/null +++ b/data/statistic.sql @@ -0,0 +1,40 @@ +CREATE TABLE IF NOT EXISTS `statistic` ( + `id` BINARY(16) PRIMARY KEY, + `playerId` BINARY(16) NOT NULL, + `rank` INT(11), -- this and below are from fantasybaseballnerd.com + `averageRank` REAL(3,3), + `atBats` INT(11), + `runs` INT(11), + `homeRuns` INT(11), + `runsBattedIn` INT(11), + `stolenBases` INT(11), + `battingAverage` REAL(3,3), + `onBasePlusSlugging` REAL(3,3), + `singles` INT(11), + `doubles` INT(11), + `triples` INT(11), + + `created` DATETIME, + `modified` DATETIME, + + FOREIGN KEY(`playerId`) REFERENCES `player`(`id`) ON DELETE CASCADE +); + +DROP TRIGGER IF EXISTS `insertStatisticTrigger`; +DELIMITER $$ +CREATE TRIGGER `insertStatisticTrigger` BEFORE INSERT ON `statistic` +FOR EACH ROW + BEGIN + SET NEW.`created` = IFNULL(NEW.`created`, NOW()); + END$$ +DELIMITER ; + +DROP TRIGGER IF EXISTS `updateStatisticTrigger`; +DELIMITER $$ +CREATE TRIGGER `updateStatisticTrigger` BEFORE UPDATE ON `statistic` +FOR EACH ROW + BEGIN + SET NEW.`created` = OLD.`created`; + SET NEW.`modified` = NOW(); + END$$ +DELIMITER ; \ No newline at end of file diff --git a/data/team.sql b/data/team.sql new file mode 100644 index 0000000..9835664 --- /dev/null +++ b/data/team.sql @@ -0,0 +1,29 @@ +CREATE TABLE IF NOT EXISTS `team` ( + `id` BINARY(16) PRIMARY KEY, + `name` VARCHAR(64) NOT NULL, + `leagueMemberId` BINARY(16) NOT NULL, + + `created` DATETIME, + `modified` DATETIME, + + FOREIGN KEY(`leagueMemberId`) REFERENCES `leagueMember`(`id`) ON DELETE CASCADE +); + +DROP TRIGGER IF EXISTS `insertTeamTrigger`; +DELIMITER $$ +CREATE TRIGGER `insertTeamTrigger` BEFORE INSERT ON `team` +FOR EACH ROW + BEGIN + SET NEW.`created` = IFNULL(NEW.`created`, NOW()); + END$$ +DELIMITER ; + +DROP TRIGGER IF EXISTS `updateTeamTrigger`; +DELIMITER $$ +CREATE TRIGGER `updateTeamTrigger` BEFORE UPDATE ON `team` +FOR EACH ROW + BEGIN + SET NEW.`created` = OLD.`created`; + SET NEW.`modified` = NOW(); + END$$ +DELIMITER ; \ No newline at end of file diff --git a/data/teamPlayer.sql b/data/teamPlayer.sql new file mode 100644 index 0000000..8d0a6c7 --- /dev/null +++ b/data/teamPlayer.sql @@ -0,0 +1,31 @@ +CREATE TABLE IF NOT EXISTS `teamPlayer` ( + `id` BINARY(16) PRIMARY KEY, + `teamId` BINARY(16) NOT NULL, + `playerId` BINARY(16) NOT NULL, + + `created` DATETIME, + `modified` DATETIME, + + UNIQUE (`teamId`, `playerId`), + FOREIGN KEY(`teamId`) REFERENCES `team`(`id`) ON DELETE CASCADE, + FOREIGN KEY(`playerId`) REFERENCES `player`(`id`) ON DELETE CASCADE +); + +DROP TRIGGER IF EXISTS `insertTeamPlayerTrigger`; +DELIMITER $$ +CREATE TRIGGER `insertTeamPlayerTrigger` BEFORE INSERT ON `teamPlayer` +FOR EACH ROW + BEGIN + SET NEW.`created` = IFNULL(NEW.`created`, NOW()); + END$$ +DELIMITER ; + +DROP TRIGGER IF EXISTS `updateTeamPlayerTrigger`; +DELIMITER $$ +CREATE TRIGGER `updateTeamPlayerTrigger` BEFORE UPDATE ON `teamPlayer` +FOR EACH ROW + BEGIN + SET NEW.`created` = OLD.`created`; + SET NEW.`modified` = NOW(); + END$$ +DELIMITER ; \ No newline at end of file diff --git a/data/trade.sql b/data/trade.sql new file mode 100644 index 0000000..92ed54e --- /dev/null +++ b/data/trade.sql @@ -0,0 +1,32 @@ +CREATE TABLE IF NOT EXISTS `trade` ( + `id` BINARY(16) PRIMARY KEY, + `teamId` BINARY(16) NOT NULL, + `oldPlayerId` BINARY(16) NOT NULL, + `newPlayerId` BINARY(16) NOT NULL, + + `created` DATETIME, + `modified` DATETIME, + + FOREIGN KEY(`teamId`) REFERENCES `team`(`id`) ON DELETE CASCADE, + FOREIGN KEY(`oldPlayerId`) REFERENCES `player`(`id`) ON DELETE CASCADE, + FOREIGN KEY(`newPlayerId`) REFERENCES `player`(`id`) ON DELETE CASCADE +); + +DROP TRIGGER IF EXISTS `insertTradeTrigger`; +DELIMITER $$ +CREATE TRIGGER `insertTradeTrigger` BEFORE INSERT ON `trade` +FOR EACH ROW + BEGIN + SET NEW.`created` = IFNULL(NEW.`created`, NOW()); + END$$ +DELIMITER ; + +DROP TRIGGER IF EXISTS `updateTradeTrigger`; +DELIMITER $$ +CREATE TRIGGER `updateTradeTrigger` BEFORE UPDATE ON `trade` +FOR EACH ROW + BEGIN + SET NEW.`created` = OLD.`created`; + SET NEW.`modified` = NOW(); + END$$ +DELIMITER ; \ No newline at end of file diff --git a/data/user.sql b/data/user.sql new file mode 100644 index 0000000..db560be --- /dev/null +++ b/data/user.sql @@ -0,0 +1,31 @@ +CREATE TABLE IF NOT EXISTS `user` ( + `id` BINARY(16) PRIMARY KEY, + `username` VARCHAR(254) NOT NULL UNIQUE, + `password` BINARY(60), + `memberId` BINARY(16) NOT NULL UNIQUE, + `verified` TINYINT(1), -- 0 when they first create the account, 1 once they verify the email address + + `created` DATETIME, + `modified` DATETIME, + + FOREIGN KEY(`memberId`) REFERENCES `member`(`id`) ON DELETE CASCADE +); + +DROP TRIGGER IF EXISTS `insertUserTrigger`; +DELIMITER $$ +CREATE TRIGGER `insertUserTrigger` BEFORE INSERT ON `user` +FOR EACH ROW + BEGIN + SET NEW.`created` = IFNULL(NEW.`created`, NOW()); + END$$ +DELIMITER ; + +DROP TRIGGER IF EXISTS `updateUserTrigger`; +DELIMITER $$ +CREATE TRIGGER `updateUserTrigger` BEFORE UPDATE ON `user` +FOR EACH ROW + BEGIN + SET NEW.`created` = OLD.`created`; + SET NEW.`modified` = NOW(); + END$$ +DELIMITER ; \ No newline at end of file diff --git a/docs/endpoints.txt b/docs/endpoints.txt new file mode 100644 index 0000000..9863cab --- /dev/null +++ b/docs/endpoints.txt @@ -0,0 +1,20 @@ +account.create + +league.create +league.invite +league.getMembers (incl role) + +member.requestToJoin +member.getLeagues + +leagueMember.getTeam + +draft.pick +draft.getTurn + +team.getPlayers (incl position, stats, injury) + +notification.create + + +start with creating an account and logging in. \ No newline at end of file diff --git a/docs/notes.txt b/docs/notes.txt new file mode 100644 index 0000000..e67ed99 --- /dev/null +++ b/docs/notes.txt @@ -0,0 +1,23 @@ +Will have to deal with when a player is no longer returned by fantasybaseballnerd, but is in use in peoples teams. +Some examples of when this would happen: a player retires, a player is demoted out of mlb. + +Will have to deal with when a league owner deletes their account or league + +what to do if any of the things are deleted that have a reference id in notification table + +todo items (not a priority): + player photos + trading + auto score keeping + divisions (split league into different groups for places) + +notifications that will exist: + your turn to draft + your player was injured + someone wants to join your league + someone invited you to join their league + your player is inactive + +automated processes: + refreshing players and their stats, setting players inactive: notification + populating injuries: notification diff --git a/docs/testing_setup.txt b/docs/testing_setup.txt new file mode 100644 index 0000000..a53325c --- /dev/null +++ b/docs/testing_setup.txt @@ -0,0 +1,12 @@ +How to Setup the Testing Environment on Localhost + +run MySQL Installer and configure server +go to last option and apply configuration. This will start the server. +Run SQLyog and connect to the localhost. +if object browser isnt showing up, hit Ctrl + B +open mysql command line client and run `use baseball; source path\to\_MASTER_.sql` + + +go get github.com/go-sql-driver/mysql +go get github.com/gorilla/mux +go get github.com/twinj/uuid \ No newline at end of file