Brijesh's Git Server — watchman @ c4703486eca9ca9390314db3e85650a3cb6457d9

observability tool, needs to be rewritten once identity is stable

fix(projects): update API endpoints to follow REST standard

Having messy API in initial versions is okay as I'll probably rewrite the whole thing from scratch anyways, but I fixed it for now.

This would've been easier I hadn't decided to stick with standard library for http server, Might switch to echo in coming versions if using vanilla net/http becomes too much of a hassle
Brijesh ops@brijesh.dev
Wed, 03 Jul 2024 19:49:18 +0530
commit

c4703486eca9ca9390314db3e85650a3cb6457d9

parent

242871c6f0dd3c1670d49334a7f551c68b7a518e

5 files changed, 197 insertions(+), 61 deletions(-)

jump to
M README.mdREADME.md

@@ -21,7 +21,7 @@

#### 1. Create Project ```sh -curl -X POST http://127.0.0.1:4000/projects -d '{"Name": "Project 1"}' +curl -X POST http://127.0.0.1:4000/projects -d '{"name": "Project 1"}' ``` #### 2. List Projects

@@ -33,19 +33,19 @@

#### 3. Get Project By ID ```sh -curl -X POST http://127.0.0.1:4000/project -d '{"ID": "1"}' +curl -X GET http://127.0.0.1:4000/projects/{id} ``` #### 4. Update Project ```sh -curl -X PUT http://127.0.0.1:4000/project -d '{"ID": "1", "Name": "Project 1 Updated"}' +curl -X PUT http://127.0.0.1:4000/projects/{id} -d '{"name": "Updated Project Name"}' ``` #### 5. Delete Project ```sh -curl -X DELETE http://127.0.0.1:4000/project -d '{"ID": "1"}' +curl -X DELETE http://127.0.0.1:4000/projects/{id} ``` ### Log Management API
M go.modgo.mod

@@ -7,6 +7,5 @@ github.com/google/uuid v1.6.0

github.com/joho/godotenv v1.5.0 github.com/mattn/go-sqlite3 v1.14.22 golang.org/x/time v0.5.0 + gopkg.in/yaml.v3 v3.0.1 ) - -require gopkg.in/yaml.v3 v3.0.1 // indirect
M internal/logs.gointernal/logs.go

