From 733105b8b5fdb28437a09ccfb87569cbe061b99e Mon Sep 17 00:00:00 2001 From: GetPsyched Date: Tue, 3 Oct 2023 02:10:10 +0530 Subject: [PATCH] feat: use pgx instead of lib/pq --- database/init.go | 8 +++-- go.mod | 9 ++++-- go.sum | 28 ++++++++++++++---- handlers/announcement.go | 28 +++++++++--------- handlers/club.go | 63 ++++++++++++++++++++-------------------- handlers/course.go | 26 ++++++++--------- handlers/student.go | 19 ++++++------ server.go | 57 ++++++++++++++++++------------------ sqlc.yaml | 1 + 9 files changed, 135 insertions(+), 104 deletions(-) diff --git a/database/init.go b/database/init.go index 1ba3291..d2eb6a3 100644 --- a/database/init.go +++ b/database/init.go @@ -1,14 +1,16 @@ package database import ( - "database/sql" + "context" "log" "os" "path/filepath" "strings" + + "github.com/jackc/pgx/v5" ) -func Init(db *sql.DB) { +func Init(conn *pgx.Conn) { filenames := []string{"announcement", "student", "faculty", "guild", "club", "course"} script := []string{} for _, filename := range filenames { @@ -23,7 +25,7 @@ func Init(db *sql.DB) { script = append(script, string(file)) } - _, err := db.Exec(strings.Join(script, "\n")) + _, err := conn.Exec(context.Background(), strings.Join(script, "\n")) if err != nil { log.Fatalln(err) } diff --git a/go.mod b/go.mod index 83b570f..cf53af2 100644 --- a/go.mod +++ b/go.mod @@ -8,17 +8,22 @@ require ( github.com/golang-jwt/jwt v3.2.2+incompatible github.com/google/go-github v17.0.0+incompatible github.com/gorilla/mux v1.8.0 + github.com/jackc/pgx/v5 v5.4.3 github.com/joho/godotenv v1.5.1 - github.com/lib/pq v1.10.7 github.com/rs/cors v1.8.3 golang.org/x/exp v0.0.0-20230314175356-6c0aa0d7709a - golang.org/x/net v0.8.0 + golang.org/x/net v0.10.0 gopkg.in/yaml.v2 v2.4.0 ) require ( github.com/andybalholm/cascadia v1.3.1 // indirect github.com/google/go-querystring v1.1.0 // indirect + github.com/jackc/pgpassfile v1.0.0 // indirect + github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a // indirect github.com/robfig/cron/v3 v3.0.1 // indirect + github.com/rogpeppe/go-internal v1.11.0 // indirect + golang.org/x/crypto v0.9.0 // indirect golang.org/x/sync v0.1.0 // indirect + golang.org/x/text v0.9.0 // indirect ) diff --git a/go.sum b/go.sum index 91c6b60..85f52d9 100644 --- a/go.sum +++ b/go.sum @@ -2,6 +2,7 @@ github.com/PuerkitoBio/goquery v1.8.1 h1:uQxhNlArOIdbrH1tr0UXwdVFgDcZDrZVdcpygAc github.com/PuerkitoBio/goquery v1.8.1/go.mod h1:Q8ICL1kNUJ2sXGoAhPGUdYDJvgQgHzJsnnd3H7Ho5jQ= github.com/andybalholm/cascadia v1.3.1 h1:nhxRkql1kdYCc8Snf7D5/D3spOX+dBgjA6u8x004T2c= github.com/andybalholm/cascadia v1.3.1/go.mod h1:R4bJ1UQfqADjvDa4P6HZHLh/3OxWWEqc0Sk8XGwHqvA= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/go-co-op/gocron v1.18.1 h1:erHHbIIav46xAV54lnyKKjrKLP+2RgjuDsbwGamBEvI= github.com/go-co-op/gocron v1.18.1/go.mod h1:UqVyvM90I1q/R1qGEX6cBORI6WArLuEgYlbncLMvzRM= @@ -15,20 +16,34 @@ github.com/google/go-querystring v1.1.0 h1:AnCroh3fv4ZBgVIf1Iwtovgjaw/GiKJo8M8yD github.com/google/go-querystring v1.1.0/go.mod h1:Kcdr2DB4koayq7X8pmAG4sNG59So17icRSOU623lUBU= github.com/gorilla/mux v1.8.0 h1:i40aqfkR1h2SlN9hojwV5ZA91wcXFOvkdNIeFDP5koI= github.com/gorilla/mux v1.8.0/go.mod h1:DVbg23sWSpFRCP0SfiEN6jmj59UnW/n46BH5rLB71So= +github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsIM= +github.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg= +github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a h1:bbPeKD0xmW/Y25WS6cokEszi5g+S0QxI/d45PkRi7Nk= +github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a/go.mod h1:5TJZWKEWniPve33vlWYSoGYefn3gLQRzjfDlhSJ9ZKM= +github.com/jackc/pgx/v5 v5.4.3 h1:cxFyXhxlvAifxnkKKdlxv8XqUf59tDlYjnV5YYfsJJY= +github.com/jackc/pgx/v5 v5.4.3/go.mod h1:Ig06C2Vu0t5qXC60W8sqIthScaEnFvojjj9dSljmHRA= github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0= github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4= -github.com/lib/pq v1.10.7 h1:p7ZhMD+KsSRozJr34udlUrhboJwWAgCg34+/ZZNvZZw= -github.com/lib/pq v1.10.7/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o= +github.com/kr/pretty v0.3.0 h1:WgNl7dwNpEZ6jJ9k1snq4pZsg7DOEN8hP9Xw0Tsjwk0= +github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/robfig/cron/v3 v3.0.1 h1:WdRxkvbJztn8LMz/QEvLN5sBU+xKpSqwwUO1Pjr4qDs= github.com/robfig/cron/v3 v3.0.1/go.mod h1:eQICP3HwyT7UooqI/z+Ov+PtYAWygg1TEWWzGIFLtro= +github.com/rogpeppe/go-internal v1.11.0 h1:cWPaGQEPrBb5/AsnsZesgZZ9yb1OQ+GOISoDNXVBh4M= +github.com/rogpeppe/go-internal v1.11.0/go.mod h1:ddIwULY96R17DhadqLgMfk9H9tvdUzkipdSkR5nkCZA= github.com/rs/cors v1.8.3 h1:O+qNyWn7Z+F9M0ILBHgMVPuB1xTOucVd5gtaYyXBpRo= github.com/rs/cors v1.8.3/go.mod h1:XyqrcTp5zjWr1wsJ8PIRZssZ8b/WMcMf71DJnit4EMU= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= +github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.8.2 h1:+h33VjcLVPDHtOdpUCuF+7gSuG3yGIftsP1YvFihtJ8= github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= go.uber.org/goleak v1.2.1 h1:NBol2c7O1ZokfZ0LEU9K6Whx/KnwvepVetCUhtKja4A= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= +golang.org/x/crypto v0.9.0 h1:LF6fAI+IutBocDJ2OT0Q1g8plpYljMZ4+lty+dsqw3g= +golang.org/x/crypto v0.9.0/go.mod h1:yrmDGqONDYtNj3tH8X9dzUun2m2lzPa9ngI6/RUPGR0= golang.org/x/exp v0.0.0-20230314175356-6c0aa0d7709a h1:7P1LPCIoT0VysPspWqC1PioUNfgs11fjexQagDNOrRo= golang.org/x/exp v0.0.0-20230314175356-6c0aa0d7709a/go.mod h1:CxIveKay+FTh1D0yPZemJVgC/95VzuuOLq5Qi4xnoYc= golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= @@ -37,8 +52,8 @@ golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v golang.org/x/net v0.0.0-20210916014120-12bc252f5db8/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= golang.org/x/net v0.7.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= -golang.org/x/net v0.8.0 h1:Zrh2ngAOFYneWTAIAPethzeaQLuHwhuBkuV6ZiRnUaQ= -golang.org/x/net v0.8.0/go.mod h1:QVkue5JL9kW//ek3r6jTKnTFis1tRmNAW2P1shuFdJc= +golang.org/x/net v0.10.0 h1:X2//UzNDwYmtCLn7To6G58Wr6f5ahEAQgKNzv9Y951M= +golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.1.0 h1:wsuoTGHzEhffawBOhz5CYhcrV4IdKZbEyZjBMuTp12o= @@ -58,13 +73,16 @@ golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= +golang.org/x/text v0.9.0 h1:2sjJmO8cDvYveuX97RDLsxlyUxLl+GHoLxBiRdHllBE= +golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= -gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= diff --git a/handlers/announcement.go b/handlers/announcement.go index 857eeb7..dd73d02 100644 --- a/handlers/announcement.go +++ b/handlers/announcement.go @@ -2,7 +2,6 @@ package handlers import ( "context" - "database/sql" "fmt" "net/http" "net/url" @@ -13,6 +12,8 @@ import ( query "breadboard/.sqlc-auto-gen" "github.com/PuerkitoBio/goquery" + "github.com/jackc/pgx/v5" + "github.com/jackc/pgx/v5/pgtype" "golang.org/x/net/html" ) @@ -198,7 +199,7 @@ func scrapeAnnouncements() (announcements []Announcement) { // // It also has some case-specific checks since scraped data doesn't have // uniformity -func parseDate(date string) (parsedDate time.Time, err error) { +func parseDate(date string) (parsedDate pgtype.Date, err error) { date = strings.Replace(date, ".", "-", 3) date = strings.Replace(date, "/", "-", 3) date = strings.Replace(date, "__", "", 2) @@ -208,7 +209,8 @@ func parseDate(date string) (parsedDate time.Time, err error) { if date == "07-05-019" { date = "07-05-2019" } - return time.Parse("02-01-2006", date) + time, err := time.Parse("02-01-2006", date) + return pgtype.Date{Time: time}, err } // insertNewAnnouncements saves announcements to database @@ -217,10 +219,10 @@ func parseDate(date string) (parsedDate time.Time, err error) { // some formatting // // TODO: try to use sqlc or some other intermediate to store this query -func FetchAnnouncements(db *sql.DB) { +func FetchAnnouncements(conn *pgx.Conn) { insert_query := query_prefix ctx := context.Background() - queries := query.New(db) + queries := query.New(conn) latest_date, err := queries.GetLatestAnnouncementDate(ctx) if err != nil { fmt.Println(err) @@ -232,13 +234,13 @@ func FetchAnnouncements(db *sql.DB) { if err != nil { fmt.Println(date, err) } else { - if err != nil && date.Before(latest_date) { + if err != nil && !date.Time.After(latest_date.Time) { fmt.Println("Detected old announcement, assuming it is in database already") break } addition := strings.Join([]string{ "('", - date.Format("2006-01-02"), + date.Time.Format("2006-01-02"), "', '", announcement.Title, "', '", @@ -252,7 +254,7 @@ func FetchAnnouncements(db *sql.DB) { } } insert_query += " ON CONFLICT (date_of_creation, title) DO NOTHING" - _, inserterr := db.ExecContext(ctx, insert_query) + _, inserterr := conn.Exec(ctx, insert_query) if inserterr != nil { fmt.Println(inserterr) } @@ -261,16 +263,16 @@ func FetchAnnouncements(db *sql.DB) { // GetAnnouncements returns all the announcements stored in database // // It is a wrapper function around -func GetAnnouncements(db *sql.DB) http.HandlerFunc { +func GetAnnouncements(conn *pgx.Conn) http.HandlerFunc { ctx := context.Background() - queries := query.New(db) + queries := query.New(conn) return func(w http.ResponseWriter, r *http.Request) { announcements, err := queries.GetAcademicAnnouncements(ctx) - if err == sql.ErrNoRows || len(announcements) == 0 { - FetchAnnouncements(db) + if err == pgx.ErrNoRows || len(announcements) == 0 { + FetchAnnouncements(conn) } announcements, err = queries.GetAcademicAnnouncements(ctx) - if err == sql.ErrNoRows { + if err == pgx.ErrNoRows { RespondError(w, 404, "Announcements not found in the database") return } diff --git a/handlers/club.go b/handlers/club.go index 1c8d8c2..58a1ac3 100644 --- a/handlers/club.go +++ b/handlers/club.go @@ -2,7 +2,6 @@ package handlers import ( "context" - "database/sql" "encoding/json" "fmt" "log" @@ -12,6 +11,8 @@ import ( query "breadboard/.sqlc-auto-gen" "github.com/gorilla/mux" + "github.com/jackc/pgx/v5" + "github.com/jackc/pgx/v5/pgtype" ) type Club struct { @@ -31,9 +32,9 @@ type Faculty struct { } // CreateClubFaculty creates a new faculty incharge for a group. -func CreateClubFaculty(db *sql.DB) http.HandlerFunc { +func CreateClubFaculty(conn *pgx.Conn) http.HandlerFunc { ctx := context.Background() - queries := query.New(db) + queries := query.New(conn) return func(w http.ResponseWriter, r *http.Request) { group_name := mux.Vars(r)["name"] vars := r.URL.Query() @@ -71,9 +72,9 @@ func CreateClubFaculty(db *sql.DB) http.HandlerFunc { } // CreateClubMember adds a new member to a club. -func CreateClubMember(db *sql.DB) http.HandlerFunc { +func CreateClubMember(conn *pgx.Conn) http.HandlerFunc { ctx := context.Background() - queries := query.New(db) + queries := query.New(conn) type CreateClubMemberParams struct { ClubNameOrAlias string `json:"club_name_or_alias"` @@ -96,7 +97,7 @@ func CreateClubMember(db *sql.DB) http.HandlerFunc { RollNumber: clubMember.RollNumber, Position: clubMember.Position, ExtraGroups: clubMember.ExtraGroups, - Comments: sql.NullString{String: clubMember.Comments, Valid: true}, + Comments: pgtype.Text{String: clubMember.Comments, Valid: true}, } err := queries.CreateClubMember(ctx, params) if err != nil { @@ -110,9 +111,9 @@ func CreateClubMember(db *sql.DB) http.HandlerFunc { } // CreateClubSocial adds a new social media handle of a group. -func CreateClubSocial(db *sql.DB) http.HandlerFunc { +func CreateClubSocial(conn *pgx.Conn) http.HandlerFunc { ctx := context.Background() - queries := query.New(db) + queries := query.New(conn) return func(w http.ResponseWriter, r *http.Request) { group_name := mux.Vars(r)["name"] vars := r.URL.Query() @@ -146,9 +147,9 @@ func CreateClubSocial(db *sql.DB) http.HandlerFunc { } // DeleteClubFaculty deletes an existing faculty incharge of a group. -func DeleteClubFaculty(db *sql.DB) http.HandlerFunc { +func DeleteClubFaculty(conn *pgx.Conn) http.HandlerFunc { ctx := context.Background() - queries := query.New(db) + queries := query.New(conn) return func(w http.ResponseWriter, r *http.Request) { vars := mux.Vars(r) id, err := strconv.Atoi(vars["id"]) @@ -173,9 +174,9 @@ func DeleteClubFaculty(db *sql.DB) http.HandlerFunc { } // DeleteClubMember deletes an existing member of a club. -func DeleteClubMember(db *sql.DB) http.HandlerFunc { +func DeleteClubMember(conn *pgx.Conn) http.HandlerFunc { ctx := context.Background() - queries := query.New(db) + queries := query.New(conn) type DeleteClubMemberParams struct { ClubNameOrAlias string `json:"club_name_or_alias"` @@ -206,9 +207,9 @@ func DeleteClubMember(db *sql.DB) http.HandlerFunc { } // DeleteClubSocial deletes an existing social media handle of a group. -func DeleteClubSocial(db *sql.DB) http.HandlerFunc { +func DeleteClubSocial(conn *pgx.Conn) http.HandlerFunc { ctx := context.Background() - queries := query.New(db) + queries := query.New(conn) return func(w http.ResponseWriter, r *http.Request) { vars := mux.Vars(r) params := query.DeleteClubSocialParams{ @@ -231,14 +232,14 @@ func DeleteClubSocial(db *sql.DB) http.HandlerFunc { // // This handler takes in a name argument which is first // checked as an alias and then as the name of a group. -func GetClub(db *sql.DB) http.HandlerFunc { +func GetClub(conn *pgx.Conn) http.HandlerFunc { ctx := context.Background() - queries := query.New(db) + queries := query.New(conn) return func(w http.ResponseWriter, r *http.Request) { vars := mux.Vars(r) group, err := queries.GetClub(ctx, vars["name"]) - if err == sql.ErrNoRows { + if err == pgx.ErrNoRows { RespondError(w, 404, "No groups found!") return } @@ -253,12 +254,12 @@ func GetClub(db *sql.DB) http.HandlerFunc { } // GetClubs retrieves the group details from the database. -func GetClubs(db *sql.DB) http.HandlerFunc { +func GetClubs(conn *pgx.Conn) http.HandlerFunc { ctx := context.Background() - queries := query.New(db) + queries := query.New(conn) return func(w http.ResponseWriter, r *http.Request) { groups, err := queries.GetClubs(ctx) - if err == sql.ErrNoRows { + if err == pgx.ErrNoRows { RespondError(w, 404, "No groups found!") return } @@ -272,9 +273,9 @@ func GetClubs(db *sql.DB) http.HandlerFunc { } // GetClubFaculty retrieves the management faculty of a group from the database. -func GetClubFaculty(db *sql.DB) http.HandlerFunc { +func GetClubFaculty(conn *pgx.Conn) http.HandlerFunc { ctx := context.Background() - queries := query.New(db) + queries := query.New(conn) return func(w http.ResponseWriter, r *http.Request) { group_name := mux.Vars(r)["name"] faculty, err := queries.GetClubFaculty(ctx, group_name) @@ -288,9 +289,9 @@ func GetClubFaculty(db *sql.DB) http.HandlerFunc { } // GetClubSocials retrieves the social media links of a group from the database. -func GetClubSocials(db *sql.DB) http.HandlerFunc { +func GetClubSocials(conn *pgx.Conn) http.HandlerFunc { ctx := context.Background() - queries := query.New(db) + queries := query.New(conn) return func(w http.ResponseWriter, r *http.Request) { group_name := mux.Vars(r)["name"] socials, err := queries.GetClubSocials(ctx, group_name) @@ -304,9 +305,9 @@ func GetClubSocials(db *sql.DB) http.HandlerFunc { } // ReadClubMembers retrieves the members of a club. -func ReadClubMembers(db *sql.DB) http.HandlerFunc { +func ReadClubMembers(conn *pgx.Conn) http.HandlerFunc { ctx := context.Background() - queries := query.New(db) + queries := query.New(conn) type ReadClubMemberParams struct { ClubNameOrAlias string `json:"club_name_or_alias"` @@ -330,9 +331,9 @@ func ReadClubMembers(db *sql.DB) http.HandlerFunc { } // UpdateClubMember updates a club member's details. -func UpdateClubMember(db *sql.DB) http.HandlerFunc { +func UpdateClubMember(conn *pgx.Conn) http.HandlerFunc { ctx := context.Background() - queries := query.New(db) + queries := query.New(conn) type UpdateClubMemberParams struct { ClubNameOrAlias string `json:"club_name_or_alias"` @@ -355,7 +356,7 @@ func UpdateClubMember(db *sql.DB) http.HandlerFunc { RollNumber: clubMember.RollNumber, Position: clubMember.Position, ExtraGroups: clubMember.ExtraGroups, - Comments: sql.NullString{String: clubMember.Comments, Valid: true}, + Comments: pgtype.Text{String: clubMember.Comments, Valid: true}, } err := queries.UpdateClubMember(ctx, params) if err != nil { @@ -369,9 +370,9 @@ func UpdateClubMember(db *sql.DB) http.HandlerFunc { } // UpdateClubSocials updates the link of a social media handle for a group. -func UpdateClubSocials(db *sql.DB) http.HandlerFunc { +func UpdateClubSocials(conn *pgx.Conn) http.HandlerFunc { ctx := context.Background() - queries := query.New(db) + queries := query.New(conn) return func(w http.ResponseWriter, r *http.Request) { vars := mux.Vars(r) link := r.URL.Query().Get("link") diff --git a/handlers/course.go b/handlers/course.go index 430cd76..32815b6 100644 --- a/handlers/course.go +++ b/handlers/course.go @@ -2,7 +2,6 @@ package handlers import ( "context" - "database/sql" "encoding/json" "fmt" "log" @@ -14,6 +13,7 @@ import ( "github.com/google/go-github/github" "github.com/gorilla/mux" + "github.com/jackc/pgx/v5" "golang.org/x/exp/slices" "gopkg.in/yaml.v2" ) @@ -36,9 +36,9 @@ type Course struct { Specifics []BranchSpecifics } -func CreateCourse(db *sql.DB) http.HandlerFunc { +func CreateCourse(conn *pgx.Conn) http.HandlerFunc { ctx := context.Background() - queries := query.New(db) + queries := query.New(conn) return func(w http.ResponseWriter, r *http.Request) { var coursePaths []string json.NewDecoder(r.Body).Decode(&coursePaths) @@ -89,14 +89,14 @@ func CreateCourse(db *sql.DB) http.HandlerFunc { } // GetCourse is a handler for retrieving a single course via the `code` argument. -func GetCourse(db *sql.DB) http.HandlerFunc { +func GetCourse(conn *pgx.Conn) http.HandlerFunc { ctx := context.Background() - queries := query.New(db) + queries := query.New(conn) return func(w http.ResponseWriter, r *http.Request) { vars := mux.Vars(r) course, err := queries.GetCourse(ctx, vars["code"]) - if err == sql.ErrNoRows { + if err == pgx.ErrNoRows { RespondError(w, 404, "Course not found in the database") return } @@ -106,9 +106,9 @@ func GetCourse(db *sql.DB) http.HandlerFunc { // GetCourses is a handler for retrieving all the courses matching the given // query parameters. It outputs all the courses if no parameter is passed. -func GetCourses(db *sql.DB) http.HandlerFunc { +func GetCourses(conn *pgx.Conn) http.HandlerFunc { ctx := context.Background() - queries := query.New(db) + queries := query.New(conn) return func(w http.ResponseWriter, r *http.Request) { vars := r.URL.Query() var semester int @@ -135,31 +135,31 @@ func GetCourses(db *sql.DB) http.HandlerFunc { // !TODO: Make code idempotent if semester == 0 && branch == "" { courses, err := queries.GetCourses(ctx) - if err != nil && err != sql.ErrNoRows { + if err != nil && err != pgx.ErrNoRows { RespondError(w, 500, err.Error()) return - } else if err == sql.ErrNoRows || len(courses) == 0 { + } else if err == pgx.ErrNoRows || len(courses) == 0 { RespondError(w, 404, "Courses not found in the database") return } RespondJSON(w, 200, courses) } else if semester != 0 && branch == "" { courses, err := queries.GetCoursesBySemester(ctx, int16(semester)) - if err == sql.ErrNoRows || len(courses) == 0 { + if err == pgx.ErrNoRows || len(courses) == 0 { RespondError(w, 404, "Courses not found in the database") return } RespondJSON(w, 200, courses) } else if semester == 0 && branch != "" { courses, err := queries.GetCoursesByBranch(ctx, branch) - if err == sql.ErrNoRows || len(courses) == 0 { + if err == pgx.ErrNoRows || len(courses) == 0 { RespondError(w, 404, "Courses not found in the database") return } RespondJSON(w, 200, courses) } else { courses, err := queries.GetCoursesByBranchAndSemester(ctx, query.GetCoursesByBranchAndSemesterParams{Branch: branch, Semester: int16(semester)}) - if err == sql.ErrNoRows || len(courses) == 0 { + if err == pgx.ErrNoRows || len(courses) == 0 { RespondError(w, 404, "Courses not found in the database") return } diff --git a/handlers/student.go b/handlers/student.go index 0ff9026..dec872c 100644 --- a/handlers/student.go +++ b/handlers/student.go @@ -2,16 +2,17 @@ package handlers import ( "context" - "database/sql" "net/http" "strconv" query "breadboard/.sqlc-auto-gen" "github.com/gorilla/mux" + "github.com/jackc/pgx/v5" + "github.com/jackc/pgx/v5/pgtype" ) -func GetDiscordLinkStatus(db *sql.DB) http.HandlerFunc { +func GetDiscordLinkStatus(db *pgx.Conn) http.HandlerFunc { ctx := context.Background() queries := query.New(db) return func(w http.ResponseWriter, r *http.Request) { @@ -22,8 +23,8 @@ func GetDiscordLinkStatus(db *sql.DB) http.HandlerFunc { return } - student, err := queries.GetDiscordLinkStatus(ctx, sql.NullInt64{Int64: int64(idInt), Valid: true}) - if err == sql.ErrNoRows { + student, err := queries.GetDiscordLinkStatus(ctx, pgtype.Int8{Int64: int64(idInt), Valid: true}) + if err == pgx.ErrNoRows { RespondJSON(w, 404, false) } else { RespondJSON(w, 200, student) @@ -32,9 +33,9 @@ func GetDiscordLinkStatus(db *sql.DB) http.HandlerFunc { } // GetHostels retrieves all the hostels and their meta data from the database. -func GetHostels(db *sql.DB) http.HandlerFunc { +func GetHostels(conn *pgx.Conn) http.HandlerFunc { ctx := context.Background() - queries := query.New(db) + queries := query.New(conn) return func(w http.ResponseWriter, r *http.Request) { hostels, _ := queries.GetHostels(ctx) RespondJSON(w, 200, hostels) @@ -42,9 +43,9 @@ func GetHostels(db *sql.DB) http.HandlerFunc { } // GetStudent retrieves a single student's details based on their roll number, email, or Discord ID. -func GetStudent(db *sql.DB) http.HandlerFunc { +func GetStudent(conn *pgx.Conn) http.HandlerFunc { ctx := context.Background() - queries := query.New(db) + queries := query.New(conn) return func(w http.ResponseWriter, r *http.Request) { id := mux.Vars(r)["id"] @@ -55,7 +56,7 @@ func GetStudent(db *sql.DB) http.HandlerFunc { Email: id, DiscordID: int64(discordId), }) - if err == sql.ErrNoRows { + if err == pgx.ErrNoRows { RespondError(w, 404, "Student not found in the database") return } diff --git a/server.go b/server.go index 2b5cf43..520e53d 100644 --- a/server.go +++ b/server.go @@ -1,7 +1,7 @@ package breadboard import ( - "database/sql" + "context" "log" "net/http" "os" @@ -13,32 +13,33 @@ import ( "github.com/go-co-op/gocron" "github.com/gorilla/mux" - _ "github.com/lib/pq" + "github.com/jackc/pgx/v5" "github.com/rs/cors" ) type server struct { - db *sql.DB + conn *pgx.Conn router *mux.Router } // NewServer returns a new app instance. func NewServer() *server { - db, err := sql.Open("postgres", os.Getenv("DATABASE_URL")) + conn, err := pgx.Connect(context.Background(), os.Getenv("DATABASE_URL")) if err != nil { - log.Fatalln(err) + log.Fatalln("Unable to connect to database:\n", err) } + // !TODO: Make separate interface to initialise database - database.Init(db) + database.Init(conn) // Initialize cronjob for fetching announcements cron := gocron.NewScheduler(time.UTC) cron.Every(1).Day().At("00:00").Do(func() { - h.FetchAnnouncements(db) + h.FetchAnnouncements(conn) }) cron.StartAsync() - s := server{db: db, router: mux.NewRouter().StrictSlash(true)} + s := server{conn: conn, router: mux.NewRouter().StrictSlash(true)} s.setRouters() return &s } @@ -61,35 +62,35 @@ func (s *server) setRouters() { }) // Status - s.router.Handle("/status/student/discord", h.GetDiscordLinkStatus(s.db)).Methods("GET") + s.router.Handle("/status/student/discord", h.GetDiscordLinkStatus(s.conn)).Methods("GET") // Announcements - s.router.HandleFunc("/announcements", h.GetAnnouncements(s.db)).Methods("GET") + s.router.HandleFunc("/announcements", h.GetAnnouncements(s.conn)).Methods("GET") // Courses - s.router.HandleFunc("/courses", h.GetCourses(s.db)).Methods("GET") - s.router.HandleFunc("/courses", m.Authenticator(h.CreateCourse(s.db))).Methods("POST") - s.router.HandleFunc("/courses/{code}", h.GetCourse(s.db)).Methods("GET") + s.router.HandleFunc("/courses", h.GetCourses(s.conn)).Methods("GET") + s.router.HandleFunc("/courses", m.Authenticator(h.CreateCourse(s.conn))).Methods("POST") + s.router.HandleFunc("/courses/{code}", h.GetCourse(s.conn)).Methods("GET") // Clubs - s.router.Handle("/clubs", h.GetClubs(s.db)).Methods("GET") - s.router.Handle("/clubs/{name}", h.GetClub(s.db)).Methods("GET") + s.router.Handle("/clubs", h.GetClubs(s.conn)).Methods("GET") + s.router.Handle("/clubs/{name}", h.GetClub(s.conn)).Methods("GET") - s.router.Handle("/clubs/{name}/faculty", h.GetClubFaculty(s.db)).Methods("GET") - s.router.Handle("/clubs/{name}/faculty", m.Authenticator(h.CreateClubFaculty(s.db))).Methods("POST") - s.router.Handle("/clubs/{name}/faculty/{fname}", m.Authenticator(h.DeleteClubFaculty(s.db))).Methods("DELETE") + s.router.Handle("/clubs/{name}/faculty", h.GetClubFaculty(s.conn)).Methods("GET") + s.router.Handle("/clubs/{name}/faculty", m.Authenticator(h.CreateClubFaculty(s.conn))).Methods("POST") + s.router.Handle("/clubs/{name}/faculty/{fname}", m.Authenticator(h.DeleteClubFaculty(s.conn))).Methods("DELETE") - s.router.Handle("/clubs/{name}/members", m.Authenticator(h.ReadClubMembers(s.db))).Methods("GET") - s.router.Handle("/clubs/{name}/members", m.Authenticator(h.CreateClubMember(s.db))).Methods("POST") - s.router.Handle("/clubs/{name}/members", m.Authenticator(h.UpdateClubMember(s.db))).Methods("PUT") - s.router.Handle("/clubs/{name}/members/{roll}", m.Authenticator(h.DeleteClubMember(s.db))).Methods("DELETE") + s.router.Handle("/clubs/{name}/members", m.Authenticator(h.ReadClubMembers(s.conn))).Methods("GET") + s.router.Handle("/clubs/{name}/members", m.Authenticator(h.CreateClubMember(s.conn))).Methods("POST") + s.router.Handle("/clubs/{name}/members", m.Authenticator(h.UpdateClubMember(s.conn))).Methods("PUT") + s.router.Handle("/clubs/{name}/members/{roll}", m.Authenticator(h.DeleteClubMember(s.conn))).Methods("DELETE") - s.router.Handle("/clubs/{name}/socials", h.GetClubSocials(s.db)).Methods("GET") - s.router.Handle("/clubs/{name}/socials", m.Authenticator(h.CreateClubSocial(s.db))).Methods("POST") - s.router.Handle("/clubs/{name}/socials/{type}", m.Authenticator(h.UpdateClubSocials(s.db))).Methods("PUT") - s.router.Handle("/clubs/{name}/socials/{type}", m.Authenticator(h.DeleteClubSocial(s.db))).Methods("DELETE") + s.router.Handle("/clubs/{name}/socials", h.GetClubSocials(s.conn)).Methods("GET") + s.router.Handle("/clubs/{name}/socials", m.Authenticator(h.CreateClubSocial(s.conn))).Methods("POST") + s.router.Handle("/clubs/{name}/socials/{type}", m.Authenticator(h.UpdateClubSocials(s.conn))).Methods("PUT") + s.router.Handle("/clubs/{name}/socials/{type}", m.Authenticator(h.DeleteClubSocial(s.conn))).Methods("DELETE") // Students - s.router.Handle("/hostels", h.GetHostels(s.db)).Methods("GET") - s.router.Handle("/students/{id}", m.Authenticator(h.GetStudent(s.db))).Methods("GET") + s.router.Handle("/hostels", h.GetHostels(s.conn)).Methods("GET") + s.router.Handle("/students/{id}", m.Authenticator(h.GetStudent(s.conn))).Methods("GET") } diff --git a/sqlc.yaml b/sqlc.yaml index 3d4a265..1afdcd6 100644 --- a/sqlc.yaml +++ b/sqlc.yaml @@ -16,5 +16,6 @@ sql: go: package: "database" out: ".sqlc-auto-gen" + sql_package: "pgx/v5" emit_json_tags: true json_tags_case_style: "snake"