commit 31edca3b06ab181049ce95fd17b30b7dc7e6977a Author: skyler Date: Fri May 2 07:54:33 2025 +0800 initial commit diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..136f3d4 --- /dev/null +++ b/go.mod @@ -0,0 +1,11 @@ +module test_server + +go 1.24.2 + +require ( + github.com/jackc/pgpassfile v1.0.0 // indirect + github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 // indirect + github.com/jackc/pgx/v5 v5.7.4 // indirect + golang.org/x/crypto v0.31.0 // indirect + golang.org/x/text v0.21.0 // indirect +) diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..29099af --- /dev/null +++ b/go.sum @@ -0,0 +1,17 @@ +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +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-20240606120523-5a60cdf6a761 h1:iCEnooe7UlwOQYpKFhBabPMi4aNAfoODPEFNiAnClxo= +github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761/go.mod h1:5TJZWKEWniPve33vlWYSoGYefn3gLQRzjfDlhSJ9ZKM= +github.com/jackc/pgx/v5 v5.7.4 h1:9wKznZrhWa2QiHL+NjTSPP6yjl3451BX3imWDnokYlg= +github.com/jackc/pgx/v5 v5.7.4/go.mod h1:ncY89UGWxg82EykZUwSpUKEfccBGGYq1xjrOpsbsfGQ= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +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= +golang.org/x/crypto v0.31.0 h1:ihbySMvVjLAeSH1IbfcRTkD/iNscyz8rGzjF/E5hV6U= +golang.org/x/crypto v0.31.0/go.mod h1:kDsLvtWBEx7MV9tJOj9bnXsPbxwJQ6csT/x4KIN4Ssk= +golang.org/x/text v0.21.0 h1:zyQAAkrwaneQ066sspRyJaG9VNi/YJ1NfzcGB3hZ/qo= +golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/index.html b/index.html new file mode 100644 index 0000000..da3e51d --- /dev/null +++ b/index.html @@ -0,0 +1,3 @@ + + Hello World! + diff --git a/main.go b/main.go new file mode 100644 index 0000000..70f29ce --- /dev/null +++ b/main.go @@ -0,0 +1,241 @@ +package main + +import ( + "net/http" + "fmt" + "encoding/json" + "os" + "context" + "github.com/jackc/pgx/v5" +) +//mixed opinions on global variables online, need production example to verify this practice +var conn = ConnectPostgres() + +//---------------------------------------------------------------------------------------------------- +//root that returns a static string +func RootHandler(w http.ResponseWriter, r *http.Request) { + fmt.Fprintln(w, "Hello World!") +} + +//---------------------------------------------------------------------------------------------------- +//json struct for incoming requests +type IncomingJson struct { + Message string `json:"message"` +} + +//take note that for structs, Capitalised fields indicates exported fields, and vice versa +//therefore "name string" will not be decoded/encoded, but "Name string" will +//the `json:name` +//json struct for api response +type OutboundJson struct { + Status string `json:"status"` + RequestData IncomingJson `json:"request_data"` +} + +//API that accepts and returns a json defined in the struct above +func JsonHandler(w http.ResponseWriter, r *http.Request){ + //checks if the method is Post, returns error if not + if r.Method != http.MethodPost { + http.Error(w, "Method not allowed,Only Post Requests", http.StatusMethodNotAllowed) + return + } + + //decode Json from request body, returns error if doesnt fit struct + var incoming IncomingJson + err := json.NewDecoder(r.Body).Decode(&incoming) + if err != nil { + http.Error(w,"Bad Request, Failed to parse body({message:str})", http.StatusBadRequest) + return + } + + //set response header to json + w.Header().Set("Content-Type", "application/json") + + //encode and set response + resp := OutboundJson{ + Status: "received", + RequestData: incoming, + } + + err = json.NewEncoder(w).Encode(resp) + if err != nil { + fmt.Println("JSON encoding Error:",err) + http.Error(w, "Error encoding JSON", http.StatusInternalServerError) + return + } +} +//---------------------------------------------------------------------------------------------------- +func ConnectPostgres()(*pgx.Conn){ + //yes i need to put this in a .env, im just testing for now and this database has nothing important + conn, err := pgx.Connect(context.Background(), "postgres://postgres:12345678@localhost:5432/postgres") + if err != nil { + fmt.Println("Unable to connect to database", err) + os.Exit(1) + } + + fmt.Println("Connected to database") + return conn +} + +//operation dictates which CRUD operation to perform (c,r,u,d) +//identifier dictates which row the operation is performed on (int) +//value dictates the actual change of operation +type IncomingPostgres struct{ + Operation string `json:"operation"` + Identifier int `json:identifier",omitempty` + Value string `json:"value",omitempty` +} + +type OutgoingPostgres struct{ + Identifier int `json:"identifier"` + Value string `json:"value"` + Status string `json:"status"` +} + +//the pointed table only has two columns: id(int) and value(str) +//the following handler provides a simple example on handling CRUD operations via an API using pgx +func PostgresHandler(w http.ResponseWriter, r *http.Request){ + //checks if the method is Post, returns error if not + if r.Method != http.MethodPost { + http.Error(w, "Method not allowed,Only Post Requests", http.StatusMethodNotAllowed) + return + } + + //decode Json from request body, returns error if doesnt fit struct + var incoming IncomingPostgres + err := json.NewDecoder(r.Body).Decode(&incoming) + if err != nil { + http.Error(w,"Bad Request, Failed to parse body({operation:str,identifier[optional]:int,value[optional]:string})", http.StatusBadRequest) + return + } + + //map of allowed inputs for the operation field, returns error if doesnt fit + allowed_operations := map[string]bool { + "c":true, + "r":true, + "u":true, + "d":true, + } + + if !allowed_operations[incoming.Operation] { + http.Error(w, "Bad Request, operation field only allows the following inputs: {c,r,u,d}", http.StatusBadRequest) + return + } + //switch case to determine the operation + //this method directly checks for zero values, which might not be feasible if zero values are allowed inputs + //consider transforming the struct into a map then checking if it exists, that would be a more robust method + var id int + var value string + switch incoming.Operation{ + case "c": + if incoming.Value != ""{ + fmt.Println("create",incoming.Value) + //SQL query for the db + rows, err := conn.Query(context.Background(),"INSERT INTO test_table (value) VALUES ($1) RETURNING(id)",incoming.Value) + if err != nil{ + fmt.Println(err) + http.Error(w,"Server Error, something is wrong", http.StatusInternalServerError) + return + } + //close the connection when the function is finished + defer rows.Close() + + if rows.Next(){ + err = rows.Scan(&id) + if err != nil{ + fmt.Println(err) + http.Error(w,"Server Error, Scanning Failed",http.StatusInternalServerError) + return + } + } + value = incoming.Value + } else { + http.Error(w,"Bad Request, Value Key is needed for create operation",http.StatusBadRequest) + return + } + case "r": + if incoming.Identifier != 0{ + fmt.Println("read",incoming.Identifier) + //SQL query for the db + rows, err := conn.Query(context.Background(),"SELECT id,value FROM test_table WHERE id=$1",incoming.Identifier) + if err != nil{ + fmt.Println(err) + http.Error(w, "Server Error, something is wrong", http.StatusInternalServerError) + return + } + //close the connection when the function is finished + defer rows.Close() + //this is a readone() method, only the first item queried will be considered and returned + if rows.Next(){ + err = rows.Scan(&id,&value) + if err != nil{ + http.Error(w,"Server Error, Scanning Failed",http.StatusInternalServerError) + return + } + } else { + http.Error(w,"Not Found, identifier does not exist",http.StatusNotFound) + return + } + } else { + http.Error(w,"Bad Request, Identifier Key is needed for read operation",http.StatusBadRequest) + return + } + + case "u": + if incoming.Identifier != 0 && incoming.Value != ""{ + fmt.Println("update",incoming.Identifier,incoming.Value) + _, err := conn.Query(context.Background(),"UPDATE test_table SET value=$1 WHERE id=$2",incoming.Value,incoming.Identifier) + if err != nil{ + fmt.Println(err) + http.Error(w,"Update Failed, double check if identifier exists",http.StatusBadRequest) + return + } + id = incoming.Identifier + value = incoming.Value + } else { + http.Error(w,"Bad Request, Identifier Key and Value Key is needed for update operation",http.StatusBadRequest) + return + } + + case "d": + if incoming.Identifier != 0{ + fmt.Println("delete",incoming.Identifier) + _,err := conn.Query(context.Background(),"DELETE FROM test_table WHERE id=$1",incoming.Identifier) + if err != nil{ + fmt.Println(err) + http.Error(w,"Delete Failed, double check if identifier exists",http.StatusBadRequest) + return + } + value = incoming.Value + id = incoming.Identifier + } else { + http.Error(w,"Bad Request, Value Key is needed for create operation",http.StatusBadRequest) + return + } + } + + w.Header().Set("Content-Type","application/json") + resp := OutgoingPostgres{ + Status: "Successful", + Identifier: id, + Value: value, + } + json.NewEncoder(w).Encode(resp) +} + + +//---------------------------------------------------------------------------------------------------- +//main entry function to register routes and serve +func main() { + defer conn.Close(context.Background()) + http.HandleFunc("/pgx",PostgresHandler) + http.HandleFunc("/json", JsonHandler) + http.HandleFunc("/", RootHandler) + fmt.Println("Starting server at http://localhost:8080") + err := http.ListenAndServe(":8080", nil) + if err != nil { + fmt.Println("Error starting server:", err) + } +} + +