@@ -63,6 +63,48 @@ w.WriteHeader(http.StatusInternalServerError)

} } +func BatchInsertLogs(w http.ResponseWriter, r *http.Request, db *sql.DB) { + if r.Method != http.MethodPost { + w.WriteHeader(http.StatusMethodNotAllowed) + fmt.Fprintf(w, "Method %s not allowed", r.Method) + return + } + var logs []schema.Log + decoder := json.NewDecoder(r.Body) + err := decoder.Decode(&logs) + if err != nil { + w.WriteHeader(http.StatusBadRequest) + fmt.Fprintf(w, "Error decoding log data: %v", err) + return + } + stmt, err := db.Prepare("INSERT INTO Logs (Time, Level, Message, Subject, UserID, ProjectID) VALUES (?, ?, ?, ?, ?, ?)") + if err != nil { + w.WriteHeader(http.StatusInternalServerError) + fmt.Fprintf(w, "Error preparing SQL statement: %v", err) + return + } + defer stmt.Close() + for _, log := range logs { + log.Time = int32(time.Now().Unix()) + _, err = stmt.Exec(log.Time, log.Level, log.Message, log.Subject, log.UserID, log.ProjectID) + if err != nil { + w.WriteHeader(http.StatusInternalServerError) + fmt.Fprintf(w, "Error inserting log into database: %v", err) + return + } + } + response := schema.Response_Type{ + Status: "OK", + Message: "Logs created successfully", + RequestID: r.Context().Value(schema.RequestIDKey{}).(string), + } + w.Header().Set("Content-Type", "application/json") + err = json.NewEncoder(w).Encode(response) + if err != nil { + w.WriteHeader(http.StatusInternalServerError) + } +} + func GetAllLogs(w http.ResponseWriter, r *http.Request, db *sql.DB) { if r.Method != http.MethodGet { w.WriteHeader(http.StatusMethodNotAllowed)
M internal/projects.gointernal/projects.go

@@ -63,40 +63,90 @@ http.Error(w, err.Error(), http.StatusInternalServerError)

} } -func ListProjects(w http.ResponseWriter, r *http.Request, db *sql.DB) { +// func ListProjects(w http.ResponseWriter, r *http.Request, db *sql.DB) { +// if r.Method != http.MethodGet { +// w.WriteHeader(http.StatusMethodNotAllowed) +// fmt.Fprintf(w, "Method %s not allowed", r.Method) +// return +// } +// +// projectID := r.URL.Query().Get("id") +// +// if projectID == "" { +// rows, err := db.Query("SELECT * FROM Projects") +// if err != nil { +// w.WriteHeader(http.StatusInternalServerError) +// fmt.Fprintf(w, "Error querying database: %v", err) +// return +// } +// defer rows.Close() +// var projects []schema.Project +// for rows.Next() { +// var project schema.Project +// err := rows.Scan(&project.ID, &project.Name) +// if err != nil { +// w.WriteHeader(http.StatusInternalServerError) +// fmt.Fprintf(w, "Error scanning row: %v", err) +// return +// } +// projects = append(projects, project) +// } +// response := schema.Response_Type{ +// Status: "OK", +// Message: "Projects retrieved successfully", +// RequestID: r.Context().Value(schema.RequestIDKey{}).(string), +// Data: projects, +// } +// w.Header().Set("Content-Type", "application/json") +// err = json.NewEncoder(w).Encode(response) +// if err != nil { +// http.Error(w, err.Error(), http.StatusInternalServerError) +// } +// } else { +// row := db.QueryRow("SELECT * FROM Projects WHERE ID = ?", projectID) +// var projectByID schema.Project +// err := row.Scan(&projectByID.ID, &projectByID.Name) +// if err != nil { +// w.WriteHeader(http.StatusInternalServerError) +// fmt.Fprintf(w, "Error querying database: %v", err) +// return +// } +// response := schema.Response_Type{ +// Status: "OK", +// Message: "Project retrieved successfully", +// RequestID: r.Context().Value(schema.RequestIDKey{}).(string), +// Data: projectByID, +// } +// w.Header().Set("Content-Type", "application/json") +// err = json.NewEncoder(w).Encode(response) +// if err != nil { +// http.Error(w, err.Error(), http.StatusInternalServerError) +// } +// } +// } + +// internal.GetProjectByID(w, r, db_connection, projectID) + +func GetProjectByID(w http.ResponseWriter, r *http.Request, db *sql.DB, projectID string) { if r.Method != http.MethodGet { w.WriteHeader(http.StatusMethodNotAllowed) fmt.Fprintf(w, "Method %s not allowed", r.Method) return } - - rows, err := db.Query("SELECT * FROM Projects") + row := db.QueryRow("SELECT * FROM Projects WHERE ID = ?", projectID) + var projectByID schema.Project + err := row.Scan(&projectByID.ID, &projectByID.Name) if err != nil { w.WriteHeader(http.StatusInternalServerError) fmt.Fprintf(w, "Error querying database: %v", err) return } - defer rows.Close() - - var projects []schema.Project - for rows.Next() { - var project schema.Project - err := rows.Scan(&project.ID, &project.Name) - if err != nil { - w.WriteHeader(http.StatusInternalServerError) - fmt.Fprintf(w, "Error scanning row: %v", err) - return - } - projects = append(projects, project) - } - response := schema.Response_Type{ Status: "OK", - Message: "Projects retrieved successfully", + Message: "Project retrieved successfully", RequestID: r.Context().Value(schema.RequestIDKey{}).(string), - Data: projects, + Data: projectByID, } - w.Header().Set("Content-Type", "application/json") err = json.NewEncoder(w).Encode(response) if err != nil {

@@ -104,31 +154,36 @@ http.Error(w, err.Error(), http.StatusInternalServerError)

} } -func GetProjectByID(w http.ResponseWriter, r *http.Request, db *sql.DB) { +func ListAllProjects(w http.ResponseWriter, r *http.Request, db *sql.DB) { if r.Method != http.MethodGet { w.WriteHeader(http.StatusMethodNotAllowed) fmt.Fprintf(w, "Method %s not allowed", r.Method) return } - - projectID := r.URL.Query().Get("id") - - row := db.QueryRow("SELECT * FROM Projects WHERE ID = ?", projectID) - var projectByID schema.Project - err := row.Scan(&projectByID.ID, &projectByID.Name) + rows, err := db.Query("SELECT * FROM Projects") if err != nil { w.WriteHeader(http.StatusInternalServerError) fmt.Fprintf(w, "Error querying database: %v", err) return } - + defer rows.Close() + var projects []schema.Project + for rows.Next() { + var project schema.Project + err := rows.Scan(&project.ID, &project.Name) + if err != nil { + w.WriteHeader(http.StatusInternalServerError) + fmt.Fprintf(w, "Error scanning row: %v", err) + return + } + projects = append(projects, project) + } response := schema.Response_Type{ Status: "OK", - Message: "Project retrieved successfully", + Message: "Projects retrieved successfully", RequestID: r.Context().Value(schema.RequestIDKey{}).(string), - Data: projectByID, + Data: projects, } - w.Header().Set("Content-Type", "application/json") err = json.NewEncoder(w).Encode(response) if err != nil {

@@ -136,7 +191,7 @@ http.Error(w, err.Error(), http.StatusInternalServerError)

} } -func UpdateProject(w http.ResponseWriter, r *http.Request, db *sql.DB) { +func UpdateProjectByID(w http.ResponseWriter, r *http.Request, db *sql.DB, projectID string) { if r.Method != http.MethodPut { w.WriteHeader(http.StatusMethodNotAllowed) fmt.Fprintf(w, "Method %s not allowed", r.Method)

@@ -152,7 +207,7 @@ fmt.Fprintf(w, "Error decoding project data: %v", err)

return } - stmt, err := db.Prepare("UPDATE Projects SET Name = ? WHERE ID = ?") + stmt, err := db.Prepare("UPDATE Projects SET Name = ?, ID = ? WHERE ID = ?") if err != nil { w.WriteHeader(http.StatusInternalServerError) fmt.Fprintf(w, "Error preparing SQL statement: %v", err)

@@ -160,7 +215,7 @@ return

} defer stmt.Close() - _, err = stmt.Exec(project.Name, project.ID) + _, err = stmt.Exec(project.Name, project.ID, projectID) if err != nil { w.WriteHeader(http.StatusInternalServerError) fmt.Fprintf(w, "Error updating project in database: %v", err)

@@ -180,22 +235,13 @@ http.Error(w, err.Error(), http.StatusInternalServerError)

} } -func DeleteProject(w http.ResponseWriter, r *http.Request, db *sql.DB) { +func DeleteProjectByID(w http.ResponseWriter, r *http.Request, db *sql.DB, projectID string) { if r.Method != http.MethodDelete { w.WriteHeader(http.StatusMethodNotAllowed) fmt.Fprintf(w, "Method %s not allowed", r.Method) return } - var project schema.Project - decoder := json.NewDecoder(r.Body) - err := decoder.Decode(&project) - if err != nil { - w.WriteHeader(http.StatusBadRequest) - fmt.Fprintf(w, "Error decoding project data: %v", err) - return - } - stmt, err := db.Prepare("DELETE FROM Projects WHERE ID = ?") if err != nil { w.WriteHeader(http.StatusInternalServerError)

@@ -204,7 +250,7 @@ return

} defer stmt.Close() - _, err = stmt.Exec(project.ID) + _, err = stmt.Exec(projectID) if err != nil { w.WriteHeader(http.StatusInternalServerError) fmt.Fprintf(w, "Error deleting project from database: %v", err)
M main.gomain.go

@@ -1,13 +1,34 @@

+// projects api endpoints should be: +// 1. GET /projects +// 2. POST /projects +// 3. GET /projects/{id} +// 4. PUT /projects/{id} +// 5. DELETE /projects/{id} +// logs api endpoints should be restful but also factor in query parameters +// 1. GET /logs +// 2 GET /logs?project_id={id} +// 3. GET /logs?user_id={id} +// 4. GET /logs?start_time={timestamp}&end_time={timestamp} +// 5. GET /logs?level={level} +// 6. POST /logs (this is for batch insertion of logs) +// 7. DELETE /logs?project_id={id} +// 8. DELETE /logs?user_id={id} +// 9. DELETE /logs?start_time={timestamp}&end_time={timestamp} +// 10. DELETE /logs?level={level} + package main import ( "database/sql" + "encoding/json" "fmt" "log" "net/http" "strconv" + "strings" "watchman/internal" "watchman/middleware" + "watchman/schema" "watchman/utils" _ "github.com/mattn/go-sqlite3"

@@ -41,25 +62,44 @@ multiplexer := http.NewServeMux()

multiplexer.HandleFunc("/health", utils.Health_Check_Handler) - multiplexer.HandleFunc("/project", func(w http.ResponseWriter, r *http.Request) { + multiplexer.HandleFunc("/projects", func(w http.ResponseWriter, r *http.Request) { switch r.Method { + case http.MethodPost: + internal.CreateProject(w, r, db_connection) case http.MethodGet: - internal.GetProjectByID(w, r, db_connection) - case http.MethodPut: - internal.UpdateProject(w, r, db_connection) - case http.MethodDelete: - internal.DeleteProject(w, r, db_connection) + internal.ListAllProjects(w, r, db_connection) default: utils.Method_Not_Allowed_Handler(w, r) } }) - multiplexer.HandleFunc("/projects", func(w http.ResponseWriter, r *http.Request) { + multiplexer.HandleFunc("/projects/", func(w http.ResponseWriter, r *http.Request) { + url := strings.TrimPrefix(r.URL.Path, "/projects/") + projectID := url + + if projectID == "" { + response := schema.Response_Type{ + Status: "ERROR", + Message: "Project ID not provided", + RequestID: r.Context().Value(schema.RequestIDKey{}).(string), + Data: nil, + } + + w.WriteHeader(http.StatusBadRequest) + w.Header().Set("Content-Type", "application/json") + err = json.NewEncoder(w).Encode(response) + if err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + } + } + switch r.Method { - case http.MethodPost: - internal.CreateProject(w, r, db_connection) case http.MethodGet: - internal.ListProjects(w, r, db_connection) + internal.GetProjectByID(w, r, db_connection, projectID) + case http.MethodPut: + internal.UpdateProjectByID(w, r, db_connection, projectID) + case http.MethodDelete: + internal.DeleteProjectByID(w, r, db_connection, projectID) default: utils.Method_Not_Allowed_Handler(w, r) }

@@ -71,6 +111,15 @@ case http.MethodPost:

internal.CreateLog(w, r, db_connection) case http.MethodGet: internal.GetAllLogs(w, r, db_connection) + default: + utils.Method_Not_Allowed_Handler(w, r) + } + }) + + multiplexer.HandleFunc("/logs/batch-insert", func(w http.ResponseWriter, r *http.Request) { + switch r.Method { + case http.MethodPost: + internal.BatchInsertLogs(w, r, db_connection) default: utils.Method_Not_Allowed_Handler(w, r) }