diff --git a/.air.toml b/.air.toml index abf3818..0f47cd0 100644 --- a/.air.toml +++ b/.air.toml @@ -12,7 +12,8 @@ delay = 1000 # 1 second delay after changes include_ext = ["go"] include_dir = ["main", "accounts", "build", "caching", "config", "middleware", "projects", "response", "scripts", - "utils", "test", "indexes", "AI", "analytics"] + "utils", "test", "indexes", "AI", "analytics", + "tables", "SqlEditor"] exclude_dir = ["tmp", "vendor", "testdata", ".git"] kill_delay = "1s" diff --git a/.gitignore b/.gitignore index 64e46af..49987f0 100644 --- a/.gitignore +++ b/.gitignore @@ -2,11 +2,13 @@ build/* *.env .idea .vscode -playground +playground/* tmp/main* *.log curl.sh .qodo *.log pr.md -response.md \ No newline at end of file +response.* +/tmp +insert_students.sh \ No newline at end of file diff --git a/AI/db_test.go b/AI/db_test.go index d6aba35..48a590b 100644 --- a/AI/db_test.go +++ b/AI/db_test.go @@ -1,7 +1,7 @@ package ai_test import ( - ai "DBHS/AI" + "DBHS/utils" "context" "fmt" "log" @@ -44,7 +44,7 @@ func TestExtractDatabaseSchema(t *testing.T) { }) // extract schema - schema, err := ai.ExtractDatabaseSchema(context.Background(), db) + schema, err := utils.ExtractDatabaseSchema(context.Background(), db) if err != nil { t.Fatalf("Failed to extract database schema: %v", err) } diff --git a/AI/handlers.go b/AI/handlers.go index 6b7c758..fc96549 100644 --- a/AI/handlers.go +++ b/AI/handlers.go @@ -3,15 +3,20 @@ package ai import ( "DBHS/config" "DBHS/response" - "net/http" + "encoding/json" + "time" + + "github.com/axiomhq/axiom-go/axiom" + "github.com/axiomhq/axiom-go/axiom/ingest" "github.com/gorilla/mux" + "io" + "net/http" ) func getAnalytics() Analytics { // this only a placeholder for now return Analytics{} } - // @Summary Generate AI Report // @Description Generate an AI-powered analytics report for a specific project // @Tags AI @@ -19,7 +24,7 @@ func getAnalytics() Analytics { // this only a placeholder for now // @Produce json // @Param project_id path string true "Project ID" // @Success 200 {object} response.Response{data=object} "Report generated successfully" -// @Failure 500 {object} response.Response "Internal server error" +// @Failure 500 {object} response.ErrorResponse500 "Internal server error" // @Router /projects/{project_id}/ai/report [get] // @Security BearerAuth func Report(app *config.Application) http.HandlerFunc { @@ -29,17 +34,281 @@ func Report(app *config.Application) http.HandlerFunc { projectID := vars["project_id"] // get user id from context - userID := r.Context().Value("user-id").(int) + userID := r.Context().Value("user-id").(int64) Analytics := getAnalytics() // TODO: get real analytics AI := config.AI report, err := getReport(projectID, userID, Analytics, AI) if err != nil { - response.InternalServerError(w, err.Error(), err) + response.InternalServerError(w, r, err.Error(), err) + config.AxiomLogger.IngestEvents(r.Context(), "ai-logs", []axiom.Event{ + { + ingest.TimestampField: time.Now(), + "project_id": projectID, + "user_id": userID, + "error": err.Error(), + "message": "Failed to generate AI report", + }, + }) + return + } + + config.AxiomLogger.IngestEvents(r.Context(), "ai-logs", []axiom.Event{ + { + ingest.TimestampField: time.Now(), + "project_id": projectID, + "user_id": userID, + "report": report, + "status": "success", + "message": "AI report generated successfully", + }, + }) + + response.OK(w, r, "Report generated successfully", report) + } +} + +// ChatBotAsk godoc +// @Summary Chat Bot Ask +// @Description This endpoint allows users to ask questions to the chatbot, which will respond using AI. It also saves the chat history for future reference. +// @Tags AI +// @Accept json +// @Produce json +// @Param project_id path string true "Project ID" +// @Param ChatBotRequest body ChatBotRequest true "Chat Bot Request" +// @Success 200 {object} response.Response{data=object} "Answer generated successfully" +// @Failure 500 {object} response.ErrorResponse500 "Internal server error" +// @Router /projects/{project_id}/ai/chatbot/ask [post] +// @Security BearerAuth +func ChatBotAsk(app *config.Application) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + + vars := mux.Vars(r) + projectOID := vars["project_id"] + projectID, err := GetProjectIDfromOID(r.Context(), config.DB, projectOID) + if err != nil { + response.InternalServerError(w, r, "Failed to get project ID", err) + return + } + userID64 := r.Context().Value("user-id").(int64) + userID := int(userID64) + + var userRequest ChatBotRequest + if err := json.NewDecoder(r.Body).Decode(&userRequest); err != nil { + response.BadRequest(w, r, "Invalid request body", err) + return + } + + transaction, err := config.DB.Begin(r.Context()) + if err != nil { + app.ErrorLog.Println(err.Error()) + response.InternalServerError(w, r, "Failed to start database transaction", err) + return + } + + // i should ignore this step if the client passed the chat history with the request + chat_data, err := GetOrCreateChatData(r.Context(), transaction, userID, projectID) + if err != nil { + app.ErrorLog.Println(err.Error()) + response.InternalServerError(w, r, "Failed to get or create chat data", err) + return + } + app.InfoLog.Printf("Chat data: %+v", chat_data) + + answer, err := config.AI.QueryChat(userRequest.Question) + if err != nil { + response.InternalServerError(w, r, err.Error(), err) + return + } + + err = SaveChatAction(r.Context(), transaction, chat_data.ID, userID, userRequest.Question, answer.ResponseText) + if err != nil { + response.InternalServerError(w, r, err.Error(), err) + return + } + + if err := transaction.Commit(r.Context()); err != nil { + app.ErrorLog.Println(err.Error()) + response.InternalServerError(w, r, "Failed to commit database transaction", err) + return + } + + response.OK(w, r, "Answer generated successfully", answer) + } +} + +// Agent godoc +// @Summary AI Agent Query +// @Description This endpoint allows users to query the AI agent with a prompt. The agent will respond with a schema change suggestion based on the prompt. +// @Tags AI +// @Accept json +// @Produce json +// @Param project_id path string true "Project ID" +// @Param Request body Request true "Request" +// @Success 200 {object} response.Response{data=AgentResponse} "Agent query successful" +// @Failure 400 {object} response.ErrorResponse400 "Bad request" +// @Failure 500 {object} response.ErrorResponse500 "Internal server error" +// @Router /projects/{project_id}/ai/agent [post] +// @Security JWTAuth +func Agent(app *config.Application) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + // get the request body + body, err := io.ReadAll(r.Body) + if err != nil { + response.BadRequest(w, r, "Failed to read request body", err) return } - response.OK(w, "Report generated successfully", report) + // parse the request body + request := &Request{} + err = json.Unmarshal(body, request) + if err != nil { + response.BadRequest(w, r, "Failed to parse request body", err) + return + } + // check if the request body is valid + if request.Prompt == "" { + response.BadRequest(w, r, "Prompt is required", nil) + return + } + + // get project id from path + vars := mux.Vars(r) + projectUID := vars["project_id"] + + // get user id from context + userID := r.Context().Value("user-id").(int64) + + AIresponse, err := AgentQuery(projectUID, userID, request.Prompt, config.AI) + if err != nil { + response.InternalServerError(w, r, "error while querying agent", err) + // log the error to Axiom + config.AxiomLogger.IngestEvents(r.Context(), "ai-logs", []axiom.Event{ + { + ingest.TimestampField: time.Now(), + "project_id": projectUID, + "user_id": userID, + "error": err.Error(), + "message": "Failed to query agent", + }, + }) + return + } + // log the success to Axiom + config.AxiomLogger.IngestEvents(r.Context(), "ai-logs", []axiom.Event{ + { + ingest.TimestampField: time.Now(), + "project_id": projectUID, + "user_id": userID, + "message": "Agent query successful", + "ddl": AIresponse.SchemaDDL, + "prompt": request.Prompt, + "SchemaChanges": AIresponse.SchemaChanges, + "response": AIresponse.Response, + }, + }) + + response.OK(w, r, "Agent query successful", AIresponse) } -} \ No newline at end of file +} + +// AgentAccept godoc +// @Summary Accept AI Agent Query +// @Description This endpoint allows users to accept the AI agent's query and execute the schema changes +// @Tags AI +// @Produce json +// @Param project_id path string true "Project ID" +// @Success 200 {object} response.Response "Query executed successfully" +// @Failure 400 {object} response.ErrorResponse400 "Bad request" +// @Failure 500 {object} response.ErrorResponse500 "Internal server error" +// @Router /projects/{project_id}/ai/agent/accept [post] +// @Security JWTAuth +func AgentAccept(app *config.Application) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + // get project id from path + vars := mux.Vars(r) + projectUID := vars["project_id"] + // get user id from context + userID := r.Context().Value("user-id").(int64) + + // execute the agent query + err := AgentExec(projectUID, userID, config.AI) + if err != nil { + if err.Error() == "changes expired or not found" { + response.BadRequest(w, r, "No schema changes found or changes expired", nil) + } else { + response.InternalServerError(w, r, "error while executing agent", err) + } + // log the error to Axiom + config.AxiomLogger.IngestEvents(r.Context(), "ai-logs", []axiom.Event{ + { + ingest.TimestampField: time.Now(), + "project_id": projectUID, + "user_id": userID, + "error": err.Error(), + "message": "Failed to execute agent query", + }, + }) + return + } + // log the success to Axiom + config.AxiomLogger.IngestEvents(r.Context(), "ai-logs", []axiom.Event{ + { + ingest.TimestampField: time.Now(), + "project_id": projectUID, + "user_id": userID, + "message": "Agent query executed successfully", + }, + }) + response.OK(w, r, "query executed successfully", nil) + } +} + +// AgentCancel godoc +// @Summary Cancel AI Agent Query +// @Description This endpoint allows users to cancel an AI agent query +// @Tags AI +// @Produce json +// @Param project_id path string true "Project ID" +// @Success 200 {object} response.Response "Agent query cancelled successfully" +// @Failure 400 {object} response.ErrorResponse400 "Bad request" +// @Failure 500 {object} response.ErrorResponse500 "Internal server error" +// @Router /projects/{project_id}/ai/agent/cancel [post] +// @Security JWTAuth +func AgentCancel(app *config.Application) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + // get project id from path + vars := mux.Vars(r) + projectUID := vars["project_id"] + // get user id from context + userID := r.Context().Value("user-id").(int64) + // cancel the agent query + err := ClearCacheForProject(projectUID) + if err != nil { + response.InternalServerError(w, r, "error while cancelling agent query", err) + // log the error to Axiom + config.AxiomLogger.IngestEvents(r.Context(), "ai-logs", []axiom.Event{ + { + ingest.TimestampField: time.Now(), + "project_id": projectUID, + "user_id": userID, + "error": err.Error(), + "message": "Failed to cancel agent query", + }, + }) + return + } + // log the cancellation to Axiom + config.AxiomLogger.IngestEvents(r.Context(), "ai-logs", []axiom.Event{ + { + ingest.TimestampField: time.Now(), + "project_id": projectUID, + "user_id": userID, + "message": "Agent query cancelled", + }, + }) + + response.OK(w, r, "Agent query cancelled successfully", nil) + } +} diff --git a/AI/models.go b/AI/models.go index 452786f..dfc3ed0 100644 --- a/AI/models.go +++ b/AI/models.go @@ -1,3 +1,32 @@ package ai -type Analytics struct {} \ No newline at end of file +import ( + "DBHS/utils" +) + +const SENDER_TYPE_AI = "ai" +const SENDER_TYPE_USER = "user" + + +type Analytics struct {} + +type ChatBotRequest struct { + Question string `json:"question"` +} + +type ChatData struct { + ID int `json:"id"` + Oid string `json:"oid"` + OwnerID int `json:"owner_id"` + ProjectID int `json:"project_id"` +} + +type Request struct { + Prompt string `json:"prompt"` +} + +type AgentResponse struct { + Response string `json:"response"` + SchemaChanges []utils.Table `json:"schema_changes"` + SchemaDDL string `json:"schema_ddl"` +} \ No newline at end of file diff --git a/AI/repository.go b/AI/repository.go index c900730..76beaed 100644 --- a/AI/repository.go +++ b/AI/repository.go @@ -132,6 +132,24 @@ const ( ) ORDER BY t.relname, i.relname;` + + + GET_USER_CHAT_FOR_PROJECT_QUERY = ` + SELECT id, oid, owner_id, project_id + FROM ai_chats + WHERE owner_id = $1 AND project_id = $2 + ` + + CREATE_NEW_CHAT_QUERY = ` + INSERT INTO ai_chats (oid, owner_id, project_id) + VALUES ($1, $2, $3) + RETURNING id, oid, owner_id, project_id; + ` + + SAVE_CHAT_MESSAGE_QUERY = ` + INSERT INTO chat_messages (chat_id, sender_type, content) + VALUES ($1, $2, $3) + ` ) // ExtractDatabaseSchema extracts the complete database schema as DDL statements @@ -326,3 +344,34 @@ func formatDataType(col TableColumn) string { return dataType } } + +func GetUserChatForProject(ctx context.Context, db utils.Querier, userID, projectID int) (ChatData, error) { + var data ChatData + err := pgxscan.Get(ctx, db, &data, GET_USER_CHAT_FOR_PROJECT_QUERY, userID, projectID) + if err != nil { + if strings.Contains(err.Error(), "no rows in result set") { + return ChatData{}, err + } + return ChatData{}, fmt.Errorf("failed to get user chat for project: %w", err) + } + return data, nil +} + +func CreateNewChat(ctx context.Context, db utils.Querier, oid string, userID, projectID int) (ChatData, error) { + var chat ChatData + err := pgxscan.Get(ctx, db, &chat, CREATE_NEW_CHAT_QUERY, oid, userID, projectID) + if err != nil { + return ChatData{}, fmt.Errorf("failed to create new chat: %w", err) + } + return chat, nil +} + +func SaveUserChatMessage(ctx context.Context, db utils.Querier, chatId int, message string) error { + _, err := db.Exec(ctx, SAVE_CHAT_MESSAGE_QUERY, chatId, SENDER_TYPE_USER, message) + return err +} + +func SaveAIChatMessage(ctx context.Context, db utils.Querier, chatId int, message string) error { + _, err := db.Exec(ctx, SAVE_CHAT_MESSAGE_QUERY, chatId, SENDER_TYPE_AI, message) + return err +} diff --git a/AI/routes.go b/AI/routes.go index 2928855..34b5cae 100644 --- a/AI/routes.go +++ b/AI/routes.go @@ -11,4 +11,9 @@ func DefineURLs() { AIProtected.Use(middleware.JwtAuthMiddleware, middleware.CheckOwnership) AIProtected.Handle("/report", middleware.MethodsAllowed(http.MethodGet)(Report(config.App))) + AIProtected.Handle(("/chatbot/ask"), middleware.MethodsAllowed(http.MethodPost)(ChatBotAsk(config.App))) + + AIProtected.Handle("/agent", middleware.MethodsAllowed(http.MethodPost)(Agent(config.App))) + AIProtected.Handle("/agent/accept", middleware.MethodsAllowed(http.MethodPost)(AgentAccept(config.App))) + AIProtected.Handle("/agent/cancel", middleware.MethodsAllowed(http.MethodPost)(AgentCancel(config.App))) } \ No newline at end of file diff --git a/AI/services.go b/AI/services.go index 839806c..f4f05b3 100644 --- a/AI/services.go +++ b/AI/services.go @@ -2,21 +2,27 @@ package ai import ( "DBHS/config" - "DBHS/tables" + "DBHS/utils" "context" "encoding/json" + "errors" + "regexp" + "time" + + "github.com/jackc/pgx/v5" + "github.com/Database-Hosting-Services/AI-Agent/RAG" ) -func getReport(projectUUID string, userID int, analytics Analytics, AI RAG.RAGmodel) (string, error) { +func getReport(projectUUID string, userID int64, analytics Analytics, AI RAG.RAGmodel) (string, error) { // get project name and connection - _, userDb, err := tables.ExtractDb(context.Background(), projectUUID, userID, config.DB) + _, userDb, err := utils.ExtractDb(context.Background(), projectUUID, userID, config.DB) if err != nil { return "", err } // get database schema - databaseSchema, err := ExtractDatabaseSchema(context.Background(), userDb) + databaseSchema, err := utils.ExtractDatabaseSchema(context.Background(), userDb) if err != nil { return "", err } @@ -35,3 +41,140 @@ func getReport(projectUUID string, userID int, analytics Analytics, AI RAG.RAGmo return report, nil } + +func SaveChatAction(ctx context.Context, db utils.Querier, chatId, userID int, question string, answer string) error { + // here i save the user prompt and the AI response together + // the chat action is a combination of the user question and the AI answer + if err := SaveUserChatMessage(ctx, db, chatId, question); err != nil { + return err + } + if err := SaveAIChatMessage(ctx, db, chatId, answer); err != nil { + return err + } + return nil +} + +func GetProjectIDfromOID(ctx context.Context, db utils.Querier, projectOID string) (int, error) { + var projectID int + err := db.QueryRow(ctx, "SELECT id FROM projects WHERE oid = $1", projectOID).Scan(&projectID) + if err != nil { + return 0, err + } + return projectID, nil +} + +func GetOrCreateChatData(ctx context.Context, db utils.Querier, userID, projectID int) (ChatData, error) { + // i suppose return the chat history if needed, currently it just returns the chat data + + chat, err := GetUserChatForProject(ctx, db, userID, projectID) + if err != nil { + if errors.Is(err, pgx.ErrNoRows) { + chat, err = CreateNewChat(ctx, db, utils.GenerateOID(), userID, projectID) + if err != nil { + return ChatData{}, err + } + } else { + return ChatData{}, err + } + } + return chat, nil +} + +func AgentQuery(projectUUID string, userID int64, prompt string, AI RAG.RAGmodel) (*RAG.AgentResponse, error) { + // get project name and connection + _, userDb, err := utils.ExtractDb(context.Background(), projectUUID, userID, config.DB) + if err != nil { + config.App.ErrorLog.Println("Error extracting database connection:", err) + return nil, err + } + + // get database schema + databaseSchema, err := ExtractDatabaseSchema(context.Background(), userDb) + if err != nil { + config.App.ErrorLog.Println("Error extracting database schema:", err) + return nil, err + } + + response, err := AI.QueryAgent("schemas-json", databaseSchema, prompt, 10) + if err != nil { + config.App.ErrorLog.Println("Error querying agent:", err) + return nil, err + } + + // remove the json code segment with all the code from the response + response.Response = removeJSONCodeSegments(response.Response) + + // add the schema changes to the cache + err = config.VerifyCache.Set("schema-changes:"+projectUUID, response.SchemaDDL, 10*time.Minute) + if err != nil { + config.App.ErrorLog.Println("Error adding schema changes to cache:", err) + return nil, err + } + return response, nil +} + +func AgentExec(projectUUID string, userID int64, AI RAG.RAGmodel) error { + // get project name and connection + _, userDb, err := utils.ExtractDb(context.Background(), projectUUID, userID, config.DB) + if err != nil { + config.App.ErrorLog.Println("Error extracting database connection:", err) + return err + } + + // get the DDL from the cache + ddl, err := config.VerifyCache.Get("schema-changes:"+projectUUID, nil) + if err != nil { + config.App.ErrorLog.Println("Error getting schema changes from cache:", err) + return err + } + + if ddl == nil { + config.App.ErrorLog.Println("No schema changes found in cache for project:", projectUUID) + return errors.New("changes expired or not found") + } + + // execute the DDL + tx, err := userDb.Begin(context.Background()) + if err != nil { + config.App.ErrorLog.Println("Error starting transaction:", err) + return err + } + defer tx.Rollback(context.Background()) + + _, err = tx.Exec(context.Background(), ddl.(string)) + if err != nil { + config.App.ErrorLog.Println("Error executing DDL:", err) + return err + } + if err := tx.Commit(context.Background()); err != nil { + config.App.ErrorLog.Println("Error committing transaction:", err) + return err + } + return nil +} + +func ClearCacheForProject(projectUUID string) error { + // clear the cache for the project + err := config.VerifyCache.Delete("schema-changes:" + projectUUID) + if err != nil { + config.App.ErrorLog.Println("Error clearing cache for project:", projectUUID, "Error:", err) + return err + } + return nil +} + +func removeJSONCodeSegments(text string) string { + // Remove JSON code blocks (```json...``` or ```...``` containing JSON) + jsonCodeBlockRegex := regexp.MustCompile("(?s)```(?:json)?\\s*\\{.*?\\}```") + text = jsonCodeBlockRegex.ReplaceAllString(text, "") + + // Remove any remaining triple backticks blocks that might contain JSON + codeBlockRegex := regexp.MustCompile("(?s)```[^`]*```") + text = codeBlockRegex.ReplaceAllString(text, "") + + // Remove inline JSON code segments (single backticks containing JSON-like content) + inlineJSONRegex := regexp.MustCompile("`[^`]*\\{[^}]*\\}[^`]*`") + text = inlineJSONRegex.ReplaceAllString(text, "") + + return text +} diff --git a/SqlEditor/handlers.go b/SqlEditor/handlers.go new file mode 100644 index 0000000..bad16fb --- /dev/null +++ b/SqlEditor/handlers.go @@ -0,0 +1,77 @@ +package sqleditor + +import ( + "DBHS/config" + "DBHS/response" + "DBHS/utils" + "encoding/json" + "net/http" + + "github.com/gorilla/mux" +) + +// ResponseBodySwagger is a swagger documentation model for ResponseBody +// @Description SQL query execution response with results and metadata +type ResponseBodySwagger struct { + // The JSON result of the query execution + Result json.RawMessage `json:"result" swaggertype:"string" example:"[{\"id\":1,\"name\":\"test\"}]"` + // Names of columns in the result set + ColumnNames []string `json:"column_names" example:"[\"id\",\"name\"]"` + // Query execution time in milliseconds + ExecutionTime float64 `json:"execution_time" example:"10.45"` +} + +// RunSqlQuery godoc +// @Summary Execute SQL query on project database +// @Description Execute a dynamic SQL query against a specific project's PostgreSQL database and return structured JSON results with metadata including column names and execution time +// @Tags sqlEditor +// @Accept json +// @Produce json +// @Param project_id path string true "Project ID (OID)" +// @Param query body sqleditor.RequestBody true "SQL query to execute" +// @Security BearerAuth +// @Success 200 {object} response.SuccessResponse{data=sqleditor.ResponseBodySwagger} "Query executed successfully with results, column names, and execution time" +// @Failure 400 {object} response.ErrorResponse "Project ID missing, invalid request body, empty query, or dangerous SQL operations detected" +// @Failure 401 {object} response.ErrorResponse "Unauthorized access" +// @Failure 404 {object} response.ErrorResponse "Project not found or referenced table/column does not exist" +// @Failure 500 {object} response.ErrorResponse "Internal server error, query execution failed, or error parsing results" +// @Router /projects/{project_id}/sqlEditor/run-query [post] +func RunSqlQuery(app *config.Application) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + urlVariables := mux.Vars(r) + projectOid := urlVariables["project_id"] + if projectOid == "" { + response.BadRequest(w, r, "Project Id is required", nil) + return + } + + // Get the request body + var RequestBody RequestBody + if err := json.NewDecoder(r.Body).Decode(&RequestBody); err != nil { + response.BadRequest(w, r, "Invalid request body", nil) + return + } + + if RequestBody.Query == "" { + response.BadRequest(w, r, "Query is required", nil) + return + } + + // Validate query for dangerous operations + isValid, err := ValidateQuery(RequestBody.Query) + if !isValid { + response.BadRequest(w, r, err.Error(), nil) + return + } + + // Get the query response + queryResponse, apiErr := GetQueryResponse(r.Context(), config.DB, projectOid, RequestBody.Query) + if apiErr.Error() != nil { + utils.ResponseHandler(w, r, apiErr) + return + } + + response.OK(w, r, "Query executed successfully", queryResponse) + config.App.InfoLog.Println("Query executed successfully for project:", projectOid) + } +} diff --git a/SqlEditor/models.go b/SqlEditor/models.go new file mode 100644 index 0000000..61b7eff --- /dev/null +++ b/SqlEditor/models.go @@ -0,0 +1,13 @@ +package sqleditor + +import "encoding/json" + +type RequestBody struct { + Query string `json:"query"` +} + +type ResponseBody struct { + Result json.RawMessage `json:"result"` + ColumnNames []string `json:"column_names"` + ExecutionTime float64 `json:"execution_time"` +} diff --git a/SqlEditor/repository.go b/SqlEditor/repository.go new file mode 100644 index 0000000..94368d1 --- /dev/null +++ b/SqlEditor/repository.go @@ -0,0 +1,70 @@ +package sqleditor + +import ( + api "DBHS/utils/apiError" + "context" + "encoding/json" + "errors" + "fmt" + "strings" + "time" + + "github.com/jackc/pgx/v5/pgxpool" + "github.com/pingcap/tidb/parser" + // "github.com/pingcap/tidb/parser/ast" + _ "github.com/pingcap/tidb/parser/test_driver" +) + +// validateSQLWithParser validates SQL syntax using TiDB SQL parser +func validateSQLWithParser(query string) error { + p := parser.New() + + // Parse the SQL statement + stmts, _, err := p.Parse(query, "", "") + if err != nil { + return fmt.Errorf("SQL syntax error: %v", err) + } + + // Check if we have any statements + if len(stmts) == 0 { + return errors.New("empty SQL statement") + } + + return nil +} + +func FetchQueryData(ctx context.Context, conn *pgxpool.Pool, query string) (ResponseBody, api.ApiError) { + // Validate SQL syntax using parser + if err := validateSQLWithParser(query); err != nil { + return ResponseBody{}, *api.NewApiError("Invalid SQL syntax", 400, err) + } + + wrappedQuery := WrapQueryWithJSONAgg(query) + + var result string + startTime := time.Now() + err := conn.QueryRow(ctx, wrappedQuery).Scan(&result) + if err != nil { + if strings.Contains(err.Error(), "does not exist") { + return ResponseBody{}, *api.NewApiError("Table/Column not found", 404, errors.New("Table or column does not exist: "+err.Error())) + } + if strings.Contains(err.Error(), "cannot scan NULL into *string") { + return ResponseBody{}, *api.NewApiError("There is no data", 200, errors.New("No data found for the query")) + } + return ResponseBody{}, *api.NewApiError("Query execution failed", 500, errors.New("failed to execute query: "+err.Error())) + } + + // Extract column names from the JSON result + columnNames, err := ExtractColumnNames(result) + + if err != nil { + return ResponseBody{}, *api.NewApiError("Internal server error", 500, errors.New("failed to extract column names: "+err.Error())) + } + + executionTime := time.Since(startTime) + return ResponseBody{ + Result: json.RawMessage(result), + ColumnNames: columnNames, + ExecutionTime: float64(executionTime.Nanoseconds()) / 1e6, // Convert to milliseconds as float64 + }, api.ApiError{} // Return empty ApiError to indicate success +} diff --git a/SqlEditor/routes.go b/SqlEditor/routes.go new file mode 100644 index 0000000..5e86147 --- /dev/null +++ b/SqlEditor/routes.go @@ -0,0 +1,16 @@ +package sqleditor + +import ( + "DBHS/config" + "DBHS/middleware" + "net/http" +) + +func DefineURLs() { + router := config.Router.PathPrefix("/api/projects/{project_id}/sqlEditor").Subrouter() + router.Use(middleware.JwtAuthMiddleware) + + router.Handle("/run-query", middleware.Route(map[string]http.HandlerFunc{ + http.MethodPost: RunSqlQuery(config.App), + })) +} diff --git a/SqlEditor/service.go b/SqlEditor/service.go new file mode 100644 index 0000000..656e8d8 --- /dev/null +++ b/SqlEditor/service.go @@ -0,0 +1,36 @@ +package sqleditor + +import ( + "DBHS/indexes" + api "DBHS/utils/apiError" + "context" + "errors" + + "github.com/jackc/pgx/v5/pgxpool" +) + +func GetQueryResponse(ctx context.Context, db *pgxpool.Pool, projectOid, query string) (ResponseBody, api.ApiError) { + owner_id, ok := ctx.Value("user-id").(int64) + if !ok || owner_id == 0 { + return ResponseBody{}, *api.NewApiError("Unauthorized", 401, errors.New("user is not authorized")) + } + + // ------------------------ Get the project pool connection ------------------------ + conn, err := indexes.ProjectPoolConnection(ctx, db, owner_id, projectOid) + if err != nil { + if err.Error() == "Project not found" || err.Error() == "connection pool not found" { + return ResponseBody{}, *api.NewApiError("Project not found", 404, errors.New(err.Error())) + } + return ResponseBody{}, *api.NewApiError("Internal server error", 500, errors.New(err.Error())) + } + defer conn.Close() + + // ------------------------ Fetch the query data ------------------------ + + requestBody, apiErr := FetchQueryData(ctx, conn, query) + if apiErr.Error() != nil { + return ResponseBody{}, apiErr + } + + return requestBody, api.ApiError{} +} diff --git a/SqlEditor/utils.go b/SqlEditor/utils.go new file mode 100644 index 0000000..0456d93 --- /dev/null +++ b/SqlEditor/utils.go @@ -0,0 +1,125 @@ +package sqleditor + +import ( + "encoding/json" + "errors" + "fmt" + "regexp" + "strings" +) + +// List of dangerous SQL keywords/operations +var dangerousOperations = []string{ + "CREATE", + "DROP", + "ALTER", + "TRUNCATE", + "GRANT", + "REVOKE", + "EXEC", + "EXECUTE", + "CALL", + "MERGE", + "REPLACE", + "RENAME", + "COMMENT", +} + +// Additional check for dangerous patterns +var dangerousPatterns = []string{ + `\bINTO\s+OUTFILE\b`, // File operations + `\bLOAD_FILE\b`, // File operations + `\bSYSTEM\b`, // System commands + `\bSHELL\b`, // Shell commands +} + +// System tables and schemas that should never be updated +var protectedTables = []string{ + "PG_DATABASE", + "PG_CLASS", + "PG_NAMESPACE", + "PG_TABLES", + "PG_ATTRIBUTE", + "INFORMATION_SCHEMA", +} + +// ValidateQuery checks if the SQL query contains dangerous operations +// Returns true if the query is safe (allows SELECT, INSERT, DELETE, and UPDATE), false if it contains dangerous operations +func ValidateQuery(sqlQuery string) (bool, error) { + // Convert to uppercase for case-insensitive matching + upperQuery := strings.ToUpper(strings.TrimSpace(sqlQuery)) + + // Check for dangerous operations at the beginning of statements + // Split by semicolon to handle multiple statements + statements := strings.Split(upperQuery, ";") + + for _, statement := range statements { + statement = strings.TrimSpace(statement) + if statement == "" { + continue + } + + // Check if statement starts with any dangerous operation + for _, operation := range dangerousOperations { + // Use regex to match word boundaries to avoid false positives + pattern := fmt.Sprintf(`^\s*%s\b`, regexp.QuoteMeta(operation)) + matched, _ := regexp.MatchString(pattern, statement) + if matched { + return false, fmt.Errorf("query contains forbidden operation: %s", operation) + } + } + + for _, pattern := range dangerousPatterns { + matched, _ := regexp.MatchString(pattern, statement) + if matched { + return false, errors.New("query contains forbidden file or system operations") + } + } + + // Check if UPDATE statement targets protected system tables + if strings.HasPrefix(statement, "UPDATE") { + for _, protectedTable := range protectedTables { + // Check if the statement contains references to protected tables + pattern := fmt.Sprintf(`\b%s`, regexp.QuoteMeta(protectedTable)) + matched, _ := regexp.MatchString(pattern, statement) + if matched { + return false, fmt.Errorf("cannot update protected system table/schema: %s", protectedTable) + } + } + } + } + + return true, nil +} + +// WrapQueryWithJSONAgg takes a SQL query and wraps it with json_agg and row_to_json +func WrapQueryWithJSONAgg(sqlQuery string) string { + // Clean the input query by removing leading/trailing whitespace and semicolon + cleanQuery := strings.TrimSuffix(strings.TrimSpace(sqlQuery), ";") + // Build the wrapped query + wrappedQuery := fmt.Sprintf(`SELECT json_agg(row_to_json(t))::text AS result FROM (%s) t;`, cleanQuery) + return wrappedQuery +} + +// extractColumnNames extracts column names from the JSON result +func ExtractColumnNames(jsonResult string) ([]string, error) { + var data []map[string]interface{} + + err := json.Unmarshal([]byte(jsonResult), &data) + if err != nil { + return nil, err + } + + // If no data, return empty slice + if len(data) == 0 { + return []string{}, nil + } + + // Extract column names from the first row + var columnNames []string + for key := range data[0] { + columnNames = append(columnNames, key) + } + + return columnNames, nil +} diff --git a/accounts/handlers.go b/accounts/handlers.go index bc0776b..e7b9e28 100644 --- a/accounts/handlers.go +++ b/accounts/handlers.go @@ -8,9 +8,10 @@ import ( "encoding/json" "errors" "fmt" + "net/http" + "strings" "github.com/gorilla/mux" "github.com/redis/go-redis/v9" - "net/http" ) // GetUserData godoc @@ -31,7 +32,7 @@ func getUserData(app *config.Application) http.HandlerFunc { err := GetUserDataService(r.Context(), config.DB, userId, user) if err != nil { app.ErrorLog.Println(err.Error()) - response.InternalServerError(w, "Internal Server Error", err) + response.InternalServerError(w, r, "Internal Server Error", err) return } @@ -43,7 +44,7 @@ func getUserData(app *config.Application) http.HandlerFunc { "created_at": user.CreatedAt, } - response.OK(w, "User data fetched", ret) + response.OK(w, r, "User data fetched", ret) } } @@ -62,24 +63,24 @@ func signUp(app *config.Application) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { var user UserUnVerified if err := json.NewDecoder(r.Body).Decode(&user); err != nil { - response.BadRequest(w, "Invalid Input", err) + response.BadRequest(w, r, "Invalid Input", err) return } if err := checkPasswordStrength(user.Password); err != nil { - response.BadRequest(w, "Invalid Password", err) + response.BadRequest(w, r, "Invalid Password", err) return } field, err := checkUserExistsInCache(user.Username, user.Email) if err != nil { app.ErrorLog.Println(err.Error()) - response.InternalServerError(w, "Server Error", err) + response.InternalServerError(w, r, "Server Error", err) return } if field != "" { - response.BadRequest(w, + response.BadRequest(w, r, "Invalid User", errors.New(fmt.Sprintf("User with this %s already exists", field)), ) @@ -89,23 +90,23 @@ func signUp(app *config.Application) http.HandlerFunc { // this return the field that exists in the database field, err = checkUserExists(r.Context(), config.DB, user.Username, user.Email) // we can make it more generic if err != nil { - response.BadRequest(w, "Invalid Input Data", err) + response.BadRequest(w, r, "Invalid Input Data", err) return } if field != "" { - response.BadRequest(w, fmt.Sprintf("Invalid input Data, this %s is already exists", field), nil) + response.BadRequest(w, r, fmt.Sprintf("Invalid input Data, this %s is already exists", field), nil) return } err = SignupUser(context.Background(), config.DB, &user) if err != nil { app.ErrorLog.Println(err.Error()) - response.InternalServerError(w, "Server Error, please try again later.", err) + response.InternalServerError(w, r, "Server Error, please try again later.", err) return } - response.Created(w, "User signed up successfully, check your email for verification", nil) + response.Created(w, r, "User signed up successfully, check your email for verification", nil) } } @@ -125,31 +126,31 @@ func SignIn(app *config.Application) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { var user UserSignIn if err := json.NewDecoder(r.Body).Decode(&user); err != nil { - response.BadRequest(w, "Invalid JSON body", err) + response.BadRequest(w, r, "Invalid JSON body", err) return } if user.Email == "" || user.Password == "" { - response.BadRequest(w, "Email and Password are required", nil) + response.BadRequest(w, r, "Email and Password are required", nil) return } resp, err := SignInUser(r.Context(), config.DB, config.VerifyCache, &user) if err != nil { - if err.Error() == "no rows in result set" || err.Error() == "InCorrect Email or Password" { - response.BadRequest(w, "InCorrect Email or Password", nil) + if strings.Contains(err.Error(), "scan") || err.Error() == "no rows in result set" || err.Error() == "InCorrect Email or Password" { + response.BadRequest(w, r, "InCorrect Email or Password", nil) return } app.InfoLog.Println(err.Error()) - response.InternalServerError(w, "Server Error, please try again later.", err) + response.InternalServerError(w, r, "Server Error, please try again later.", err) return } verification, ok := resp["Verification"].(string) if ok { - response.Redirect(w, verification, resp) + response.Redirect(w, r, verification, resp) } else { - response.OK(w, "User signed in successfully", resp) + response.OK(w, r, "User signed in successfully", resp) } } } @@ -170,7 +171,7 @@ func Verify(app *config.Application) http.HandlerFunc { decoder := json.NewDecoder(r.Body) if err := decoder.Decode(&user); err != nil { app.ErrorLog.Println(err.Error()) - response.BadRequest(w, "Invalid JSON body", err) + response.BadRequest(w, r, "Invalid JSON body", err) return } @@ -180,17 +181,17 @@ func Verify(app *config.Application) http.HandlerFunc { if err.Error() == "Wrong verification code" || err == redis.Nil { switch err.Error() { case "Wrong verification code": - response.BadRequest(w, err.Error(), nil) + response.BadRequest(w, r, err.Error(), nil) return case redis.Nil.Error(): - response.BadRequest(w, "email not found please sign up first", nil) + response.BadRequest(w, r, "email not found please sign up first", nil) return } } - response.InternalServerError(w, "Server Error, please try again later.", err) + response.InternalServerError(w, r, "Server Error, please try again later.", err) return } - response.Created(w, "User verified successfully", data) + response.Created(w, r, "User verified successfully", data) } } @@ -209,21 +210,21 @@ func resendCode(app *config.Application) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { var user UserSignIn if err := json.NewDecoder(r.Body).Decode(&user); err != nil { - response.BadRequest(w, "Invalid JSON body", err) + response.BadRequest(w, r, "Invalid JSON body", err) return } err := UpdateVerificationCode(config.VerifyCache, user) if err != nil { if err.Error() == "invalid email" { - response.BadRequest(w, "Invalid Email", err) + response.BadRequest(w, r, "Invalid Email", err) return } - response.InternalServerError(w, "Server Error, please try again later.", err) + response.InternalServerError(w, r, "Server Error, please try again later.", err) return } app.InfoLog.Println("Verification code sent successfully", user.Email) - response.OK(w, "Verification code sent successfully", nil) + response.OK(w, r, "Verification code sent successfully", nil) } } @@ -244,16 +245,16 @@ func UpdatePassword(app *config.Application) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { var UserPassword UpdatePasswordModel if err := json.NewDecoder(r.Body).Decode(&UserPassword); err != nil { - response.BadRequest(w, "Invalid JSON body", err) + response.BadRequest(w, r, "Invalid JSON body", err) return } err := UpdateUserPassword(r.Context(), config.DB, &UserPassword) if err != nil { app.ErrorLog.Println(err.Error()) - response.InternalServerError(w, "Server Error, please try again later.", err) + response.InternalServerError(w, r, "Server Error, please try again later.", err) return } - response.OK(w, "Password updated successfully", nil) + response.OK(w, r, "Password updated successfully", nil) } } @@ -277,13 +278,13 @@ func UpdateUser(app *config.Application) http.HandlerFunc { userOid := urlVariables["id"] if userOid == "" { - response.BadRequest(w, "User Id is required", nil) + response.BadRequest(w, r, "User Id is required", nil) return } var requestData UpdateUserRequest if err := json.NewDecoder(r.Body).Decode(&requestData); err != nil { - response.BadRequest(w, "Invalid Input Data", err) + response.BadRequest(w, r, "Invalid Input Data", err) return } defer r.Body.Close() @@ -291,27 +292,27 @@ func UpdateUser(app *config.Application) http.HandlerFunc { transaction, err := config.DB.Begin(r.Context()) if err != nil { app.ErrorLog.Println(err.Error()) - response.InternalServerError(w, "Server Error, please try again later.", err) + response.InternalServerError(w, r, "Server Error, please try again later.", err) return } fieldsToUpdate, newValues, err := utils.GetNonZeroFieldsFromStruct(&requestData) if err != nil { - response.BadRequest(w, "Invalid Input Data", err) + response.BadRequest(w, r, "Invalid Input Data", err) return } query, err := BuildUserUpdateQuery(userOid, fieldsToUpdate) if err != nil { app.ErrorLog.Println(err.Error()) - response.InternalServerError(w, "Internal Server Error", err) + response.InternalServerError(w, r, "Internal Server Error", err) return } err = UpdateUserData(r.Context(), transaction, query, newValues) if err != nil { app.ErrorLog.Println(err.Error()) - response.InternalServerError(w, "Internal Server Error", err) + response.InternalServerError(w, r, "Internal Server Error", err) return } @@ -323,11 +324,11 @@ func UpdateUser(app *config.Application) http.HandlerFunc { if err := transaction.Commit(r.Context()); err != nil { app.ErrorLog.Println(err.Error()) - response.InternalServerError(w, "Server Error, please try again later.", err) + response.InternalServerError(w, r, "Server Error, please try again later.", err) return } - response.OK(w, "User's data updated successfully", Data) + response.OK(w, r, "User's data updated successfully", Data) } } @@ -345,21 +346,21 @@ func ForgetPassword(app *config.Application) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { var user User if err := json.NewDecoder(r.Body).Decode(&user); err != nil { - response.BadRequest(w, "Invalid JSON body", err) + response.BadRequest(w, r, "Invalid JSON body", err) return } err := ForgetPasswordService(r.Context(), config.DB, config.VerifyCache, user.Email) if err != nil { app.ErrorLog.Println(err.Error()) if err.Error() == "User does not exist" { - response.BadRequest(w, err.Error(), err) + response.BadRequest(w, r, err.Error(), err) return } - response.InternalServerError(w, "Server Error, please try again later.", err) + response.InternalServerError(w, r, "Server Error, please try again later.", err) return } - response.OK(w, "Verification Code Sent", nil) + response.OK(w, r, "Verification Code Sent", nil) } } @@ -377,7 +378,7 @@ func ForgetPasswordVerify(app *config.Application) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { var body ResetPasswordForm if err := json.NewDecoder(r.Body).Decode(&body); err != nil { - response.BadRequest(w, "Invalid JSON body", err) + response.BadRequest(w, r, "Invalid JSON body", err) return } @@ -387,16 +388,16 @@ func ForgetPasswordVerify(app *config.Application) http.HandlerFunc { if err.Error() == "Wrong verification code" || err == redis.Nil { switch err.Error() { case "Wrong verification code": - response.BadRequest(w, err.Error(), nil) + response.BadRequest(w, r, err.Error(), nil) return case redis.Nil.Error(): - response.BadRequest(w, "email not found please sign up first", nil) + response.BadRequest(w, r, "email not found please sign up first", nil) return } } - response.InternalServerError(w, "Server Error, please try again later.", err) + response.InternalServerError(w, r, "Server Error, please try again later.", err) return } - response.OK(w, "Password updated successfully", nil) + response.OK(w, r, "Password updated successfully", nil) } } diff --git a/accounts/repository.go b/accounts/repository.go index ccf0694..c97d6b7 100644 --- a/accounts/repository.go +++ b/accounts/repository.go @@ -5,6 +5,8 @@ import ( "fmt" "DBHS/utils" + + "github.com/georgysavva/scany/v2/pgxscan" "github.com/jackc/pgx/v5" ) @@ -42,8 +44,9 @@ func CreateUser(ctx context.Context, db pgx.Tx, user *User) error { */ -func GetUser(ctx context.Context, db utils.Querier, SearchField interface{}, query string, dest ...interface{}) error { - err := db.QueryRow(ctx, query, SearchField).Scan(dest...) +func GetUser(ctx context.Context, db utils.Querier, SearchField interface{}, query string, dest *User) error { + err := pgxscan.Get(ctx, db, dest, query, SearchField) + if err != nil { if err == pgx.ErrNoRows { return fmt.Errorf("user with %s not found", SearchField) diff --git a/accounts/service.go b/accounts/service.go index 1df8d89..21041ba 100644 --- a/accounts/service.go +++ b/accounts/service.go @@ -57,16 +57,7 @@ func SignInUser(ctx context.Context, db *pgxpool.Pool, cache *caching.RedisClien } var authenticatedUser User - err = GetUser(ctx, db, user.Email, SELECT_USER_BY_Email, []interface{}{ - &authenticatedUser.ID, - &authenticatedUser.OID, - &authenticatedUser.Username, - &authenticatedUser.Email, - &authenticatedUser.Password, - &authenticatedUser.Image, - &authenticatedUser.CreatedAt, - &authenticatedUser.LastLogin, - }...) + err = GetUser(ctx, db, user.Email, SELECT_USER_BY_Email, &authenticatedUser) if err != nil { if err.Error() == "user with "+user.Email+" not found" { @@ -187,7 +178,7 @@ func VerifyUser(ctx context.Context, db *pgxpool.Pool, cache *caching.RedisClien return nil, err } - if err := GetUser(ctx, transaction, user.Email, SELECT_ID_FROM_USER_BY_EMAIL, []interface{}{&user.ID}...); err != nil { + if err := GetUser(ctx, transaction, user.Email, SELECT_ID_FROM_USER_BY_EMAIL, &user.User); err != nil { return nil, err } @@ -223,16 +214,7 @@ func VerifyUser(ctx context.Context, db *pgxpool.Pool, cache *caching.RedisClien func ForgetPasswordService(ctx context.Context, db *pgxpool.Pool, cache *caching.RedisClient, email string) error { // check if a user exist with this email var user UserUnVerified - err := GetUser(ctx, db, email, SELECT_USER_BY_Email, []interface{}{ - &user.ID, - &user.OID, - &user.Username, - &user.Email, - &user.Password, - &user.Image, - &user.CreatedAt, - &user.LastLogin, - }...) + err := GetUser(ctx, db, email, SELECT_USER_BY_Email, &user.User) if err != nil { return fmt.Errorf("User does not exist") @@ -261,16 +243,7 @@ func ForgetPasswordVerifyService(ctx context.Context, db *pgxpool.Pool, cache *c return fmt.Errorf("Wrong verification code") } - if err := GetUser(ctx, db, resetForm.Email, SELECT_USER_BY_Email, []interface{}{ - &user.ID, - &user.OID, - &user.Username, - &user.Email, - &user.Password, - &user.Image, - &user.CreatedAt, - &user.LastLogin, - }...); err != nil { + if err := GetUser(ctx, db, resetForm.Email, SELECT_USER_BY_Email, &user.User); err != nil { return err } @@ -303,14 +276,5 @@ func UpdateUserData(ctx context.Context, db pgx.Tx, query string, args []interfa } func GetUserDataService(ctx context.Context, db *pgxpool.Pool, userId interface{}, dist *User) error { - return GetUser(ctx, db, userId, SELECT_USER_BY_ID, []interface{}{ - &dist.ID, - &dist.OID, - &dist.Username, - &dist.Email, - &dist.Password, - &dist.Image, - &dist.CreatedAt, - &dist.LastLogin, - }...) + return GetUser(ctx, db, userId, SELECT_USER_BY_ID, dist) } diff --git a/analytics/SqlQueries.go b/analytics/SqlQueries.go index 06d3f4f..f42f5a0 100644 --- a/analytics/SqlQueries.go +++ b/analytics/SqlQueries.go @@ -11,8 +11,8 @@ const ( GET_TOTAL_TimeAndQueries = ` SELECT - ROUND(SUM(total_exec_time)::numeric, 2) as total_time_ms, - SUM(calls) as total_queries + COALESCE(ROUND(SUM(total_exec_time)::numeric, 2), 0) as total_time_ms, + COALESCE(SUM(calls), 0) as total_queries FROM pg_stat_statements pss JOIN pg_database pd ON pss.dbid = pd.oid WHERE pd.datname = $1 @@ -20,9 +20,9 @@ const ( GET_READ_WRITE_CPU = ` SELECT - SUM(CASE WHEN query ILIKE 'SELECT%' THEN calls ELSE 0 END) as read_queries, - SUM(CASE WHEN query ILIKE ANY(ARRAY['INSERT%', 'UPDATE%', 'DELETE%']) THEN calls ELSE 0 END) as write_queries, - ROUND(SUM(total_exec_time)::numeric, 2) as total_cpu_time_ms + COALESCE(SUM(CASE WHEN query ILIKE 'SELECT%' THEN calls ELSE 0 END), 0) as read_queries, + COALESCE(SUM(CASE WHEN query ILIKE ANY(ARRAY['INSERT%', 'UPDATE%', 'DELETE%']) THEN calls ELSE 0 END), 0) as write_queries, + COALESCE(ROUND(SUM(total_exec_time)::numeric, 2), 0) as total_cpu_time_ms FROM pg_stat_statements pss JOIN pg_database pd ON pss.dbid = pd.oid WHERE pd.datname = current_database() @@ -31,13 +31,13 @@ const ( GET_ALL_CURRENT_STORAGE = `SELECT created_at::text, data->>'Management storage', data->>'Actual data' FROM analytics WHERE type = 'Storage' and "projectId" = $1 ORDER BY created_at DESC;` - GET_ALL_EXECUTION_TIME_STATS = `SELECT created_at::text, (data->>'total_time_ms')::numeric, (data->>'total_queries')::bigint FROM analytics WHERE type = 'ExecutionTimeStats' AND "projectId" = $1; ` + GET_ALL_EXECUTION_TIME_STATS = `SELECT created_at::text, COALESCE((data->>'total_time_ms')::numeric, 0), COALESCE((data->>'total_queries')::bigint, 0) FROM analytics WHERE type = 'ExecutionTimeStats' AND "projectId" = $1; ` - GET_ALL_DATABASE_USAGE_STATS = `SELECT created_at::text, (data->>'read_write_cost')::numeric, (data->>'cpu_cost')::numeric, (data->>'total_cost')::numeric FROM analytics WHERE type = 'DatabaseUsageStats' AND "projectId" = $1;` + GET_ALL_DATABASE_USAGE_STATS = `SELECT created_at::text, COALESCE((data->>'read_write_cost')::numeric, 0), COALESCE((data->>'cpu_cost')::numeric, 0), COALESCE((data->>'total_cost')::numeric, 0) FROM analytics WHERE type = 'DatabaseUsageStats' AND "projectId" = $1;` // Queries to get the last records for each type of analytics - GET_LAST_EXECUTION_TIME_STATS = `SELECT created_at::text, (data->>'total_time_ms')::numeric, (data->>'total_queries')::bigint FROM analytics WHERE type = 'ExecutionTimeStats' AND "projectId" = $1 ORDER BY created_at DESC LIMIT 1;` + GET_LAST_EXECUTION_TIME_STATS = `SELECT created_at::text, COALESCE((data->>'total_time_ms')::numeric, 0), COALESCE((data->>'total_queries')::bigint, 0) FROM analytics WHERE type = 'ExecutionTimeStats' AND "projectId" = $1 ORDER BY created_at DESC LIMIT 1;` - GET_LAST_DATABASE_USAGE_STATS = `SELECT created_at::text, (data->>'read_write_cost')::numeric, (data->>'cpu_cost')::numeric, (data->>'total_cost')::numeric FROM analytics WHERE type = 'DatabaseUsageStats' AND "projectId" = $1 ORDER BY created_at DESC LIMIT 1;` + GET_LAST_DATABASE_USAGE_STATS = `SELECT created_at::text, COALESCE((data->>'read_write_cost')::numeric, 0), COALESCE((data->>'cpu_cost')::numeric, 0), COALESCE((data->>'total_cost')::numeric, 0) FROM analytics WHERE type = 'DatabaseUsageStats' AND "projectId" = $1 ORDER BY created_at DESC LIMIT 1;` ) diff --git a/analytics/handlers.go b/analytics/handlers.go index 5f084fd..ddeded6 100644 --- a/analytics/handlers.go +++ b/analytics/handlers.go @@ -27,7 +27,7 @@ func CurrentStorage(app *config.Application) http.HandlerFunc { urlVariables := mux.Vars(r) projectOid := urlVariables["project_id"] if projectOid == "" { - response.BadRequest(w, "Project Id is required", nil) + response.BadRequest(w, r, "Project Id is required", nil) return } @@ -37,10 +37,10 @@ func CurrentStorage(app *config.Application) http.HandlerFunc { return } if len(storage) == 0 { - response.NotFound(w, "No storage information found for the project", nil) + response.NotFound(w, r, "No storage information found for the project", nil) return } - response.OK(w, "Storage history retrieved successfully", storage) + response.OK(w, r, "Storage history retrieved successfully", StorageResponse) } } @@ -62,17 +62,17 @@ func ExecutionTime(app *config.Application) http.HandlerFunc { urlVariables := mux.Vars(r) projectOid := urlVariables["project_id"] if projectOid == "" { - response.BadRequest(w, "Project Id is required", nil) + response.BadRequest(w, r, "Project Id is required", nil) return } - stats, apiErr := GetALLExecutionTimeStats(r.Context(), config.DB, projectOid) + _, apiErr := GetALLExecutionTimeStats(r.Context(), config.DB, projectOid) if apiErr.Error() != nil { utils.ResponseHandler(w, r, apiErr) return } - response.OK(w, "Execution time statistics retrieved successfully", stats) + response.OK(w, r, "Execution time statistics retrieved successfully", DatabaseActivityResponse) } } @@ -94,16 +94,16 @@ func DatabaseUsage(app *config.Application) http.HandlerFunc { urlVariables := mux.Vars(r) projectOid := urlVariables["project_id"] if projectOid == "" { - response.BadRequest(w, "Project Id is required", nil) + response.BadRequest(w, r, "Project Id is required", nil) return } - stats, apiErr := GetALLDatabaseUsageStats(r.Context(), config.DB, projectOid) + _, apiErr := GetALLDatabaseUsageStats(r.Context(), config.DB, projectOid) if apiErr.Error() != nil { utils.ResponseHandler(w, r, apiErr) return } - response.OK(w, "Database usage statistics retrieved successfully", stats) + response.OK(w, r, "Database usage statistics retrieved successfully", DatabaseUsageStatsResponse) } } diff --git a/analytics/models.go b/analytics/models.go index a93cb0b..7c021b2 100644 --- a/analytics/models.go +++ b/analytics/models.go @@ -43,3 +43,177 @@ type DatabaseUsageCostWithDates struct { CPUCost float64 `json:"cpu_cost"` TotalCost float64 `json:"total_cost"` } + +var ( + // Storage dummy data - showing growth over time (all in KB, under 100) + StorageResponse = []StorageWithDates{ + { + Timestamp: "2025-01-01T00:00:00Z", + ManagementStorage: "12 kB", + ActualData: "45 kB", + }, + { + Timestamp: "2025-01-02T00:00:00Z", + ManagementStorage: "14 kB", + ActualData: "52 kB", + }, + { + Timestamp: "2025-01-03T00:00:00Z", + ManagementStorage: "16 kB", + ActualData: "58 kB", + }, + { + Timestamp: "2025-01-04T00:00:00Z", + ManagementStorage: "18 kB", + ActualData: "64 kB", + }, + { + Timestamp: "2025-01-05T00:00:00Z", + ManagementStorage: "21 kB", + ActualData: "71 kB", + }, + { + Timestamp: "2025-01-06T00:00:00Z", + ManagementStorage: "24 kB", + ActualData: "77 kB", + }, + { + Timestamp: "2025-01-07T00:00:00Z", + ManagementStorage: "27 kB", + ActualData: "84 kB", + }, + { + Timestamp: "2025-01-08T00:00:00Z", + ManagementStorage: "30 kB", + ActualData: "89 kB", + }, + { + Timestamp: "2025-01-09T00:00:00Z", + ManagementStorage: "33 kB", + ActualData: "95 kB", + }, + { + Timestamp: "2025-01-10T00:00:00Z", + ManagementStorage: "36 kB", + ActualData: "99 kB", + }, + } + + // Database Activity dummy data - showing realistic patterns with peaks and valleys (under 100) + DatabaseActivityResponse = []DatabaseActivityWithDates{ + { + Timestamp: "2025-01-01T00:00:00Z", + TotalTimeMs: 12.45, + TotalQueries: 15, + }, + { + Timestamp: "2025-01-02T00:00:00Z", + TotalTimeMs: 17.89, + TotalQueries: 21, + }, + { + Timestamp: "2025-01-03T00:00:00Z", + TotalTimeMs: 21.34, + TotalQueries: 28, + }, + { + Timestamp: "2025-01-04T00:00:00Z", + TotalTimeMs: 18.76, + TotalQueries: 24, + }, + { + Timestamp: "2025-01-05T00:00:00Z", + TotalTimeMs: 25.67, + TotalQueries: 32, + }, + { + Timestamp: "2025-01-06T00:00:00Z", + TotalTimeMs: 22.34, + TotalQueries: 29, + }, + { + Timestamp: "2025-01-07T00:00:00Z", + TotalTimeMs: 30.45, + TotalQueries: 37, + }, + { + Timestamp: "2025-01-08T00:00:00Z", + TotalTimeMs: 27.89, + TotalQueries: 34, + }, + { + Timestamp: "2025-01-09T00:00:00Z", + TotalTimeMs: 24.56, + TotalQueries: 31, + }, + { + Timestamp: "2025-01-10T00:00:00Z", + TotalTimeMs: 29.98, + TotalQueries: 36, + }, + } + + // Database Usage Stats dummy data - showing cost variations (under 100) + DatabaseUsageStatsResponse = []DatabaseUsageCostWithDates{ + { + ReadWriteCost: 25.45, + CPUCost: 17.23, + TotalCost: 42.68, + Timestamp: "2025-01-01T00:00:00Z", + }, + { + ReadWriteCost: 32.78, + CPUCost: 19.34, + TotalCost: 52.12, + Timestamp: "2025-01-02T00:00:00Z", + }, + { + ReadWriteCost: 38.56, + CPUCost: 22.67, + TotalCost: 61.23, + Timestamp: "2025-01-03T00:00:00Z", + }, + { + ReadWriteCost: 36.89, + CPUCost: 25.45, + TotalCost: 62.34, + Timestamp: "2025-01-04T00:00:00Z", + }, + { + ReadWriteCost: 44.67, + CPUCost: 34.23, + TotalCost: 78.90, + Timestamp: "2025-01-05T00:00:00Z", + }, + { + ReadWriteCost: 41.45, + CPUCost: 28.78, + TotalCost: 70.23, + Timestamp: "2025-01-06T00:00:00Z", + }, + { + ReadWriteCost: 47.34, + CPUCost: 36.89, + TotalCost: 84.23, + Timestamp: "2025-01-07T00:00:00Z", + }, + { + ReadWriteCost: 46.78, + CPUCost: 33.56, + TotalCost: 80.34, + Timestamp: "2025-01-08T00:00:00Z", + }, + { + ReadWriteCost: 43.45, + CPUCost: 35.67, + TotalCost: 79.12, + Timestamp: "2025-01-09T00:00:00Z", + }, + { + ReadWriteCost: 48.67, + CPUCost: 37.89, + TotalCost: 86.56, + Timestamp: "2025-01-10T00:00:00Z", + }, + } +) diff --git a/analytics/service.go b/analytics/service.go index 41256e0..353ae3d 100644 --- a/analytics/service.go +++ b/analytics/service.go @@ -194,6 +194,16 @@ func GetALLDatabaseUsageStats(ctx context.Context, db *pgxpool.Pool, projectOid if err := rows.Scan(&record.Timestamp, &record.ReadWriteCost, &record.CPUCost, &record.TotalCost); err != nil { return nil, *api.NewApiError("Internal server error", 500, errors.New("failed to scan database usage record: "+err.Error())) } + record.CPUCost *= 10000000 + record.ReadWriteCost *= 10000000 + if record.CPUCost < 0 { + record.CPUCost *= -1 + } + + if record.ReadWriteCost < 0 { + record.ReadWriteCost *= -1 + } + record.TotalCost = record.CPUCost + record.ReadWriteCost usageRecords = append(usageRecords, record) } diff --git a/config/application.go b/config/application.go index fcf65be..9975919 100644 --- a/config/application.go +++ b/config/application.go @@ -9,6 +9,7 @@ import ( "path/filepath" "runtime" + "github.com/axiomhq/axiom-go/axiom" "github.com/jackc/pgx/v5" "github.com/joho/godotenv" @@ -59,6 +60,8 @@ var ( ConfigManager *UserDbConfig AI Agent.RAGmodel + + AxiomLogger *axiom.Client ) func loadEnv() { @@ -71,7 +74,9 @@ func loadEnv() { } } -const deploy = true +const ( + deploy = true // Set to true if running in production, false for development +) func Init(infoLog, errorLog *log.Logger) { @@ -136,18 +141,7 @@ func Init(infoLog, errorLog *log.Logger) { errorLog.Fatalf("Unable to parse database URL: %v", err) } - // this clear the cached prepared statement - //محدش يعدل فيها عشان انا اتبضنت من كتفم دي لغه - // there is an error occurs when you restart the server : - // ERROR: prepared statement "stmtcache_d40c25297f5a9db6d92b9594942d1217a18da17e46487cf5" already exists (SQLSTATE 42P05) - // it means that the prepared statement already exists and you cannot recache it - // so this function should remove all cached prepared statements when the server starts - config.AfterConnect = func(ctx context.Context, conn *pgx.Conn) error { - // Clear any existing statements - _, err := conn.Exec(ctx, "DISCARD ALL") - return err - } - + config.ConnConfig.DefaultQueryExecMode = pgx.QueryExecModeExec DB, err = pgxpool.NewWithConfig(context.Background(), config) if err != nil { errorLog.Fatalf("Unable to connect to database: %v", err) @@ -193,6 +187,14 @@ func Init(infoLog, errorLog *log.Logger) { PineconeIndexHost: os.Getenv("PINECONE_INDEX_HOST"), }) infoLog.Println("Connected to AI successfully! ✅") + + AxiomLogger, err = axiom.NewClient( + axiom.SetPersonalTokenConfig(os.Getenv("AXIOM_TOKEN"), os.Getenv("AXIOM_ORG_ID")), // Set your Axiom personal token and organization ID + ) + if err != nil { + errorLog.Fatalf("Failed to create Axiom client: %v", err) + } + infoLog.Println("Connected to Axiom successfully! ✅") } func CloseDB() { @@ -205,5 +207,4 @@ func CloseDB() { AdminDB.Close() App.InfoLog.Println("Admin database connection closed. 🔌") } - // config.CloseAllPools(); } diff --git a/config/helpers.go b/config/helpers.go deleted file mode 100644 index b60bc29..0000000 --- a/config/helpers.go +++ /dev/null @@ -1,22 +0,0 @@ -package config - -import ( - "fmt" - "net/http" - "runtime/debug" -) - -func (app *Application) serverError(w http.ResponseWriter, err error) { - trace := fmt.Sprintf("%s\n%s", err.Error(), debug.Stack()) - app.ErrorLog.Output(2, trace) - - http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) -} - -func (app *Application) clientError(w http.ResponseWriter, status int) { - http.Error(w, http.StatusText(status), status) -} - -func (app *Application) notFound(w http.ResponseWriter) { - app.clientError(w, http.StatusNotFound) -} diff --git a/config/utils.go b/config/utils.go index 684aef6..c8706cb 100644 --- a/config/utils.go +++ b/config/utils.go @@ -1,11 +1,11 @@ package config import ( - "DBHS/utils" "context" "fmt" "strings" + "github.com/georgysavva/scany/v2/pgxscan" "github.com/jackc/pgx/v5/pgxpool" ) @@ -103,7 +103,7 @@ func (m *UserDbConfig) GetDbConnection(ctx context.Context, dbName string) (*pgx return newPool, nil } -func LoadTypeMap(ctx context.Context, db utils.Querier) error { +func LoadTypeMap(ctx context.Context, db pgxscan.Querier) error { rows, err := db.Query(ctx, "SELECT oid, typname FROM pg_type") if err != nil { diff --git a/docs/docs.go b/docs/docs.go index adbf9ae..d601216 100644 --- a/docs/docs.go +++ b/docs/docs.go @@ -173,22 +173,22 @@ const docTemplate = `{ ] } }, - "400": { - "description": "Project ID is required", + "401": { + "description": "Unauthorized", "schema": { - "$ref": "#/definitions/response.ErrorResponse" + "$ref": "#/definitions/response.ErrorResponse401" } }, - "401": { - "description": "Unauthorized", + "404": { + "description": "Project not found", "schema": { - "$ref": "#/definitions/response.ErrorResponse" + "$ref": "#/definitions/response.ErrorResponse404" } }, "500": { "description": "Internal server error", "schema": { - "$ref": "#/definitions/response.ErrorResponse" + "$ref": "#/definitions/response.ErrorResponse500" } } } @@ -199,7 +199,7 @@ const docTemplate = `{ "BearerAuth": [] } ], - "description": "Create a new table in the specified project", + "description": "Create new table in the specified project", "consumes": [ "application/json" ], @@ -209,7 +209,7 @@ const docTemplate = `{ "tags": [ "tables" ], - "summary": "Create a new table", + "summary": "Create new table", "parameters": [ { "type": "string", @@ -224,7 +224,7 @@ const docTemplate = `{ "in": "body", "required": true, "schema": { - "$ref": "#/definitions/tables.ClientTable" + "$ref": "#/definitions/tables.Table" } } ], @@ -236,21 +236,27 @@ const docTemplate = `{ } }, "400": { - "description": "Bad Request", + "description": "Bad request", "schema": { - "$ref": "#/definitions/response.ErrorResponse" + "$ref": "#/definitions/response.ErrorResponse400" } }, "401": { "description": "Unauthorized", "schema": { - "$ref": "#/definitions/response.ErrorResponse" + "$ref": "#/definitions/response.ErrorResponse401" + } + }, + "404": { + "description": "Project not found", + "schema": { + "$ref": "#/definitions/response.ErrorResponse404" } }, "500": { - "description": "Internal Server Error", + "description": "Internal server error", "schema": { - "$ref": "#/definitions/response.ErrorResponse" + "$ref": "#/definitions/response.ErrorResponse500" } } } @@ -302,19 +308,13 @@ const docTemplate = `{ }, { "type": "string", - "description": "Column to order by", - "name": "order_by", - "in": "query" - }, - { - "type": "string", - "description": "Sort order (asc or desc)", + "description": "Sort order example: ?order=id:asc\u0026order=name:desc , this sort first by id then name", "name": "order", "in": "query" }, { "type": "string", - "description": "Filter condition (e.g. name=value)", + "description": "Filter condition example: ?filter=id:gt:2\u0026filter=name:like:ragnar, this gets records with ids greater than 2 and with name equal ragnar, valid operators [eq: =, neq: !=, lt: \u003c, lte: \u003c=, gt: \u003e, gte: \u003e=, like: LIKE]", "name": "filter", "in": "query" } @@ -341,19 +341,25 @@ const docTemplate = `{ "400": { "description": "Bad Request", "schema": { - "$ref": "#/definitions/response.ErrorResponse" + "$ref": "#/definitions/response.ErrorResponse400" } }, "401": { "description": "Unauthorized", "schema": { - "$ref": "#/definitions/response.ErrorResponse" + "$ref": "#/definitions/response.ErrorResponse401" + } + }, + "404": { + "description": "Not Found", + "schema": { + "$ref": "#/definitions/response.ErrorResponse404" } }, "500": { "description": "Internal Server Error", "schema": { - "$ref": "#/definitions/response.ErrorResponse" + "$ref": "#/definitions/response.ErrorResponse500" } } } @@ -391,12 +397,12 @@ const docTemplate = `{ "required": true }, { - "description": "Table update information", + "description": "new table schema updates", "name": "updates", "in": "body", "required": true, "schema": { - "$ref": "#/definitions/tables.TableUpdate" + "$ref": "#/definitions/tables.UpdateTableSchema" } } ], @@ -410,19 +416,104 @@ const docTemplate = `{ "400": { "description": "Bad Request", "schema": { - "$ref": "#/definitions/response.ErrorResponse" + "$ref": "#/definitions/response.ErrorResponse400" } }, "401": { "description": "Unauthorized", "schema": { - "$ref": "#/definitions/response.ErrorResponse" + "$ref": "#/definitions/response.ErrorResponse401" + } + }, + "404": { + "description": "Not Found", + "schema": { + "$ref": "#/definitions/response.ErrorResponse404" } }, "500": { "description": "Internal Server Error", "schema": { - "$ref": "#/definitions/response.ErrorResponse" + "$ref": "#/definitions/response.ErrorResponse500" + } + } + } + }, + "post": { + "security": [ + { + "BearerAuth": [] + } + ], + "description": "insert new row in the specified project table", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "tables" + ], + "summary": "insert new row", + "parameters": [ + { + "type": "string", + "description": "Project ID", + "name": "project_id", + "in": "path", + "required": true + }, + { + "type": "string", + "description": "Table ID", + "name": "table_id", + "in": "path", + "required": true + }, + { + "description": "Row information", + "name": "row", + "in": "body", + "required": true, + "schema": { + "type": "array", + "items": { + "type": "object", + "additionalProperties": true + } + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/response.SuccessResponse" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/response.ErrorResponse400" + } + }, + "401": { + "description": "Unauthorized", + "schema": { + "$ref": "#/definitions/response.ErrorResponse401" + } + }, + "404": { + "description": "Not Found", + "schema": { + "$ref": "#/definitions/response.ErrorResponse404" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/response.ErrorResponse500" } } } @@ -467,19 +558,102 @@ const docTemplate = `{ "400": { "description": "Bad Request", "schema": { - "$ref": "#/definitions/response.ErrorResponse" + "$ref": "#/definitions/response.ErrorResponse400" } }, "401": { "description": "Unauthorized", "schema": { - "$ref": "#/definitions/response.ErrorResponse" + "$ref": "#/definitions/response.ErrorResponse401" + } + }, + "404": { + "description": "Not Found", + "schema": { + "$ref": "#/definitions/response.ErrorResponse404" } }, "500": { "description": "Internal Server Error", "schema": { - "$ref": "#/definitions/response.ErrorResponse" + "$ref": "#/definitions/response.ErrorResponse500" + } + } + } + } + }, + "/api/projects/{project_id}/tables/{table_id}/schema": { + "get": { + "security": [ + { + "BearerAuth": [] + } + ], + "description": "Get the schema of the specified table in the project", + "produces": [ + "application/json" + ], + "tags": [ + "tables" + ], + "summary": "Get the schema of a table", + "parameters": [ + { + "type": "string", + "description": "Project ID", + "name": "project_id", + "in": "path", + "required": true + }, + { + "type": "string", + "description": "Table ID", + "name": "table_id", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "Table schema", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/response.SuccessResponse" + }, + { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/tables.Table" + } + } + } + ] + } + }, + "400": { + "description": "Bad request", + "schema": { + "$ref": "#/definitions/response.ErrorResponse400" + } + }, + "401": { + "description": "Unauthorized", + "schema": { + "$ref": "#/definitions/response.ErrorResponse401" + } + }, + "404": { + "description": "Project not found", + "schema": { + "$ref": "#/definitions/response.ErrorResponse404" + } + }, + "500": { + "description": "Internal server error", + "schema": { + "$ref": "#/definitions/response.ErrorResponse500" } } } @@ -695,23 +869,253 @@ const docTemplate = `{ } }, "400": { - "description": "Project ID is required", + "description": "Project ID is required", + "schema": { + "$ref": "#/definitions/response.ErrorResponse400" + } + }, + "401": { + "description": "Unauthorized", + "schema": { + "$ref": "#/definitions/response.ErrorResponse401" + } + }, + "404": { + "description": "Project not found", + "schema": { + "$ref": "#/definitions/response.ErrorResponse404" + } + }, + "500": { + "description": "Internal server error", + "schema": { + "$ref": "#/definitions/response.ErrorResponse500" + } + } + } + }, + "patch": { + "security": [ + { + "BearerAuth": [] + } + ], + "description": "Update a project's details by its ID", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "projects" + ], + "summary": "Update a project", + "parameters": [ + { + "type": "string", + "description": "Project ID", + "name": "project_id", + "in": "path", + "required": true + }, + { + "description": "Project update information", + "name": "project", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/projects.updateProjectDataModel" + } + } + ], + "responses": { + "200": { + "description": "Project updated successfully", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/response.SuccessResponse" + }, + { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/projects.Project" + } + } + } + ] + } + }, + "400": { + "description": "Invalid input or Project ID is required", + "schema": { + "$ref": "#/definitions/response.ErrorResponse400" + } + }, + "500": { + "description": "Internal server error", + "schema": { + "$ref": "#/definitions/response.ErrorResponse500" + } + } + } + } + }, + "/projects/{project_id}/ai/agent": { + "post": { + "security": [ + { + "JWTAuth": [] + } + ], + "description": "This endpoint allows users to query the AI agent with a prompt. The agent will respond with a schema change suggestion based on the prompt.", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "AI" + ], + "summary": "AI Agent Query", + "parameters": [ + { + "type": "string", + "description": "Project ID", + "name": "project_id", + "in": "path", + "required": true + }, + { + "description": "Request", + "name": "Request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/ai.Request" + } + } + ], + "responses": { + "200": { + "description": "Agent query successful", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/response.Response" + }, + { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/ai.AgentResponse" + } + } + } + ] + } + }, + "400": { + "description": "Bad request", + "schema": { + "$ref": "#/definitions/response.ErrorResponse400" + } + }, + "500": { + "description": "Internal server error", + "schema": { + "$ref": "#/definitions/response.ErrorResponse500" + } + } + } + } + }, + "/projects/{project_id}/ai/agent/accept": { + "post": { + "security": [ + { + "JWTAuth": [] + } + ], + "description": "This endpoint allows users to accept the AI agent's query and execute the schema changes", + "produces": [ + "application/json" + ], + "tags": [ + "AI" + ], + "summary": "Accept AI Agent Query", + "parameters": [ + { + "type": "string", + "description": "Project ID", + "name": "project_id", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "Query executed successfully", + "schema": { + "$ref": "#/definitions/response.Response" + } + }, + "400": { + "description": "Bad request", + "schema": { + "$ref": "#/definitions/response.ErrorResponse400" + } + }, + "500": { + "description": "Internal server error", + "schema": { + "$ref": "#/definitions/response.ErrorResponse500" + } + } + } + } + }, + "/projects/{project_id}/ai/agent/cancel": { + "post": { + "security": [ + { + "JWTAuth": [] + } + ], + "description": "This endpoint allows users to cancel an AI agent query", + "produces": [ + "application/json" + ], + "tags": [ + "AI" + ], + "summary": "Cancel AI Agent Query", + "parameters": [ + { + "type": "string", + "description": "Project ID", + "name": "project_id", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "Agent query cancelled successfully", + "schema": { + "$ref": "#/definitions/response.Response" + } + }, + "400": { + "description": "Bad request", "schema": { "$ref": "#/definitions/response.ErrorResponse400" } }, - "401": { - "description": "Unauthorized", - "schema": { - "$ref": "#/definitions/response.ErrorResponse401" - } - }, - "404": { - "description": "Project not found", - "schema": { - "$ref": "#/definitions/response.ErrorResponse404" - } - }, "500": { "description": "Internal server error", "schema": { @@ -719,14 +1123,16 @@ const docTemplate = `{ } } } - }, - "patch": { + } + }, + "/projects/{project_id}/ai/chatbot/ask": { + "post": { "security": [ { "BearerAuth": [] } ], - "description": "Update a project's details by its ID", + "description": "This endpoint allows users to ask questions to the chatbot, which will respond using AI. It also saves the chat history for future reference.", "consumes": [ "application/json" ], @@ -734,9 +1140,9 @@ const docTemplate = `{ "application/json" ], "tags": [ - "projects" + "AI" ], - "summary": "Update a project", + "summary": "Chat Bot Ask", "parameters": [ { "type": "string", @@ -746,40 +1152,34 @@ const docTemplate = `{ "required": true }, { - "description": "Project update information", - "name": "project", + "description": "Chat Bot Request", + "name": "ChatBotRequest", "in": "body", "required": true, "schema": { - "$ref": "#/definitions/projects.updateProjectDataModel" + "$ref": "#/definitions/ai.ChatBotRequest" } } ], "responses": { "200": { - "description": "Project updated successfully", + "description": "Answer generated successfully", "schema": { "allOf": [ { - "$ref": "#/definitions/response.SuccessResponse" + "$ref": "#/definitions/response.Response" }, { "type": "object", "properties": { "data": { - "$ref": "#/definitions/projects.Project" + "type": "object" } } } ] } }, - "400": { - "description": "Invalid input or Project ID is required", - "schema": { - "$ref": "#/definitions/response.ErrorResponse400" - } - }, "500": { "description": "Internal server error", "schema": { @@ -838,7 +1238,7 @@ const docTemplate = `{ "500": { "description": "Internal server error", "schema": { - "$ref": "#/definitions/response.Response" + "$ref": "#/definitions/response.ErrorResponse500" } } } @@ -1371,6 +1771,88 @@ const docTemplate = `{ } } }, + "/projects/{project_id}/sqlEditor/run-query": { + "post": { + "security": [ + { + "BearerAuth": [] + } + ], + "description": "Execute a dynamic SQL query against a specific project's PostgreSQL database and return structured JSON results with metadata including column names and execution time", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "sqlEditor" + ], + "summary": "Execute SQL query on project database", + "parameters": [ + { + "type": "string", + "description": "Project ID (OID)", + "name": "project_id", + "in": "path", + "required": true + }, + { + "description": "SQL query to execute", + "name": "query", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/sqleditor.RequestBody" + } + } + ], + "responses": { + "200": { + "description": "Query executed successfully with results, column names, and execution time", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/response.SuccessResponse" + }, + { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/sqleditor.ResponseBodySwagger" + } + } + } + ] + } + }, + "400": { + "description": "Project ID missing, invalid request body, empty query, or dangerous SQL operations detected", + "schema": { + "$ref": "#/definitions/response.ErrorResponse" + } + }, + "401": { + "description": "Unauthorized access", + "schema": { + "$ref": "#/definitions/response.ErrorResponse" + } + }, + "404": { + "description": "Project not found or referenced table/column does not exist", + "schema": { + "$ref": "#/definitions/response.ErrorResponse" + } + }, + "500": { + "description": "Internal server error, query execution failed, or error parsing results", + "schema": { + "$ref": "#/definitions/response.ErrorResponse" + } + } + } + } + }, "/user/forget-password/verify": { "post": { "description": "Verify code and reset user password", @@ -1664,7 +2146,7 @@ const docTemplate = `{ "500": { "description": "Server error", "schema": { - "$ref": "#/definitions/accounts.ErrorResponse400EmailNotFound" + "$ref": "#/definitions/accounts.ErrorResponse" } } } @@ -2087,6 +2569,39 @@ const docTemplate = `{ } } }, + "ai.AgentResponse": { + "type": "object", + "properties": { + "response": { + "type": "string" + }, + "schema_changes": { + "type": "array", + "items": { + "$ref": "#/definitions/utils.Table" + } + }, + "schema_ddl": { + "type": "string" + } + } + }, + "ai.ChatBotRequest": { + "type": "object", + "properties": { + "question": { + "type": "string" + } + } + }, + "ai.Request": { + "type": "object", + "properties": { + "prompt": { + "type": "string" + } + } + }, "analytics.DatabaseActivityWithDates": { "type": "object", "properties": { @@ -2308,51 +2823,38 @@ const docTemplate = `{ } } }, - "tables.ClientTable": { - "type": "object", - "properties": { - "columns": { - "type": "array", - "items": { - "$ref": "#/definitions/tables.Column" - } - }, - "tableName": { - "type": "string" - } - } - }, - "tables.Column": { + "sqleditor.RequestBody": { "type": "object", "properties": { - "foreignKey": { - "$ref": "#/definitions/tables.ForeignKey" - }, - "isNullable": { - "type": "boolean" - }, - "isPrimaryKey": { - "type": "boolean" - }, - "isUnique": { - "type": "boolean" - }, - "name": { - "type": "string" - }, - "type": { + "query": { "type": "string" } } }, - "tables.ColumnCollection": { + "sqleditor.ResponseBodySwagger": { + "description": "SQL query execution response with results and metadata", "type": "object", "properties": { - "columns": { + "column_names": { + "description": "Names of columns in the result set", "type": "array", "items": { - "$ref": "#/definitions/tables.Column" - } + "type": "string" + }, + "example": [ + "[\"id\"", + "\"name\"]" + ] + }, + "execution_time": { + "description": "Query execution time in milliseconds", + "type": "number", + "example": 10.45 + }, + "result": { + "description": "The JSON result of the query execution", + "type": "string", + "example": "[{\"id\":1,\"name\":\"test\"}]" } } }, @@ -2374,30 +2876,50 @@ const docTemplate = `{ } } }, - "tables.ForeignKey": { + "tables.ShowColumn": { "type": "object", "properties": { - "columnName": { + "name": { "type": "string" }, - "tableName": { + "type": { "type": "string" } } }, - "tables.ShowColumn": { + "tables.Table": { "type": "object", + "required": [ + "name", + "schema" + ], "properties": { + "description": { + "type": "string" + }, + "id": { + "type": "integer" + }, "name": { "type": "string" }, - "type": { + "oid": { "type": "string" + }, + "project_id": { + "type": "integer" + }, + "schema": { + "$ref": "#/definitions/utils.Table" } } }, - "tables.Table": { + "tables.UpdateTableSchema": { "type": "object", + "required": [ + "name", + "schema" + ], "properties": { "description": { "type": "string" @@ -2413,37 +2935,136 @@ const docTemplate = `{ }, "project_id": { "type": "integer" + }, + "renames": { + "type": "array", + "items": { + "$ref": "#/definitions/utils.RenameRelation" + } + }, + "schema": { + "$ref": "#/definitions/utils.Table" + } + } + }, + "utils.ConstraintInfo": { + "type": "object", + "properties": { + "CheckClause": { + "type": "string" + }, + "ColumnName": { + "type": "string" + }, + "ConstraintName": { + "type": "string" + }, + "ConstraintType": { + "type": "string" + }, + "ForeignColumnName": { + "type": "string" + }, + "ForeignTableName": { + "type": "string" + }, + "OrdinalPosition": { + "type": "integer" + }, + "TableName": { + "type": "string" + } + } + }, + "utils.IndexInfo": { + "type": "object", + "properties": { + "ColumnName": { + "type": "string" + }, + "IndexName": { + "type": "string" + }, + "IndexType": { + "type": "string" + }, + "IsPrimary": { + "type": "boolean" + }, + "IsUnique": { + "type": "boolean" + }, + "TableName": { + "type": "string" + } + } + }, + "utils.RenameRelation": { + "type": "object", + "properties": { + "newName": { + "type": "string" + }, + "oldName": { + "type": "string" } } }, - "tables.TableUpdate": { + "utils.Table": { "type": "object", "properties": { - "delete": { + "Columns": { "type": "array", "items": { - "type": "string" + "$ref": "#/definitions/utils.TableColumn" } }, - "insert": { - "$ref": "#/definitions/tables.ColumnCollection" + "Constraints": { + "type": "array", + "items": { + "$ref": "#/definitions/utils.ConstraintInfo" + } }, - "update": { + "Indexes": { "type": "array", "items": { - "$ref": "#/definitions/tables.UpdateColumn" + "$ref": "#/definitions/utils.IndexInfo" } + }, + "TableName": { + "type": "string" } } }, - "tables.UpdateColumn": { + "utils.TableColumn": { "type": "object", "properties": { - "name": { + "CharacterMaximumLength": { + "type": "integer" + }, + "ColumnDefault": { + "type": "string" + }, + "ColumnName": { + "type": "string" + }, + "DataType": { "type": "string" }, - "update": { - "$ref": "#/definitions/tables.Column" + "IsNullable": { + "type": "boolean" + }, + "NumericPrecision": { + "type": "integer" + }, + "NumericScale": { + "type": "integer" + }, + "OrdinalPosition": { + "type": "integer" + }, + "TableName": { + "type": "string" } } } diff --git a/docs/swagger.json b/docs/swagger.json index 4b94852..e468377 100644 --- a/docs/swagger.json +++ b/docs/swagger.json @@ -166,22 +166,22 @@ ] } }, - "400": { - "description": "Project ID is required", + "401": { + "description": "Unauthorized", "schema": { - "$ref": "#/definitions/response.ErrorResponse" + "$ref": "#/definitions/response.ErrorResponse401" } }, - "401": { - "description": "Unauthorized", + "404": { + "description": "Project not found", "schema": { - "$ref": "#/definitions/response.ErrorResponse" + "$ref": "#/definitions/response.ErrorResponse404" } }, "500": { "description": "Internal server error", "schema": { - "$ref": "#/definitions/response.ErrorResponse" + "$ref": "#/definitions/response.ErrorResponse500" } } } @@ -192,7 +192,7 @@ "BearerAuth": [] } ], - "description": "Create a new table in the specified project", + "description": "Create new table in the specified project", "consumes": [ "application/json" ], @@ -202,7 +202,7 @@ "tags": [ "tables" ], - "summary": "Create a new table", + "summary": "Create new table", "parameters": [ { "type": "string", @@ -217,7 +217,7 @@ "in": "body", "required": true, "schema": { - "$ref": "#/definitions/tables.ClientTable" + "$ref": "#/definitions/tables.Table" } } ], @@ -229,21 +229,27 @@ } }, "400": { - "description": "Bad Request", + "description": "Bad request", "schema": { - "$ref": "#/definitions/response.ErrorResponse" + "$ref": "#/definitions/response.ErrorResponse400" } }, "401": { "description": "Unauthorized", "schema": { - "$ref": "#/definitions/response.ErrorResponse" + "$ref": "#/definitions/response.ErrorResponse401" + } + }, + "404": { + "description": "Project not found", + "schema": { + "$ref": "#/definitions/response.ErrorResponse404" } }, "500": { - "description": "Internal Server Error", + "description": "Internal server error", "schema": { - "$ref": "#/definitions/response.ErrorResponse" + "$ref": "#/definitions/response.ErrorResponse500" } } } @@ -295,19 +301,13 @@ }, { "type": "string", - "description": "Column to order by", - "name": "order_by", - "in": "query" - }, - { - "type": "string", - "description": "Sort order (asc or desc)", + "description": "Sort order example: ?order=id:asc\u0026order=name:desc , this sort first by id then name", "name": "order", "in": "query" }, { "type": "string", - "description": "Filter condition (e.g. name=value)", + "description": "Filter condition example: ?filter=id:gt:2\u0026filter=name:like:ragnar, this gets records with ids greater than 2 and with name equal ragnar, valid operators [eq: =, neq: !=, lt: \u003c, lte: \u003c=, gt: \u003e, gte: \u003e=, like: LIKE]", "name": "filter", "in": "query" } @@ -334,19 +334,25 @@ "400": { "description": "Bad Request", "schema": { - "$ref": "#/definitions/response.ErrorResponse" + "$ref": "#/definitions/response.ErrorResponse400" } }, "401": { "description": "Unauthorized", "schema": { - "$ref": "#/definitions/response.ErrorResponse" + "$ref": "#/definitions/response.ErrorResponse401" + } + }, + "404": { + "description": "Not Found", + "schema": { + "$ref": "#/definitions/response.ErrorResponse404" } }, "500": { "description": "Internal Server Error", "schema": { - "$ref": "#/definitions/response.ErrorResponse" + "$ref": "#/definitions/response.ErrorResponse500" } } } @@ -384,12 +390,12 @@ "required": true }, { - "description": "Table update information", + "description": "new table schema updates", "name": "updates", "in": "body", "required": true, "schema": { - "$ref": "#/definitions/tables.TableUpdate" + "$ref": "#/definitions/tables.UpdateTableSchema" } } ], @@ -403,19 +409,104 @@ "400": { "description": "Bad Request", "schema": { - "$ref": "#/definitions/response.ErrorResponse" + "$ref": "#/definitions/response.ErrorResponse400" } }, "401": { "description": "Unauthorized", "schema": { - "$ref": "#/definitions/response.ErrorResponse" + "$ref": "#/definitions/response.ErrorResponse401" + } + }, + "404": { + "description": "Not Found", + "schema": { + "$ref": "#/definitions/response.ErrorResponse404" } }, "500": { "description": "Internal Server Error", "schema": { - "$ref": "#/definitions/response.ErrorResponse" + "$ref": "#/definitions/response.ErrorResponse500" + } + } + } + }, + "post": { + "security": [ + { + "BearerAuth": [] + } + ], + "description": "insert new row in the specified project table", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "tables" + ], + "summary": "insert new row", + "parameters": [ + { + "type": "string", + "description": "Project ID", + "name": "project_id", + "in": "path", + "required": true + }, + { + "type": "string", + "description": "Table ID", + "name": "table_id", + "in": "path", + "required": true + }, + { + "description": "Row information", + "name": "row", + "in": "body", + "required": true, + "schema": { + "type": "array", + "items": { + "type": "object", + "additionalProperties": true + } + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/response.SuccessResponse" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/response.ErrorResponse400" + } + }, + "401": { + "description": "Unauthorized", + "schema": { + "$ref": "#/definitions/response.ErrorResponse401" + } + }, + "404": { + "description": "Not Found", + "schema": { + "$ref": "#/definitions/response.ErrorResponse404" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/response.ErrorResponse500" } } } @@ -460,19 +551,102 @@ "400": { "description": "Bad Request", "schema": { - "$ref": "#/definitions/response.ErrorResponse" + "$ref": "#/definitions/response.ErrorResponse400" } }, "401": { "description": "Unauthorized", "schema": { - "$ref": "#/definitions/response.ErrorResponse" + "$ref": "#/definitions/response.ErrorResponse401" + } + }, + "404": { + "description": "Not Found", + "schema": { + "$ref": "#/definitions/response.ErrorResponse404" } }, "500": { "description": "Internal Server Error", "schema": { - "$ref": "#/definitions/response.ErrorResponse" + "$ref": "#/definitions/response.ErrorResponse500" + } + } + } + } + }, + "/api/projects/{project_id}/tables/{table_id}/schema": { + "get": { + "security": [ + { + "BearerAuth": [] + } + ], + "description": "Get the schema of the specified table in the project", + "produces": [ + "application/json" + ], + "tags": [ + "tables" + ], + "summary": "Get the schema of a table", + "parameters": [ + { + "type": "string", + "description": "Project ID", + "name": "project_id", + "in": "path", + "required": true + }, + { + "type": "string", + "description": "Table ID", + "name": "table_id", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "Table schema", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/response.SuccessResponse" + }, + { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/tables.Table" + } + } + } + ] + } + }, + "400": { + "description": "Bad request", + "schema": { + "$ref": "#/definitions/response.ErrorResponse400" + } + }, + "401": { + "description": "Unauthorized", + "schema": { + "$ref": "#/definitions/response.ErrorResponse401" + } + }, + "404": { + "description": "Project not found", + "schema": { + "$ref": "#/definitions/response.ErrorResponse404" + } + }, + "500": { + "description": "Internal server error", + "schema": { + "$ref": "#/definitions/response.ErrorResponse500" } } } @@ -688,23 +862,253 @@ } }, "400": { - "description": "Project ID is required", + "description": "Project ID is required", + "schema": { + "$ref": "#/definitions/response.ErrorResponse400" + } + }, + "401": { + "description": "Unauthorized", + "schema": { + "$ref": "#/definitions/response.ErrorResponse401" + } + }, + "404": { + "description": "Project not found", + "schema": { + "$ref": "#/definitions/response.ErrorResponse404" + } + }, + "500": { + "description": "Internal server error", + "schema": { + "$ref": "#/definitions/response.ErrorResponse500" + } + } + } + }, + "patch": { + "security": [ + { + "BearerAuth": [] + } + ], + "description": "Update a project's details by its ID", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "projects" + ], + "summary": "Update a project", + "parameters": [ + { + "type": "string", + "description": "Project ID", + "name": "project_id", + "in": "path", + "required": true + }, + { + "description": "Project update information", + "name": "project", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/projects.updateProjectDataModel" + } + } + ], + "responses": { + "200": { + "description": "Project updated successfully", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/response.SuccessResponse" + }, + { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/projects.Project" + } + } + } + ] + } + }, + "400": { + "description": "Invalid input or Project ID is required", + "schema": { + "$ref": "#/definitions/response.ErrorResponse400" + } + }, + "500": { + "description": "Internal server error", + "schema": { + "$ref": "#/definitions/response.ErrorResponse500" + } + } + } + } + }, + "/projects/{project_id}/ai/agent": { + "post": { + "security": [ + { + "JWTAuth": [] + } + ], + "description": "This endpoint allows users to query the AI agent with a prompt. The agent will respond with a schema change suggestion based on the prompt.", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "AI" + ], + "summary": "AI Agent Query", + "parameters": [ + { + "type": "string", + "description": "Project ID", + "name": "project_id", + "in": "path", + "required": true + }, + { + "description": "Request", + "name": "Request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/ai.Request" + } + } + ], + "responses": { + "200": { + "description": "Agent query successful", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/response.Response" + }, + { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/ai.AgentResponse" + } + } + } + ] + } + }, + "400": { + "description": "Bad request", + "schema": { + "$ref": "#/definitions/response.ErrorResponse400" + } + }, + "500": { + "description": "Internal server error", + "schema": { + "$ref": "#/definitions/response.ErrorResponse500" + } + } + } + } + }, + "/projects/{project_id}/ai/agent/accept": { + "post": { + "security": [ + { + "JWTAuth": [] + } + ], + "description": "This endpoint allows users to accept the AI agent's query and execute the schema changes", + "produces": [ + "application/json" + ], + "tags": [ + "AI" + ], + "summary": "Accept AI Agent Query", + "parameters": [ + { + "type": "string", + "description": "Project ID", + "name": "project_id", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "Query executed successfully", + "schema": { + "$ref": "#/definitions/response.Response" + } + }, + "400": { + "description": "Bad request", + "schema": { + "$ref": "#/definitions/response.ErrorResponse400" + } + }, + "500": { + "description": "Internal server error", + "schema": { + "$ref": "#/definitions/response.ErrorResponse500" + } + } + } + } + }, + "/projects/{project_id}/ai/agent/cancel": { + "post": { + "security": [ + { + "JWTAuth": [] + } + ], + "description": "This endpoint allows users to cancel an AI agent query", + "produces": [ + "application/json" + ], + "tags": [ + "AI" + ], + "summary": "Cancel AI Agent Query", + "parameters": [ + { + "type": "string", + "description": "Project ID", + "name": "project_id", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "Agent query cancelled successfully", + "schema": { + "$ref": "#/definitions/response.Response" + } + }, + "400": { + "description": "Bad request", "schema": { "$ref": "#/definitions/response.ErrorResponse400" } }, - "401": { - "description": "Unauthorized", - "schema": { - "$ref": "#/definitions/response.ErrorResponse401" - } - }, - "404": { - "description": "Project not found", - "schema": { - "$ref": "#/definitions/response.ErrorResponse404" - } - }, "500": { "description": "Internal server error", "schema": { @@ -712,14 +1116,16 @@ } } } - }, - "patch": { + } + }, + "/projects/{project_id}/ai/chatbot/ask": { + "post": { "security": [ { "BearerAuth": [] } ], - "description": "Update a project's details by its ID", + "description": "This endpoint allows users to ask questions to the chatbot, which will respond using AI. It also saves the chat history for future reference.", "consumes": [ "application/json" ], @@ -727,9 +1133,9 @@ "application/json" ], "tags": [ - "projects" + "AI" ], - "summary": "Update a project", + "summary": "Chat Bot Ask", "parameters": [ { "type": "string", @@ -739,40 +1145,34 @@ "required": true }, { - "description": "Project update information", - "name": "project", + "description": "Chat Bot Request", + "name": "ChatBotRequest", "in": "body", "required": true, "schema": { - "$ref": "#/definitions/projects.updateProjectDataModel" + "$ref": "#/definitions/ai.ChatBotRequest" } } ], "responses": { "200": { - "description": "Project updated successfully", + "description": "Answer generated successfully", "schema": { "allOf": [ { - "$ref": "#/definitions/response.SuccessResponse" + "$ref": "#/definitions/response.Response" }, { "type": "object", "properties": { "data": { - "$ref": "#/definitions/projects.Project" + "type": "object" } } } ] } }, - "400": { - "description": "Invalid input or Project ID is required", - "schema": { - "$ref": "#/definitions/response.ErrorResponse400" - } - }, "500": { "description": "Internal server error", "schema": { @@ -831,7 +1231,7 @@ "500": { "description": "Internal server error", "schema": { - "$ref": "#/definitions/response.Response" + "$ref": "#/definitions/response.ErrorResponse500" } } } @@ -1364,6 +1764,88 @@ } } }, + "/projects/{project_id}/sqlEditor/run-query": { + "post": { + "security": [ + { + "BearerAuth": [] + } + ], + "description": "Execute a dynamic SQL query against a specific project's PostgreSQL database and return structured JSON results with metadata including column names and execution time", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "sqlEditor" + ], + "summary": "Execute SQL query on project database", + "parameters": [ + { + "type": "string", + "description": "Project ID (OID)", + "name": "project_id", + "in": "path", + "required": true + }, + { + "description": "SQL query to execute", + "name": "query", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/sqleditor.RequestBody" + } + } + ], + "responses": { + "200": { + "description": "Query executed successfully with results, column names, and execution time", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/response.SuccessResponse" + }, + { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/sqleditor.ResponseBodySwagger" + } + } + } + ] + } + }, + "400": { + "description": "Project ID missing, invalid request body, empty query, or dangerous SQL operations detected", + "schema": { + "$ref": "#/definitions/response.ErrorResponse" + } + }, + "401": { + "description": "Unauthorized access", + "schema": { + "$ref": "#/definitions/response.ErrorResponse" + } + }, + "404": { + "description": "Project not found or referenced table/column does not exist", + "schema": { + "$ref": "#/definitions/response.ErrorResponse" + } + }, + "500": { + "description": "Internal server error, query execution failed, or error parsing results", + "schema": { + "$ref": "#/definitions/response.ErrorResponse" + } + } + } + } + }, "/user/forget-password/verify": { "post": { "description": "Verify code and reset user password", @@ -1657,7 +2139,7 @@ "500": { "description": "Server error", "schema": { - "$ref": "#/definitions/accounts.ErrorResponse400EmailNotFound" + "$ref": "#/definitions/accounts.ErrorResponse" } } } @@ -2080,6 +2562,39 @@ } } }, + "ai.AgentResponse": { + "type": "object", + "properties": { + "response": { + "type": "string" + }, + "schema_changes": { + "type": "array", + "items": { + "$ref": "#/definitions/utils.Table" + } + }, + "schema_ddl": { + "type": "string" + } + } + }, + "ai.ChatBotRequest": { + "type": "object", + "properties": { + "question": { + "type": "string" + } + } + }, + "ai.Request": { + "type": "object", + "properties": { + "prompt": { + "type": "string" + } + } + }, "analytics.DatabaseActivityWithDates": { "type": "object", "properties": { @@ -2301,51 +2816,38 @@ } } }, - "tables.ClientTable": { - "type": "object", - "properties": { - "columns": { - "type": "array", - "items": { - "$ref": "#/definitions/tables.Column" - } - }, - "tableName": { - "type": "string" - } - } - }, - "tables.Column": { + "sqleditor.RequestBody": { "type": "object", "properties": { - "foreignKey": { - "$ref": "#/definitions/tables.ForeignKey" - }, - "isNullable": { - "type": "boolean" - }, - "isPrimaryKey": { - "type": "boolean" - }, - "isUnique": { - "type": "boolean" - }, - "name": { - "type": "string" - }, - "type": { + "query": { "type": "string" } } }, - "tables.ColumnCollection": { + "sqleditor.ResponseBodySwagger": { + "description": "SQL query execution response with results and metadata", "type": "object", "properties": { - "columns": { + "column_names": { + "description": "Names of columns in the result set", "type": "array", "items": { - "$ref": "#/definitions/tables.Column" - } + "type": "string" + }, + "example": [ + "[\"id\"", + "\"name\"]" + ] + }, + "execution_time": { + "description": "Query execution time in milliseconds", + "type": "number", + "example": 10.45 + }, + "result": { + "description": "The JSON result of the query execution", + "type": "string", + "example": "[{\"id\":1,\"name\":\"test\"}]" } } }, @@ -2367,30 +2869,50 @@ } } }, - "tables.ForeignKey": { + "tables.ShowColumn": { "type": "object", "properties": { - "columnName": { + "name": { "type": "string" }, - "tableName": { + "type": { "type": "string" } } }, - "tables.ShowColumn": { + "tables.Table": { "type": "object", + "required": [ + "name", + "schema" + ], "properties": { + "description": { + "type": "string" + }, + "id": { + "type": "integer" + }, "name": { "type": "string" }, - "type": { + "oid": { "type": "string" + }, + "project_id": { + "type": "integer" + }, + "schema": { + "$ref": "#/definitions/utils.Table" } } }, - "tables.Table": { + "tables.UpdateTableSchema": { "type": "object", + "required": [ + "name", + "schema" + ], "properties": { "description": { "type": "string" @@ -2406,37 +2928,136 @@ }, "project_id": { "type": "integer" + }, + "renames": { + "type": "array", + "items": { + "$ref": "#/definitions/utils.RenameRelation" + } + }, + "schema": { + "$ref": "#/definitions/utils.Table" + } + } + }, + "utils.ConstraintInfo": { + "type": "object", + "properties": { + "CheckClause": { + "type": "string" + }, + "ColumnName": { + "type": "string" + }, + "ConstraintName": { + "type": "string" + }, + "ConstraintType": { + "type": "string" + }, + "ForeignColumnName": { + "type": "string" + }, + "ForeignTableName": { + "type": "string" + }, + "OrdinalPosition": { + "type": "integer" + }, + "TableName": { + "type": "string" + } + } + }, + "utils.IndexInfo": { + "type": "object", + "properties": { + "ColumnName": { + "type": "string" + }, + "IndexName": { + "type": "string" + }, + "IndexType": { + "type": "string" + }, + "IsPrimary": { + "type": "boolean" + }, + "IsUnique": { + "type": "boolean" + }, + "TableName": { + "type": "string" + } + } + }, + "utils.RenameRelation": { + "type": "object", + "properties": { + "newName": { + "type": "string" + }, + "oldName": { + "type": "string" } } }, - "tables.TableUpdate": { + "utils.Table": { "type": "object", "properties": { - "delete": { + "Columns": { "type": "array", "items": { - "type": "string" + "$ref": "#/definitions/utils.TableColumn" } }, - "insert": { - "$ref": "#/definitions/tables.ColumnCollection" + "Constraints": { + "type": "array", + "items": { + "$ref": "#/definitions/utils.ConstraintInfo" + } }, - "update": { + "Indexes": { "type": "array", "items": { - "$ref": "#/definitions/tables.UpdateColumn" + "$ref": "#/definitions/utils.IndexInfo" } + }, + "TableName": { + "type": "string" } } }, - "tables.UpdateColumn": { + "utils.TableColumn": { "type": "object", "properties": { - "name": { + "CharacterMaximumLength": { + "type": "integer" + }, + "ColumnDefault": { + "type": "string" + }, + "ColumnName": { + "type": "string" + }, + "DataType": { "type": "string" }, - "update": { - "$ref": "#/definitions/tables.Column" + "IsNullable": { + "type": "boolean" + }, + "NumericPrecision": { + "type": "integer" + }, + "NumericScale": { + "type": "integer" + }, + "OrdinalPosition": { + "type": "integer" + }, + "TableName": { + "type": "string" } } } diff --git a/docs/swagger.yaml b/docs/swagger.yaml index f68a49c..f762472 100644 --- a/docs/swagger.yaml +++ b/docs/swagger.yaml @@ -203,6 +203,27 @@ definitions: example: User verified successfully type: string type: object + ai.AgentResponse: + properties: + response: + type: string + schema_changes: + items: + $ref: '#/definitions/utils.Table' + type: array + schema_ddl: + type: string + type: object + ai.ChatBotRequest: + properties: + question: + type: string + type: object + ai.Request: + properties: + prompt: + type: string + type: object analytics.DatabaseActivityWithDates: properties: timestamp: @@ -348,36 +369,30 @@ definitions: example: Operation successful type: string type: object - tables.ClientTable: + sqleditor.RequestBody: properties: - columns: - items: - $ref: '#/definitions/tables.Column' - type: array - tableName: - type: string - type: object - tables.Column: - properties: - foreignKey: - $ref: '#/definitions/tables.ForeignKey' - isNullable: - type: boolean - isPrimaryKey: - type: boolean - isUnique: - type: boolean - name: - type: string - type: + query: type: string type: object - tables.ColumnCollection: + sqleditor.ResponseBodySwagger: + description: SQL query execution response with results and metadata properties: - columns: + column_names: + description: Names of columns in the result set + example: + - '["id"' + - '"name"]' items: - $ref: '#/definitions/tables.Column' + type: string type: array + execution_time: + description: Query execution time in milliseconds + example: 10.45 + type: number + result: + description: The JSON result of the query execution + example: '[{"id":1,"name":"test"}]' + type: string type: object tables.Data: properties: @@ -391,21 +406,32 @@ definitions: type: object type: array type: object - tables.ForeignKey: + tables.ShowColumn: properties: - columnName: + name: type: string - tableName: + type: type: string type: object - tables.ShowColumn: + tables.Table: properties: + description: + type: string + id: + type: integer name: type: string - type: + oid: type: string + project_id: + type: integer + schema: + $ref: '#/definitions/utils.Table' + required: + - name + - schema type: object - tables.Table: + tables.UpdateTableSchema: properties: description: type: string @@ -417,26 +443,94 @@ definitions: type: string project_id: type: integer + renames: + items: + $ref: '#/definitions/utils.RenameRelation' + type: array + schema: + $ref: '#/definitions/utils.Table' + required: + - name + - schema + type: object + utils.ConstraintInfo: + properties: + CheckClause: + type: string + ColumnName: + type: string + ConstraintName: + type: string + ConstraintType: + type: string + ForeignColumnName: + type: string + ForeignTableName: + type: string + OrdinalPosition: + type: integer + TableName: + type: string type: object - tables.TableUpdate: + utils.IndexInfo: properties: - delete: + ColumnName: + type: string + IndexName: + type: string + IndexType: + type: string + IsPrimary: + type: boolean + IsUnique: + type: boolean + TableName: + type: string + type: object + utils.RenameRelation: + properties: + newName: + type: string + oldName: + type: string + type: object + utils.Table: + properties: + Columns: items: - type: string + $ref: '#/definitions/utils.TableColumn' type: array - insert: - $ref: '#/definitions/tables.ColumnCollection' - update: + Constraints: items: - $ref: '#/definitions/tables.UpdateColumn' + $ref: '#/definitions/utils.ConstraintInfo' type: array + Indexes: + items: + $ref: '#/definitions/utils.IndexInfo' + type: array + TableName: + type: string type: object - tables.UpdateColumn: + utils.TableColumn: properties: - name: + CharacterMaximumLength: + type: integer + ColumnDefault: + type: string + ColumnName: + type: string + DataType: + type: string + IsNullable: + type: boolean + NumericPrecision: + type: integer + NumericScale: + type: integer + OrdinalPosition: + type: integer + TableName: type: string - update: - $ref: '#/definitions/tables.Column' type: object info: contact: {} @@ -539,18 +633,18 @@ paths: $ref: '#/definitions/tables.Table' type: array type: object - "400": - description: Project ID is required - schema: - $ref: '#/definitions/response.ErrorResponse' "401": description: Unauthorized schema: - $ref: '#/definitions/response.ErrorResponse' + $ref: '#/definitions/response.ErrorResponse401' + "404": + description: Project not found + schema: + $ref: '#/definitions/response.ErrorResponse404' "500": description: Internal server error schema: - $ref: '#/definitions/response.ErrorResponse' + $ref: '#/definitions/response.ErrorResponse500' security: - BearerAuth: [] summary: Get all tables in a project @@ -559,7 +653,7 @@ paths: post: consumes: - application/json - description: Create a new table in the specified project + description: Create new table in the specified project parameters: - description: Project ID in: path @@ -571,7 +665,7 @@ paths: name: table required: true schema: - $ref: '#/definitions/tables.ClientTable' + $ref: '#/definitions/tables.Table' produces: - application/json responses: @@ -580,20 +674,24 @@ paths: schema: $ref: '#/definitions/response.SuccessResponse' "400": - description: Bad Request + description: Bad request schema: - $ref: '#/definitions/response.ErrorResponse' + $ref: '#/definitions/response.ErrorResponse400' "401": description: Unauthorized schema: - $ref: '#/definitions/response.ErrorResponse' + $ref: '#/definitions/response.ErrorResponse401' + "404": + description: Project not found + schema: + $ref: '#/definitions/response.ErrorResponse404' "500": - description: Internal Server Error + description: Internal server error schema: - $ref: '#/definitions/response.ErrorResponse' + $ref: '#/definitions/response.ErrorResponse500' security: - BearerAuth: [] - summary: Create a new table + summary: Create new table tags: - tables /api/projects/{project_id}/tables/{table_id}: @@ -620,15 +718,19 @@ paths: "400": description: Bad Request schema: - $ref: '#/definitions/response.ErrorResponse' + $ref: '#/definitions/response.ErrorResponse400' "401": description: Unauthorized schema: - $ref: '#/definitions/response.ErrorResponse' + $ref: '#/definitions/response.ErrorResponse401' + "404": + description: Not Found + schema: + $ref: '#/definitions/response.ErrorResponse404' "500": description: Internal Server Error schema: - $ref: '#/definitions/response.ErrorResponse' + $ref: '#/definitions/response.ErrorResponse500' security: - BearerAuth: [] summary: Delete a table @@ -657,15 +759,14 @@ paths: name: limit required: true type: integer - - description: Column to order by - in: query - name: order_by - type: string - - description: Sort order (asc or desc) + - description: 'Sort order example: ?order=id:asc&order=name:desc , this sort + first by id then name' in: query name: order type: string - - description: Filter condition (e.g. name=value) + - description: 'Filter condition example: ?filter=id:gt:2&filter=name:like:ragnar, + this gets records with ids greater than 2 and with name equal ragnar, valid + operators [eq: =, neq: !=, lt: <, lte: <=, gt: >, gte: >=, like: LIKE]' in: query name: filter type: string @@ -684,20 +785,76 @@ paths: "400": description: Bad Request schema: - $ref: '#/definitions/response.ErrorResponse' + $ref: '#/definitions/response.ErrorResponse400' "401": description: Unauthorized schema: - $ref: '#/definitions/response.ErrorResponse' + $ref: '#/definitions/response.ErrorResponse401' + "404": + description: Not Found + schema: + $ref: '#/definitions/response.ErrorResponse404' "500": description: Internal Server Error schema: - $ref: '#/definitions/response.ErrorResponse' + $ref: '#/definitions/response.ErrorResponse500' security: - BearerAuth: [] summary: Read table data tags: - tables + post: + consumes: + - application/json + description: insert new row in the specified project table + parameters: + - description: Project ID + in: path + name: project_id + required: true + type: string + - description: Table ID + in: path + name: table_id + required: true + type: string + - description: Row information + in: body + name: row + required: true + schema: + items: + additionalProperties: true + type: object + type: array + produces: + - application/json + responses: + "200": + description: OK + schema: + $ref: '#/definitions/response.SuccessResponse' + "400": + description: Bad Request + schema: + $ref: '#/definitions/response.ErrorResponse400' + "401": + description: Unauthorized + schema: + $ref: '#/definitions/response.ErrorResponse401' + "404": + description: Not Found + schema: + $ref: '#/definitions/response.ErrorResponse404' + "500": + description: Internal Server Error + schema: + $ref: '#/definitions/response.ErrorResponse500' + security: + - BearerAuth: [] + summary: insert new row + tags: + - tables put: consumes: - application/json @@ -713,12 +870,12 @@ paths: name: table_id required: true type: string - - description: Table update information + - description: new table schema updates in: body name: updates required: true schema: - $ref: '#/definitions/tables.TableUpdate' + $ref: '#/definitions/tables.UpdateTableSchema' produces: - application/json responses: @@ -729,20 +886,71 @@ paths: "400": description: Bad Request schema: - $ref: '#/definitions/response.ErrorResponse' + $ref: '#/definitions/response.ErrorResponse400' "401": description: Unauthorized schema: - $ref: '#/definitions/response.ErrorResponse' + $ref: '#/definitions/response.ErrorResponse401' + "404": + description: Not Found + schema: + $ref: '#/definitions/response.ErrorResponse404' "500": description: Internal Server Error schema: - $ref: '#/definitions/response.ErrorResponse' + $ref: '#/definitions/response.ErrorResponse500' security: - BearerAuth: [] summary: Update an existing table tags: - tables + /api/projects/{project_id}/tables/{table_id}/schema: + get: + description: Get the schema of the specified table in the project + parameters: + - description: Project ID + in: path + name: project_id + required: true + type: string + - description: Table ID + in: path + name: table_id + required: true + type: string + produces: + - application/json + responses: + "200": + description: Table schema + schema: + allOf: + - $ref: '#/definitions/response.SuccessResponse' + - properties: + data: + $ref: '#/definitions/tables.Table' + type: object + "400": + description: Bad request + schema: + $ref: '#/definitions/response.ErrorResponse400' + "401": + description: Unauthorized + schema: + $ref: '#/definitions/response.ErrorResponse401' + "404": + description: Project not found + schema: + $ref: '#/definitions/response.ErrorResponse404' + "500": + description: Internal server error + schema: + $ref: '#/definitions/response.ErrorResponse500' + security: + - BearerAuth: [] + summary: Get the schema of a table + tags: + - tables /projects: get: description: Get all projects owned by the authenticated user @@ -926,6 +1134,147 @@ paths: summary: Update a project tags: - projects + /projects/{project_id}/ai/agent: + post: + consumes: + - application/json + description: This endpoint allows users to query the AI agent with a prompt. + The agent will respond with a schema change suggestion based on the prompt. + parameters: + - description: Project ID + in: path + name: project_id + required: true + type: string + - description: Request + in: body + name: Request + required: true + schema: + $ref: '#/definitions/ai.Request' + produces: + - application/json + responses: + "200": + description: Agent query successful + schema: + allOf: + - $ref: '#/definitions/response.Response' + - properties: + data: + $ref: '#/definitions/ai.AgentResponse' + type: object + "400": + description: Bad request + schema: + $ref: '#/definitions/response.ErrorResponse400' + "500": + description: Internal server error + schema: + $ref: '#/definitions/response.ErrorResponse500' + security: + - JWTAuth: [] + summary: AI Agent Query + tags: + - AI + /projects/{project_id}/ai/agent/accept: + post: + description: This endpoint allows users to accept the AI agent's query and execute + the schema changes + parameters: + - description: Project ID + in: path + name: project_id + required: true + type: string + produces: + - application/json + responses: + "200": + description: Query executed successfully + schema: + $ref: '#/definitions/response.Response' + "400": + description: Bad request + schema: + $ref: '#/definitions/response.ErrorResponse400' + "500": + description: Internal server error + schema: + $ref: '#/definitions/response.ErrorResponse500' + security: + - JWTAuth: [] + summary: Accept AI Agent Query + tags: + - AI + /projects/{project_id}/ai/agent/cancel: + post: + description: This endpoint allows users to cancel an AI agent query + parameters: + - description: Project ID + in: path + name: project_id + required: true + type: string + produces: + - application/json + responses: + "200": + description: Agent query cancelled successfully + schema: + $ref: '#/definitions/response.Response' + "400": + description: Bad request + schema: + $ref: '#/definitions/response.ErrorResponse400' + "500": + description: Internal server error + schema: + $ref: '#/definitions/response.ErrorResponse500' + security: + - JWTAuth: [] + summary: Cancel AI Agent Query + tags: + - AI + /projects/{project_id}/ai/chatbot/ask: + post: + consumes: + - application/json + description: This endpoint allows users to ask questions to the chatbot, which + will respond using AI. It also saves the chat history for future reference. + parameters: + - description: Project ID + in: path + name: project_id + required: true + type: string + - description: Chat Bot Request + in: body + name: ChatBotRequest + required: true + schema: + $ref: '#/definitions/ai.ChatBotRequest' + produces: + - application/json + responses: + "200": + description: Answer generated successfully + schema: + allOf: + - $ref: '#/definitions/response.Response' + - properties: + data: + type: object + type: object + "500": + description: Internal server error + schema: + $ref: '#/definitions/response.ErrorResponse500' + security: + - BearerAuth: [] + summary: Chat Bot Ask + tags: + - AI /projects/{project_id}/ai/report: get: consumes: @@ -952,7 +1301,7 @@ paths: "500": description: Internal server error schema: - $ref: '#/definitions/response.Response' + $ref: '#/definitions/response.ErrorResponse500' security: - BearerAuth: [] summary: Generate AI Report @@ -1292,6 +1641,61 @@ paths: summary: Update the name of a specific index tags: - indexes + /projects/{project_id}/sqlEditor/run-query: + post: + consumes: + - application/json + description: Execute a dynamic SQL query against a specific project's PostgreSQL + database and return structured JSON results with metadata including column + names and execution time + parameters: + - description: Project ID (OID) + in: path + name: project_id + required: true + type: string + - description: SQL query to execute + in: body + name: query + required: true + schema: + $ref: '#/definitions/sqleditor.RequestBody' + produces: + - application/json + responses: + "200": + description: Query executed successfully with results, column names, and + execution time + schema: + allOf: + - $ref: '#/definitions/response.SuccessResponse' + - properties: + data: + $ref: '#/definitions/sqleditor.ResponseBodySwagger' + type: object + "400": + description: Project ID missing, invalid request body, empty query, or dangerous + SQL operations detected + schema: + $ref: '#/definitions/response.ErrorResponse' + "401": + description: Unauthorized access + schema: + $ref: '#/definitions/response.ErrorResponse' + "404": + description: Project not found or referenced table/column does not exist + schema: + $ref: '#/definitions/response.ErrorResponse' + "500": + description: Internal server error, query execution failed, or error parsing + results + schema: + $ref: '#/definitions/response.ErrorResponse' + security: + - BearerAuth: [] + summary: Execute SQL query on project database + tags: + - sqlEditor /user/forget-password/verify: post: consumes: diff --git a/go.mod b/go.mod index a74cb3a..f024bd8 100644 --- a/go.mod +++ b/go.mod @@ -1,81 +1,95 @@ module DBHS -go 1.24.0 +go 1.24.4 + +toolchain go1.24.5 // go 1.23 // update go version to 1.24.0 require ( - github.com/Database-Hosting-Services/AI-Agent v1.0.0 + github.com/Database-Hosting-Services/AI-Agent v1.0.8 github.com/MarceloPetrucio/go-scalar-api-reference v0.0.0-20240521013641-ce5d2efe0e06 + github.com/axiomhq/axiom-go v0.25.0 github.com/georgysavva/scany/v2 v2.1.4 - github.com/golang-jwt/jwt/v5 v5.2.1 + github.com/golang-jwt/jwt/v5 v5.2.2 github.com/google/uuid v1.6.0 github.com/gorilla/mux v1.8.1 - github.com/jackc/pgx/v5 v5.7.2 + github.com/jackc/pgx/v5 v5.7.5 github.com/joho/godotenv v1.5.1 - github.com/redis/go-redis/v9 v9.7.1 + github.com/pingcap/tidb/parser v0.0.0-20231013125129-93a834a6bf8d + github.com/redis/go-redis/v9 v9.11.0 github.com/robfig/cron/v3 v3.0.1 github.com/stretchr/testify v1.10.0 github.com/swaggo/swag v1.16.4 golang.org/x/crypto v0.39.0 - golang.org/x/time v0.11.0 + golang.org/x/time v0.12.0 gopkg.in/gomail.v2 v2.0.0-20160411212932-81ebce5c23df ) require ( - cloud.google.com/go v0.115.0 // indirect + cloud.google.com/go v0.119.0 // indirect cloud.google.com/go/ai v0.8.0 // indirect - cloud.google.com/go/auth v0.6.0 // indirect - cloud.google.com/go/auth/oauth2adapt v0.2.2 // indirect + cloud.google.com/go/auth v0.15.0 // indirect + cloud.google.com/go/auth/oauth2adapt v0.2.7 // indirect cloud.google.com/go/compute/metadata v0.6.0 // indirect cloud.google.com/go/longrunning v0.5.7 // indirect github.com/KyleBanks/depth v1.2.1 // indirect github.com/apapsch/go-jsonmerge/v2 v2.0.0 // indirect + github.com/cenkalti/backoff/v4 v4.3.0 // indirect github.com/cespare/xxhash/v2 v2.3.0 // indirect - github.com/davecgh/go-spew v1.1.1 // indirect + github.com/cznic/mathutil v0.0.0-20181122101859-297441e03548 // indirect + github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect github.com/felixge/httpsnoop v1.0.4 // indirect - github.com/go-logr/logr v1.4.2 // indirect + github.com/go-logr/logr v1.4.3 // indirect github.com/go-logr/stdr v1.2.2 // indirect github.com/go-openapi/jsonpointer v0.21.1 // indirect github.com/go-openapi/jsonreference v0.21.0 // indirect github.com/go-openapi/spec v0.21.0 // indirect github.com/go-openapi/swag v0.23.1 // indirect - github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect - github.com/golang/protobuf v1.5.4 // indirect github.com/google/generative-ai-go v0.20.1 // indirect - github.com/google/s2a-go v0.1.7 // indirect - github.com/googleapis/enterprise-certificate-proxy v0.3.2 // indirect - github.com/googleapis/gax-go/v2 v2.12.5 // indirect + github.com/google/go-querystring v1.1.0 // indirect + github.com/google/s2a-go v0.1.9 // indirect + github.com/googleapis/enterprise-certificate-proxy v0.3.6 // indirect + github.com/googleapis/gax-go/v2 v2.14.1 // indirect github.com/jackc/pgpassfile v1.0.0 // indirect github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 // indirect github.com/jackc/puddle/v2 v2.2.2 // indirect github.com/josharian/intern v1.0.0 // indirect + github.com/klauspost/compress v1.18.0 // indirect github.com/lib/pq v1.10.9 // indirect github.com/mailru/easyjson v0.9.0 // indirect github.com/oapi-codegen/runtime v1.1.1 // indirect github.com/pinecone-io/go-pinecone/v4 v4.0.1 // indirect - github.com/pmezard/go-difflib v1.0.0 // indirect - github.com/rogpeppe/go-internal v1.14.1 // indirect - go.opencensus.io v0.24.0 // indirect + github.com/pingcap/errors v0.11.5-0.20210425183316-da1aaba5fb63 // indirect + github.com/pingcap/failpoint v0.0.0-20220801062533-2eaa32854a6c // indirect + github.com/pingcap/log v1.1.0 // indirect + github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect + github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect + github.com/tidwall/gjson v1.18.0 // indirect + github.com/tidwall/pretty v1.2.1 // indirect go.opentelemetry.io/auto/sdk v1.1.0 // indirect - go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.51.0 // indirect - go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.51.0 // indirect - go.opentelemetry.io/otel v1.35.0 // indirect - go.opentelemetry.io/otel/metric v1.35.0 // indirect - go.opentelemetry.io/otel/trace v1.35.0 // indirect + go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.60.0 // indirect + go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.61.0 // indirect + go.opentelemetry.io/otel v1.37.0 // indirect + go.opentelemetry.io/otel/metric v1.37.0 // indirect + go.opentelemetry.io/otel/trace v1.37.0 // indirect + go.uber.org/atomic v1.11.0 // indirect + go.uber.org/multierr v1.11.0 // indirect + go.uber.org/zap v1.27.0 // indirect golang.org/x/net v0.41.0 // indirect golang.org/x/oauth2 v0.28.0 // indirect golang.org/x/sync v0.15.0 // indirect golang.org/x/sys v0.33.0 // indirect golang.org/x/text v0.26.0 // indirect - golang.org/x/tools v0.33.0 // indirect - google.golang.org/api v0.186.0 // indirect + golang.org/x/tools v0.34.0 // indirect + google.golang.org/api v0.226.0 // indirect google.golang.org/genproto/googleapis/api v0.0.0-20250603155806-513f23925822 // indirect google.golang.org/genproto/googleapis/rpc v0.0.0-20250603155806-513f23925822 // indirect google.golang.org/grpc v1.73.0 // indirect google.golang.org/protobuf v1.36.6 // indirect gopkg.in/alexcesaro/quotedprintable.v3 v3.0.0-20150716171945-2caba252f4dc // indirect + gopkg.in/natefinch/lumberjack.v2 v2.2.1 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect ) diff --git a/go.sum b/go.sum index cbadec6..309251d 100644 --- a/go.sum +++ b/go.sum @@ -1,19 +1,20 @@ -cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= -cloud.google.com/go v0.115.0 h1:CnFSK6Xo3lDYRoBKEcAtia6VSC837/ZkJuRduSFnr14= -cloud.google.com/go v0.115.0/go.mod h1:8jIM5vVgoAEoiVxQ/O4BFTfHqulPZgs/ufEzMcFMdWU= +cloud.google.com/go v0.119.0 h1:tw7OjErMzJKbbjaEHkrt60KQrK5Wus/boCZ7tm5/RNE= +cloud.google.com/go v0.119.0/go.mod h1:fwB8QLzTcNevxqi8dcpR+hoMIs3jBherGS9VUBDAW08= cloud.google.com/go/ai v0.8.0 h1:rXUEz8Wp2OlrM8r1bfmpF2+VKqc1VJpafE3HgzRnD/w= cloud.google.com/go/ai v0.8.0/go.mod h1:t3Dfk4cM61sytiggo2UyGsDVW3RF1qGZaUKDrZFyqkE= -cloud.google.com/go/auth v0.6.0 h1:5x+d6b5zdezZ7gmLWD1m/xNjnaQ2YDhmIz/HH3doy1g= -cloud.google.com/go/auth v0.6.0/go.mod h1:b4acV+jLQDyjwm4OXHYjNvRi4jvGBzHWJRtJcy+2P4g= -cloud.google.com/go/auth/oauth2adapt v0.2.2 h1:+TTV8aXpjeChS9M+aTtN/TjdQnzJvmzKFt//oWu7HX4= -cloud.google.com/go/auth/oauth2adapt v0.2.2/go.mod h1:wcYjgpZI9+Yu7LyYBg4pqSiaRkfEK3GQcpb7C/uyF1Q= +cloud.google.com/go/auth v0.15.0 h1:Ly0u4aA5vG/fsSsxu98qCQBemXtAtJf+95z9HK+cxps= +cloud.google.com/go/auth v0.15.0/go.mod h1:WJDGqZ1o9E9wKIL+IwStfyn/+s59zl4Bi+1KQNVXLZ8= +cloud.google.com/go/auth/oauth2adapt v0.2.7 h1:/Lc7xODdqcEw8IrZ9SvwnlLX6j9FHQM74z6cBk9Rw6M= +cloud.google.com/go/auth/oauth2adapt v0.2.7/go.mod h1:NTbTTzfvPl1Y3V1nPpOgl2w6d/FjO7NNUQaWSox6ZMc= cloud.google.com/go/compute/metadata v0.6.0 h1:A6hENjEsCDtC1k8byVsgwvVcioamEHvZ4j01OwKxG9I= cloud.google.com/go/compute/metadata v0.6.0/go.mod h1:FjyFAW1MW0C203CEOMDTu3Dk1FlqW3Rga40jzHL4hfg= cloud.google.com/go/longrunning v0.5.7 h1:WLbHekDbjK1fVFD3ibpFFVoyizlLRl73I7YKuAKilhU= cloud.google.com/go/longrunning v0.5.7/go.mod h1:8GClkudohy1Fxm3owmBGid8W0pSgodEMwEAztp38Xng= github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= -github.com/Database-Hosting-Services/AI-Agent v1.0.0 h1:f3tVVFK4prqewAjr+e616LEKjOSX8JFPH8gUE48E3PM= -github.com/Database-Hosting-Services/AI-Agent v1.0.0/go.mod h1:4JoX4wIMVtU9ydEOFS6MiDQKnuRfCxSmuesGhmCokAY= +github.com/Database-Hosting-Services/AI-Agent v1.0.7 h1:y8kROFN8sVfhzThGmFybuWD6KUvXnoJAhOdU55oT2uA= +github.com/Database-Hosting-Services/AI-Agent v1.0.7/go.mod h1:4JoX4wIMVtU9ydEOFS6MiDQKnuRfCxSmuesGhmCokAY= +github.com/Database-Hosting-Services/AI-Agent v1.0.8 h1:gVg5ZiK1oNwDdEYUWPCPlYNZjeLtUqkdBuD3RhHPLug= +github.com/Database-Hosting-Services/AI-Agent v1.0.8/go.mod h1:4JoX4wIMVtU9ydEOFS6MiDQKnuRfCxSmuesGhmCokAY= github.com/KyleBanks/depth v1.2.1 h1:5h8fQADFrWtarTdtDudMmGsC7GPbOAu6RVB3ffsVFHc= github.com/KyleBanks/depth v1.2.1/go.mod h1:jzSb9d0L43HxTQfT+oSA1EEp2q+ne2uh6XgeJcm8brE= github.com/MarceloPetrucio/go-scalar-api-reference v0.0.0-20240521013641-ce5d2efe0e06 h1:W4Yar1SUsPmmA51qoIRb174uDO/Xt3C48MB1YX9Y3vM= @@ -21,34 +22,37 @@ github.com/MarceloPetrucio/go-scalar-api-reference v0.0.0-20240521013641-ce5d2ef github.com/RaveNoX/go-jsoncommentstrip v1.0.0/go.mod h1:78ihd09MekBnJnxpICcwzCMzGrKSKYe4AqU6PDYYpjk= github.com/apapsch/go-jsonmerge/v2 v2.0.0 h1:axGnT1gRIfimI7gJifB699GoE/oq+F2MU7Dml6nw9rQ= github.com/apapsch/go-jsonmerge/v2 v2.0.0/go.mod h1:lvDnEdqiQrp0O42VQGgmlKpxL1AP2+08jFMw88y4klk= +github.com/axiomhq/axiom-go v0.25.0 h1:D7tVqaiUiaUtF6JpeX5ddoLTJhtKpswMgEfDuDxSjvs= +github.com/axiomhq/axiom-go v0.25.0/go.mod h1:OZMPuSVdmdidEcJfJS4hRRaNowCySrjIUP5O5Q4qafc= +github.com/benbjohnson/clock v1.1.0/go.mod h1:J11/hYXuz8f4ySSvYwY0FKfm+ezbsZBKZxNJlLklBHA= github.com/bmatcuk/doublestar v1.1.1/go.mod h1:UD6OnuiIn0yFxxA2le/rnRU1G4RaI4UvFv1sNto9p6w= github.com/bsm/ginkgo/v2 v2.12.0 h1:Ny8MWAHyOepLGlLKYmXG4IEkioBysk6GpaRTLC8zwWs= github.com/bsm/ginkgo/v2 v2.12.0/go.mod h1:SwYbGRRDovPVboqFv0tPTcG1sN61LM1Z4ARdbAV9g4c= github.com/bsm/gomega v1.27.10 h1:yeMWxP2pV2fG3FgAODIY8EiRE3dy0aeFYt4l7wh6yKA= github.com/bsm/gomega v1.27.10/go.mod h1:JyEr/xRbxbtgWNi8tIEVPUYZ5Dzef52k01W3YH0H+O0= -github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= +github.com/cenkalti/backoff/v4 v4.3.0 h1:MyRJ/UdXutAwSAT+s3wNd7MfTIcy71VQueUuFK343L8= +github.com/cenkalti/backoff/v4 v4.3.0/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyYozVcomhLiZE= +github.com/cenkalti/backoff/v5 v5.0.2 h1:rIfFVxEf1QsI7E1ZHfp/B4DF/6QBAUhmgkxc0H7Zss8= +github.com/cenkalti/backoff/v5 v5.0.2/go.mod h1:rkhZdG3JZukswDf7f0cwqPNk4K0sa+F97BxZthm/crw= github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= -github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= -github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc= github.com/cockroachdb/cockroach-go/v2 v2.2.0 h1:/5znzg5n373N/3ESjHF5SMLxiW4RKB05Ql//KWfeTFs= github.com/cockroachdb/cockroach-go/v2 v2.2.0/go.mod h1:u3MiKYGupPPjkn3ozknpMUpxPaNLTFWAya419/zv6eI= +github.com/cznic/mathutil v0.0.0-20181122101859-297441e03548 h1:iwZdTE0PVqJCos1vaoKsclOGD3ADKpshg3SRtYBbwso= +github.com/cznic/mathutil v0.0.0-20181122101859-297441e03548/go.mod h1:e6NPNENfs9mPDVNRekM7lKScauxd5kXTr1Mfyig6TDM= 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/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM= +github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f h1:lO4WD4F/rVNCu3HqELle0jiPLLBs70cWOduZpkS1E78= github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f/go.mod h1:cuUVRXasLTGF7a8hSLbxyZXjz+1KgoB3wDUb6vlszIc= -github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= -github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= -github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98= -github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c= github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg= github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U= github.com/georgysavva/scany/v2 v2.1.4 h1:nrzHEJ4oQVRoiKmocRqA1IyGOmM/GQOEsg9UjMR5Ip4= github.com/georgysavva/scany/v2 v2.1.4/go.mod h1:fqp9yHZzM/PFVa3/rYEC57VmDx+KDch0LoqrJzkvtos= github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= -github.com/go-logr/logr v1.4.2 h1:6pFjapn8bFcIbiKo3XT4j/BhANplGihG6tvd+8rYgrY= -github.com/go-logr/logr v1.4.2/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= +github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI= +github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE= github.com/go-openapi/jsonpointer v0.21.1 h1:whnzv/pNXtK2FbX/W9yJfRmE2gsmkfahjMKB0fZvcic= @@ -59,53 +63,39 @@ github.com/go-openapi/spec v0.21.0 h1:LTVzPc3p/RzRnkQqLRndbAzjY0d0BCL72A6j3CdL9Z github.com/go-openapi/spec v0.21.0/go.mod h1:78u6VdPw81XU44qEWGhtr982gJ5BWg2c0I5XwVMotYk= github.com/go-openapi/swag v0.23.1 h1:lpsStH0n2ittzTnbaSloVZLuB5+fvSY/+hnagBjSNZU= github.com/go-openapi/swag v0.23.1/go.mod h1:STZs8TbRvEQQKUA+JZNAm3EWlgaOBGpyFDqQnDHMef0= -github.com/gofrs/flock v0.8.1 h1:+gYjHKf32LDeiEEFhQaotPbLuUXjY5ZqxKgXy7n59aw= -github.com/gofrs/flock v0.8.1/go.mod h1:F1TvTiK9OcQqauNUHlbJvyl9Qa1QvF/gOUDKA14jxHU= -github.com/golang-jwt/jwt/v5 v5.2.1 h1:OuVbFODueb089Lh128TAcimifWaLhJwVflnrgM17wHk= -github.com/golang-jwt/jwt/v5 v5.2.1/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk= -github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= -github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= -github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da h1:oI5xCqsCo564l8iNU+DwB5epxmsaqB+rhGL0m5jtYqE= -github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= -github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= -github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= -github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= -github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8= -github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA= -github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs= -github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w= -github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0= -github.com/golang/protobuf v1.4.1/go.mod h1:U8fpvMrcmy5pZrNK1lt4xCsGvpyWQ/VVv6QDs8UjoX8= -github.com/golang/protobuf v1.4.3/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= +github.com/go-sql-driver/mysql v1.7.1 h1:lUIinVbN1DY0xBg0eMOzmmtGoHwWBbvnWubQUrtU8EI= +github.com/go-sql-driver/mysql v1.7.1/go.mod h1:OXbVy3sEdcQ2Doequ6Z5BW6fXNQTmx+9S1MCJN5yJMI= +github.com/gofrs/flock v0.12.1 h1:MTLVXXHf8ekldpJk3AKicLij9MdwOWkZ+a/jHHZby9E= +github.com/gofrs/flock v0.12.1/go.mod h1:9zxTsyu5xtJ9DK+1tFZyibEV7y3uwDxPPfbxeeHCoD0= +github.com/golang-jwt/jwt/v5 v5.2.2 h1:Rl4B7itRWVtYIHFrSNd7vhTiz9UpLdi6gZhZ3wEeDy8= +github.com/golang-jwt/jwt/v5 v5.2.2/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk= github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek= github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps= github.com/google/generative-ai-go v0.20.1 h1:6dEIujpgN2V0PgLhr6c/M1ynRdc7ARtiIDPFzj45uNQ= github.com/google/generative-ai-go v0.20.1/go.mod h1:TjOnZJmZKzarWbjUJgy+r3Ee7HGBRVLhOIgupnwR4Bg= -github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= -github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= -github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= -github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= -github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= -github.com/google/go-cmp v0.5.3/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= -github.com/google/s2a-go v0.1.7 h1:60BLSyTrOV4/haCDW4zb1guZItoSq8foHCXrAnjBo/o= -github.com/google/s2a-go v0.1.7/go.mod h1:50CgR4k1jNlWBu4UfS4AcfhVe1r6pdZPygJ3R8F0Qdw= -github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/google/go-querystring v1.1.0 h1:AnCroh3fv4ZBgVIf1Iwtovgjaw/GiKJo8M8yD/fhyJ8= +github.com/google/go-querystring v1.1.0/go.mod h1:Kcdr2DB4koayq7X8pmAG4sNG59So17icRSOU623lUBU= +github.com/google/s2a-go v0.1.9 h1:LGD7gtMgezd8a/Xak7mEWL0PjoTQFvpRudN895yqKW0= +github.com/google/s2a-go v0.1.9/go.mod h1:YA0Ei2ZQL3acow2O62kdp9UlnvMmU7kA6Eutn0dXayM= github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= -github.com/googleapis/enterprise-certificate-proxy v0.3.2 h1:Vie5ybvEvT75RniqhfFxPRy3Bf7vr3h0cechB90XaQs= -github.com/googleapis/enterprise-certificate-proxy v0.3.2/go.mod h1:VLSiSSBs/ksPL8kq3OBOQ6WRI2QnaFynd1DCjZ62+V0= -github.com/googleapis/gax-go/v2 v2.12.5 h1:8gw9KZK8TiVKB6q3zHY3SBzLnrGp6HQjyfYBYGmXdxA= -github.com/googleapis/gax-go/v2 v2.12.5/go.mod h1:BUDKcWo+RaKq5SC9vVYL0wLADa3VcfswbOMMRmB9H3E= +github.com/googleapis/enterprise-certificate-proxy v0.3.6 h1:GW/XbdyBFQ8Qe+YAmFU9uHLo7OnF5tL52HFAgMmyrf4= +github.com/googleapis/enterprise-certificate-proxy v0.3.6/go.mod h1:MkHOF77EYAE7qfSuSS9PU6g4Nt4e11cnsDUowfwewLA= +github.com/googleapis/gax-go/v2 v2.14.1 h1:hb0FFeiPaQskmvakKu5EbCbpntQn48jyHuvrkurSS/Q= +github.com/googleapis/gax-go/v2 v2.14.1/go.mod h1:Hb/NubMaVM88SrNkvl8X/o8XWwDJEPqouaLeN2IUxoA= github.com/gorilla/mux v1.8.1 h1:TuBL49tXwgrFYWhqrNgrUNEY92u81SPhu7sTdzQEiWY= github.com/gorilla/mux v1.8.1/go.mod h1:AKf9I4AEqPTmMytcMc0KkNouC66V3BtZ4qD5fmWSiMQ= +github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.1 h1:X5VWvz21y3gzm9Nw/kaUeku/1+uBhcekkmy4IkffJww= +github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.1/go.mod h1:Zanoh4+gvIgluNqcfMVTJueD4wSS5hT7zTt4Mrutd90= 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.2 h1:mLoDLV6sonKlvjIEsV56SkWNCnuNv531l94GaIzO+XI= -github.com/jackc/pgx/v5 v5.7.2/go.mod h1:ncY89UGWxg82EykZUwSpUKEfccBGGYq1xjrOpsbsfGQ= +github.com/jackc/pgx/v5 v5.7.5 h1:JHGfMnQY+IEtGM63d+NGMjoRpysB2JBwDr5fsngwmJs= +github.com/jackc/pgx/v5 v5.7.5/go.mod h1:aruU7o91Tc2q2cFp5h4uP3f6ztExVpyVv88Xl/8Vl8M= github.com/jackc/puddle/v2 v2.2.2 h1:PR8nw+E/1w0GLuRFSmiioY6UooMp6KJv0/61nB7icHo= github.com/jackc/puddle/v2 v2.2.2/go.mod h1:vriiEXHvEE654aYKXXjOvZM39qJ0q+azkZFrfEOc3H4= github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0= @@ -113,8 +103,13 @@ github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwA github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY= github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y= github.com/juju/gnuflag v0.0.0-20171113085948-2ce1bb71843d/go.mod h1:2PavIy+JPciBPrBUjwbNvtwB6RQlve+hkpll6QSNmOE= +github.com/klauspost/compress v1.18.0 h1:c/Cqfb0r+Yi+JtIEq73FWXVkRonBlf0CRNYc8Zttxdo= +github.com/klauspost/compress v1.18.0/go.mod h1:2Pp+KzxcywXVXMr50+X0Q/Lsb43OQHYWRCY2AiWywWQ= +github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= +github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= +github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= github.com/lib/pq v1.10.9 h1:YXG7RB+JIjhP29X+OtkiDnYaXQwpS4JEWq7dtCCRUEw= @@ -125,134 +120,144 @@ github.com/oapi-codegen/runtime v1.1.1 h1:EXLHh0DXIJnWhdRPN2w4MXAzFyE4CskzhNLUmt github.com/oapi-codegen/runtime v1.1.1/go.mod h1:SK9X900oXmPWilYR5/WKPzt3Kqxn/uS/+lbpREv+eCg= github.com/pinecone-io/go-pinecone/v4 v4.0.1 h1:eieqQYlRM1RKAoMaw7x3lSGw2V2XAmTC5psX0sqPlXw= github.com/pinecone-io/go-pinecone/v4 v4.0.1/go.mod h1:bLU4DLM79YPfaVLOj23yBPsIohnZDIuUmnTsQXWHzSg= +github.com/pingcap/errors v0.11.0/go.mod h1:Oi8TUi2kEtXXLMJk9l1cGmz20kV3TaQ0usTwv5KuLY8= +github.com/pingcap/errors v0.11.4/go.mod h1:Oi8TUi2kEtXXLMJk9l1cGmz20kV3TaQ0usTwv5KuLY8= +github.com/pingcap/errors v0.11.5-0.20210425183316-da1aaba5fb63 h1:+FZIDR/D97YOPik4N4lPDaUcLDF/EQPogxtlHB2ZZRM= +github.com/pingcap/errors v0.11.5-0.20210425183316-da1aaba5fb63/go.mod h1:X2r9ueLEUZgtx2cIogM0v4Zj5uvvzhuuiu7Pn8HzMPg= +github.com/pingcap/failpoint v0.0.0-20220801062533-2eaa32854a6c h1:CgbKAHto5CQgWM9fSBIvaxsJHuGP0uM74HXtv3MyyGQ= +github.com/pingcap/failpoint v0.0.0-20220801062533-2eaa32854a6c/go.mod h1:4qGtCB0QK0wBzKtFEGDhxXnSnbQApw1gc9siScUl8ew= +github.com/pingcap/log v1.1.0 h1:ELiPxACz7vdo1qAvvaWJg1NrYFoY6gqAh/+Uo6aXdD8= +github.com/pingcap/log v1.1.0/go.mod h1:DWQW5jICDR7UJh4HtxXSM20Churx4CQL0fwL/SoOSA4= +github.com/pingcap/tidb/parser v0.0.0-20231013125129-93a834a6bf8d h1:EHXDxa7eq8vWc2T8cwstlr3A48dx4TvMsCh5Y7z2VZ8= +github.com/pingcap/tidb/parser v0.0.0-20231013125129-93a834a6bf8d/go.mod h1:cwq4bKUlftpWuznB+rqNwbN0xy6/i5SL/nYvEKeJn4s= +github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= -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/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= -github.com/redis/go-redis/v9 v9.7.1 h1:4LhKRCIduqXqtvCUlaq9c8bdHOkICjDMrr1+Zb3osAc= -github.com/redis/go-redis/v9 v9.7.1/go.mod h1:f6zhXITC7JUJIlPEiBOTXxJgPLdZcA93GewI7inzyWw= +github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U= +github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/redis/go-redis/v9 v9.11.0 h1:E3S08Gl/nJNn5vkxd2i78wZxWAPNZgUNTp8WIJUAiIs= +github.com/redis/go-redis/v9 v9.11.0/go.mod h1:huWgSWd8mW6+m0VPhJjSSQ+d6Nh1VICQ6Q5lHuCH/Iw= +github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec h1:W09IVJc94icq4NjY3clb7Lk8O1qJ8BdBEF8z0ibU0rE= +github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo= 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.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ= github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc= +github.com/sergi/go-diff v1.1.0/go.mod h1:STckp+ISIX8hZLjrqAeVduY0gWCT9IjLuqbuNXdaHfM= github.com/spkg/bom v0.0.0-20160624110644-59b7046e48ad/go.mod h1:qLr4V1qq6nMqFKkMo8ZTx3f+BZEkzsRUY10Xsm2mwU0= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= -github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= -github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= github.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY= github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA= github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= +github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= -github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= -github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= -github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= github.com/swaggo/swag v1.16.4 h1:clWJtd9LStiG3VeijiCfOVODP6VpHtKdQy9ELFG3s1A= github.com/swaggo/swag v1.16.4/go.mod h1:VBsHJRsDvfYvqoiMKnsdwhNV9LEMHgEDZcyVYX0sxPg= -go.opencensus.io v0.24.0 h1:y73uSU6J157QMP2kn2r30vwW1A2W2WFwSCGnAVxeaD0= -go.opencensus.io v0.24.0/go.mod h1:vNK8G9p7aAivkbmorf4v+7Hgx+Zs0yY+0fOtgBfjQKo= +github.com/tidwall/gjson v1.18.0 h1:FIDeeyB800efLX89e5a8Y0BNH+LOngJyGrIWxG2FKQY= +github.com/tidwall/gjson v1.18.0/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk= +github.com/tidwall/match v1.1.1 h1:+Ho715JplO36QYgwN9PGYNhgZvoUSc9X2c80KVTi+GA= +github.com/tidwall/match v1.1.1/go.mod h1:eRSPERbgtNPcGhD8UCthc6PmLEQXEWd3PRB5JTxsfmM= +github.com/tidwall/pretty v1.2.0/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU= +github.com/tidwall/pretty v1.2.1 h1:qjsOFOWWQl+N3RsoF5/ssm1pHmJJwhjlSbZ51I6wMl4= +github.com/tidwall/pretty v1.2.1/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU= +github.com/tidwall/sjson v1.2.5 h1:kLy8mja+1c9jlljvWTlSazM7cKDRfJuR/bOJhcY5NcY= +github.com/tidwall/sjson v1.2.5/go.mod h1:Fvgq9kS/6ociJEDnK0Fk1cpYF4FIW6ZF7LAe+6jwd28= go.opentelemetry.io/auto/sdk v1.1.0 h1:cH53jehLUN6UFLY71z+NDOiNJqDdPRaXzTel0sJySYA= go.opentelemetry.io/auto/sdk v1.1.0/go.mod h1:3wSPjt5PWp2RhlCcmmOial7AvC4DQqZb7a7wCow3W8A= -go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.51.0 h1:A3SayB3rNyt+1S6qpI9mHPkeHTZbD7XILEqWnYZb2l0= -go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.51.0/go.mod h1:27iA5uvhuRNmalO+iEUdVn5ZMj2qy10Mm+XRIpRmyuU= -go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.51.0 h1:Xs2Ncz0gNihqu9iosIZ5SkBbWo5T8JhhLJFMQL1qmLI= -go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.51.0/go.mod h1:vy+2G/6NvVMpwGX/NyLqcC41fxepnuKHk16E6IZUcJc= -go.opentelemetry.io/otel v1.35.0 h1:xKWKPxrxB6OtMCbmMY021CqC45J+3Onta9MqjhnusiQ= -go.opentelemetry.io/otel v1.35.0/go.mod h1:UEqy8Zp11hpkUrL73gSlELM0DupHoiq72dR+Zqel/+Y= -go.opentelemetry.io/otel/metric v1.35.0 h1:0znxYu2SNyuMSQT4Y9WDWej0VpcsxkuklLa4/siN90M= -go.opentelemetry.io/otel/metric v1.35.0/go.mod h1:nKVFgxBZ2fReX6IlyW28MgZojkoAkJGaE8CpgeAU3oE= -go.opentelemetry.io/otel/sdk v1.35.0 h1:iPctf8iprVySXSKJffSS79eOjl9pvxV9ZqOWT0QejKY= -go.opentelemetry.io/otel/sdk v1.35.0/go.mod h1:+ga1bZliga3DxJ3CQGg3updiaAJoNECOgJREo9KHGQg= -go.opentelemetry.io/otel/sdk/metric v1.35.0 h1:1RriWBmCKgkeHEhM7a2uMjMUfP7MsOF5JpUCaEqEI9o= -go.opentelemetry.io/otel/sdk/metric v1.35.0/go.mod h1:is6XYCUMpcKi+ZsOvfluY5YstFnhW0BidkR+gL+qN+w= -go.opentelemetry.io/otel/trace v1.35.0 h1:dPpEfJu1sDIqruz7BHFG3c7528f6ddfSWfFDVt/xgMs= -go.opentelemetry.io/otel/trace v1.35.0/go.mod h1:WUk7DtFp1Aw2MkvqGdwiXYDZZNvA/1J8o6xRXLrIkyc= +go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.60.0 h1:x7wzEgXfnzJcHDwStJT+mxOz4etr2EcexjqhBvmoakw= +go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.60.0/go.mod h1:rg+RlpR5dKwaS95IyyZqj5Wd4E13lk/msnTS0Xl9lJM= +go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.61.0 h1:F7Jx+6hwnZ41NSFTO5q4LYDtJRXBf2PD0rNBkeB/lus= +go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.61.0/go.mod h1:UHB22Z8QsdRDrnAtX4PntOl36ajSxcdUMt1sF7Y6E7Q= +go.opentelemetry.io/otel v1.37.0 h1:9zhNfelUvx0KBfu/gb+ZgeAfAgtWrfHJZcAqFC228wQ= +go.opentelemetry.io/otel v1.37.0/go.mod h1:ehE/umFRLnuLa/vSccNq9oS1ErUlkkK71gMcN34UG8I= +go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.37.0 h1:Ahq7pZmv87yiyn3jeFz/LekZmPLLdKejuO3NcK9MssM= +go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.37.0/go.mod h1:MJTqhM0im3mRLw1i8uGHnCvUEeS7VwRyxlLC78PA18M= +go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.37.0 h1:bDMKF3RUSxshZ5OjOTi8rsHGaPKsAt76FaqgvIUySLc= +go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.37.0/go.mod h1:dDT67G/IkA46Mr2l9Uj7HsQVwsjASyV9SjGofsiUZDA= +go.opentelemetry.io/otel/metric v1.37.0 h1:mvwbQS5m0tbmqML4NqK+e3aDiO02vsf/WgbsdpcPoZE= +go.opentelemetry.io/otel/metric v1.37.0/go.mod h1:04wGrZurHYKOc+RKeye86GwKiTb9FKm1WHtO+4EVr2E= +go.opentelemetry.io/otel/sdk v1.37.0 h1:ItB0QUqnjesGRvNcmAcU0LyvkVyGJ2xftD29bWdDvKI= +go.opentelemetry.io/otel/sdk v1.37.0/go.mod h1:VredYzxUvuo2q3WRcDnKDjbdvmO0sCzOvVAiY+yUkAg= +go.opentelemetry.io/otel/sdk/metric v1.36.0 h1:r0ntwwGosWGaa0CrSt8cuNuTcccMXERFwHX4dThiPis= +go.opentelemetry.io/otel/sdk/metric v1.36.0/go.mod h1:qTNOhFDfKRwX0yXOqJYegL5WRaW376QbB7P4Pb0qva4= +go.opentelemetry.io/otel/trace v1.37.0 h1:HLdcFNbRQBE2imdSEgm/kwqmQj1Or1l/7bW6mxVK7z4= +go.opentelemetry.io/otel/trace v1.37.0/go.mod h1:TlgrlQ+PtQO5XFerSPUYG0JSgGyryXewPGyayAWSBS0= +go.opentelemetry.io/proto/otlp v1.7.0 h1:jX1VolD6nHuFzOYso2E73H85i92Mv8JQYk0K9vz09os= +go.opentelemetry.io/proto/otlp v1.7.0/go.mod h1:fSKjH6YJ7HDlwzltzyMj036AJ3ejJLCgCSHGj4efDDo= +go.uber.org/atomic v1.6.0/go.mod h1:sABNBOSYdrvTF6hTgEIbc7YasKWGhgEQZyfxyTvoXHQ= +go.uber.org/atomic v1.7.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc= +go.uber.org/atomic v1.9.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc= +go.uber.org/atomic v1.11.0 h1:ZvwS0R+56ePWxUNi+Atn9dWONBPp/AUETXlHW0DxSjE= +go.uber.org/atomic v1.11.0/go.mod h1:LUxbIzbOniOlMKjJjyPfpl4v+PKK2cNJn91OQbhoJI0= +go.uber.org/goleak v1.1.10/go.mod h1:8a7PlsEVH3e/a/GLqe5IIrQx6GzcnRmZEufDUTk4A7A= +go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= +go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE= +go.uber.org/multierr v1.6.0/go.mod h1:cdWPpRnG4AhwMwsgIHip0KRBQjJy5kYEpYjJxpXp9iU= +go.uber.org/multierr v1.7.0/go.mod h1:7EAYxJLBy9rStEaz58O2t4Uvip6FSURkq8/ppBp95ak= +go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0= +go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y= +go.uber.org/zap v1.19.0/go.mod h1:xg/QME4nWcxGxrpdeYfq7UvYrLh66cuVKdrbD1XF/NI= +go.uber.org/zap v1.27.0 h1:aJMhYGrd5QSmlpLMr2MftRKl7t8J8PTZPA732ud/XR8= +go.uber.org/zap v1.27.0/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= -golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/crypto v0.39.0 h1:SHs+kF4LP+f+p14esP5jAoDpHU8Gu/v9lFRK6IT5imM= golang.org/x/crypto v0.39.0/go.mod h1:L+Xg3Wf6HoL4Bn4238Z6ft6KfEpN0tJGo53AAPC632U= -golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= -golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= -golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU= -golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= +golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= golang.org/x/mod v0.25.0 h1:n7a+ZbQKQA/Ysbyb0/6IbB1H/X41mKgbhfv7AfG/44w= golang.org/x/mod v0.25.0/go.mod h1:IXM97Txy2VM4PJ3gI61r1YEk/gAj6zAHN3AdZt6S9Ww= -golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= -golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= -golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= -golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= -golang.org/x/net v0.0.0-20201110031124-69a78807bb2b/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= +golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.41.0 h1:vBTly1HeNPEn3wtREYfy4GZ/NECgw2Cnl+nK6Nz3uvw= golang.org/x/net v0.41.0/go.mod h1:B/K4NNqkfmg07DQYrbwvSluqCJOOXwUjeb/5lOisjbA= -golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= golang.org/x/oauth2 v0.28.0 h1:CrgCKl8PPAVtLnU3c+EDw6x11699EWlsDeWNWKdIOkc= golang.org/x/oauth2 v0.28.0/go.mod h1:onh5ek6nERTohokkhCD/y2cV4Do3fxFHFuAejCkRWT8= -golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.15.0 h1:KWH3jNZsfyT6xfAfKiz6MRNmd46ByHDYaZ7KSkCtdW8= golang.org/x/sync v0.15.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA= -golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= -golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.33.0 h1:q3i8TbbEz+JRD9ywIRlyRAQbM0qF7hu24q3teo2hbuw= golang.org/x/sys v0.33.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= -golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.26.0 h1:P42AVeLghgTYr4+xUnTRKDMqpar+PtX7KWuNQL21L8M= golang.org/x/text v0.26.0/go.mod h1:QK15LZJUUQVJxhz7wXgxSy/CJaTFjd0G+YLonydOVQA= -golang.org/x/time v0.11.0 h1:/bpjEDfN9tkoN/ryeYHnv5hcMlc8ncjMcM4XBk5NWV0= -golang.org/x/time v0.11.0/go.mod h1:CDIdPxbZBQxdj6cxyCIdrNogrJKMJ7pr37NYpMcMDSg= -golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= -golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= -golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY= +golang.org/x/time v0.12.0 h1:ScB/8o8olJvc+CQPWrK3fPZNfh7qgwCrY0zJmoEQLSE= +golang.org/x/time v0.12.0/go.mod h1:CDIdPxbZBQxdj6cxyCIdrNogrJKMJ7pr37NYpMcMDSg= golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= -golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= -golang.org/x/tools v0.33.0 h1:4qz2S3zmRxbGIhDIAgjxvFutSvH5EfnsYrRBj0UI0bc= -golang.org/x/tools v0.33.0/go.mod h1:CIJMaWEY88juyUfo7UbgPqbC8rU2OqfAV1h2Qp0oMYI= +golang.org/x/tools v0.0.0-20191029041327-9cc4af7d6b2c/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191108193012-7d206e10da11/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.34.0 h1:qIpSLOxeCYGg9TrcJokLBG4KFA6d795g0xkBkiESGlo= +golang.org/x/tools v0.34.0/go.mod h1:pAP9OwEaY1CAW3HOmg3hLZC5Z0CCmzjAF2UQMSqNARg= +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= -google.golang.org/api v0.186.0 h1:n2OPp+PPXX0Axh4GuSsL5QL8xQCTb2oDwyzPnQvqUug= -google.golang.org/api v0.186.0/go.mod h1:hvRbBmgoje49RV3xqVXrmP6w93n6ehGgIVPYrGtBFFc= -google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= -google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= -google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= -google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= -google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo= +google.golang.org/api v0.226.0 h1:9A29y1XUD+YRXfnHkO66KggxHBZWg9LsTGqm7TkUvtQ= +google.golang.org/api v0.226.0/go.mod h1:WP/0Xm4LVvMOCldfvOISnWquSRWbG2kArDZcg+W2DbY= google.golang.org/genproto/googleapis/api v0.0.0-20250603155806-513f23925822 h1:oWVWY3NzT7KJppx2UKhKmzPq4SRe0LdCijVRwvGeikY= google.golang.org/genproto/googleapis/api v0.0.0-20250603155806-513f23925822/go.mod h1:h3c4v36UTKzUiuaOKQ6gr3S+0hovBtUrXzTG/i3+XEc= google.golang.org/genproto/googleapis/rpc v0.0.0-20250603155806-513f23925822 h1:fc6jSaCT0vBduLYZHYrBBNY4dsWuvgyff9noRNDdBeE= google.golang.org/genproto/googleapis/rpc v0.0.0-20250603155806-513f23925822/go.mod h1:qQ0YXyHHx3XkvlzUtpXDkS29lDSafHMZBAZDc03LQ3A= -google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= -google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg= -google.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY= -google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= -google.golang.org/grpc v1.33.2/go.mod h1:JMHMWHQWaTccqQQlmk3MJZS+GWXOdAesneDmEnv2fbc= google.golang.org/grpc v1.73.0 h1:VIWSmpI2MegBtTuFt5/JWy2oXxtjJ/e89Z70ImfD2ok= google.golang.org/grpc v1.73.0/go.mod h1:50sbHOUqWoCQGI8V2HQLJM0B+LMlIUjNSZmow7EVBQc= -google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8= -google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0= -google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM= -google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE= -google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo= -google.golang.org/protobuf v1.22.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= -google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= -google.golang.org/protobuf v1.23.1-0.20200526195155-81db48ad09cc/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= -google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c= google.golang.org/protobuf v1.36.6 h1:z1NpPI8ku2WgiWnf+t9wTPsn6eP1L7ksHUlkfLvd9xY= google.golang.org/protobuf v1.36.6/go.mod h1:jduwjTPXsFjZGTmRluh+L6NjiWu7pchiJ2/5YcXBHnY= gopkg.in/alexcesaro/quotedprintable.v3 v3.0.0-20150716171945-2caba252f4dc h1:2gGKlE2+asNV9m7xrywl36YYNnBG5ZQ0r/BOOxqPpmk= gopkg.in/alexcesaro/quotedprintable.v3 v3.0.0-20150716171945-2caba252f4dc/go.mod h1:m7x9LTH6d71AHyAX77c9yqWCCa3UKHcVEj9y7hAtKDk= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= gopkg.in/gomail.v2 v2.0.0-20160411212932-81ebce5c23df h1:n7WqCuqOuCbNr617RXOY0AWRXxgwEyPp2z+p0+hgMuE= gopkg.in/gomail.v2 v2.0.0-20160411212932-81ebce5c23df/go.mod h1:LRQQ+SO6ZHR7tOkpBDuZnXENFzX8qRjMDMyPD6BRkCw= +gopkg.in/natefinch/lumberjack.v2 v2.0.0/go.mod h1:l0ndWWf7gzL7RNwBG7wST/UCcT4T24xpD6X8LsfU/+k= +gopkg.in/natefinch/lumberjack.v2 v2.2.1 h1:bBRl1b0OH9s/DuPhuXpNl+VtCaJXFZ5/uEFST95x9zc= +gopkg.in/natefinch/lumberjack.v2 v2.2.1/go.mod h1:YD8tP3GAjkrDg1eZH7EGmyESg/lsYskCTPBJVb9jqSc= +gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= -honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= -honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= diff --git a/indexes/handlers.go b/indexes/handlers.go index 8efeec2..a5ebd4c 100644 --- a/indexes/handlers.go +++ b/indexes/handlers.go @@ -29,18 +29,18 @@ func CreateIndex(app *config.Application) http.HandlerFunc { urlVariables := mux.Vars(r) projectOid := urlVariables["project_id"] if projectOid == "" { - response.BadRequest(w, "Project Id is required", nil) + response.BadRequest(w, r, "Project Id is required", nil) return } var indexData IndexData if err := json.NewDecoder(r.Body).Decode(&indexData); err != nil { - response.BadRequest(w, "Invalid request body", nil) + response.BadRequest(w, r, "Invalid request body", nil) return } if indexData.IndexName == "" || indexData.IndexType == "" || len(indexData.Columns) == 0 || indexData.TableName == "" { - response.BadRequest(w, "Index name, type, columns and table name are required", nil) + response.BadRequest(w, r, "Index name, type, columns and table name are required", nil) return } @@ -52,7 +52,7 @@ func CreateIndex(app *config.Application) http.HandlerFunc { return } - response.Created(w, "Index created successfully", nil) + response.Created(w, r, "Index created successfully", nil) } } @@ -72,7 +72,7 @@ func ProjectIndexes(app *config.Application) http.HandlerFunc { urlVariables := mux.Vars(r) projectOid := urlVariables["project_id"] if projectOid == "" { - response.BadRequest(w, "Project Id is required", nil) + response.BadRequest(w, r, "Project Id is required", nil) return } @@ -84,7 +84,7 @@ func ProjectIndexes(app *config.Application) http.HandlerFunc { return } - response.OK(w, "Indexes retrieved successfully", indexes) + response.OK(w, r, "Indexes retrieved successfully", indexes) } } @@ -107,7 +107,7 @@ func GetIndex(app *config.Application) http.HandlerFunc { urlVariables := mux.Vars(r) indexOid, projectOid := urlVariables["index_oid"], urlVariables["project_id"] if indexOid == "" || projectOid == "" { - response.BadRequest(w, "Index Id and Project Id are required", nil) + response.BadRequest(w, r, "Index Id and Project Id are required", nil) return } @@ -118,7 +118,7 @@ func GetIndex(app *config.Application) http.HandlerFunc { return } - response.OK(w, "Index retrieved successfully", index) + response.OK(w, r, "Index retrieved successfully", index) } } @@ -141,7 +141,7 @@ func DeleteIndex(app *config.Application) http.HandlerFunc { urlVariables := mux.Vars(r) indexOid, projectOid := urlVariables["index_oid"], urlVariables["project_id"] if indexOid == "" || projectOid == "" { - response.BadRequest(w, "Index Id and Project Id are required", nil) + response.BadRequest(w, r, "Index Id and Project Id are required", nil) return } @@ -151,7 +151,7 @@ func DeleteIndex(app *config.Application) http.HandlerFunc { return } - response.OK(w, "Index deleted successfully", nil) + response.OK(w, r, "Index deleted successfully", nil) } } @@ -176,18 +176,18 @@ func UpdateIndexName(app *config.Application) http.HandlerFunc { urlVariables := mux.Vars(r) indexOid, projectOid := urlVariables["index_oid"], urlVariables["project_id"] if indexOid == "" || projectOid == "" { - response.BadRequest(w, "Index Id and Project Id are required", nil) + response.BadRequest(w, r, "Index Id and Project Id are required", nil) return } var indexData UpdateName if err := json.NewDecoder(r.Body).Decode(&indexData); err != nil { - response.BadRequest(w, "Invalid request body", nil) + response.BadRequest(w, r, "Invalid request body", nil) return } if indexData.Name == "" { - response.BadRequest(w, "Index name is required", nil) + response.BadRequest(w, r, "Index name is required", nil) return } @@ -198,6 +198,6 @@ func UpdateIndexName(app *config.Application) http.HandlerFunc { return } - response.OK(w, "Index name updated successfully", nil) + response.OK(w, r, "Index name updated successfully", nil) } } diff --git a/main/routes.go b/main/routes.go index 4df776e..686c582 100644 --- a/main/routes.go +++ b/main/routes.go @@ -1,8 +1,9 @@ package main import ( - "DBHS/accounts" "DBHS/AI" + sqleditor "DBHS/SqlEditor" + "DBHS/accounts" "DBHS/analytics" "DBHS/indexes" "DBHS/projects" @@ -18,4 +19,5 @@ func defineURLs() { tables.DefineURLs() ai.DefineURLs() analytics.DefineURLs() + sqleditor.DefineURLs() } diff --git a/middleware/middleware.go b/middleware/middleware.go index c190bd0..7f6d65c 100644 --- a/middleware/middleware.go +++ b/middleware/middleware.go @@ -21,7 +21,7 @@ func MethodsAllowed(methods ...string) func(http.Handler) http.Handler { return } } - response.MethodNotAllowed(w, strings.Join(methods, ","), "", nil) + response.MethodNotAllowed(w, r, strings.Join(methods, ","), "", nil) }) } } @@ -30,7 +30,7 @@ func Route(hundlers map[string]http.HandlerFunc) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { handler, ok := hundlers[r.Method] if !ok { - response.MethodNotAllowed(w, strings.Join(slices.Collect(maps.Keys(hundlers)), ","), "", nil) + response.MethodNotAllowed(w, r, strings.Join(slices.Collect(maps.Keys(hundlers)), ","), "", nil) return } handler.ServeHTTP(w, r) @@ -40,38 +40,97 @@ func Route(hundlers map[string]http.HandlerFunc) http.Handler { func CheckOwnership(next http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { urlVariables := mux.Vars(r) - projectId := urlVariables["project_id"] - userId := r.Context().Value("user-id").(int) - ok, err := utils.CheckOwnershipQuery(r.Context(), projectId, userId, config.DB) + projectOID := urlVariables["project_id"] + userId := r.Context().Value("user-id").(int64) + config.App.InfoLog.Printf("Checking ownership for project %s and user %d", projectOID, userId) + // check if the project exists + exists, err := utils.CheckProjectExist(r.Context(), projectOID, config.DB) if err != nil { - response.InternalServerError(w, err.Error(), err) + response.InternalServerError(w, r, "Failed to check project existence", err) return } + if !exists { + config.App.ErrorLog.Printf("Project %s does not exist", projectOID) + response.NotFound(w, r, "Project not found", nil) + return + } + // check if the user is the owner of the project + ok, err := utils.CheckOwnershipQuery(r.Context(), projectOID, userId, config.DB) + if err != nil { + response.InternalServerError(w, r, err.Error(), err) + return + } + config.App.InfoLog.Printf("Ownership check for project %s by user %d: %t", projectOID, userId, ok) if !ok { - response.UnAuthorized(w, "UnAuthorized", nil) + response.UnAuthorized(w, r, "UnAuthorized", nil) return } + next.ServeHTTP(w, r) + }) +} +func CheckOTableExist(next http.Handler) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + urlVariables := mux.Vars(r) + projectOID := urlVariables["project_id"] + tableOID := urlVariables["table_id"] + if tableOID != "" { + // get the project id from the database + _, projectId, err := utils.GetProjectNameID(r.Context(), projectOID, config.DB) + if err != nil { + response.InternalServerError(w, r, "Failed to get project ID", err) + return + } + // check if the table exists + exists, err := utils.CheckTableExist(r.Context(), tableOID, config.DB) + if err != nil { + response.InternalServerError(w, r, "Failed to check table existence", err) + return + } + + if !exists { + config.App.ErrorLog.Printf("Table %s does not exist in project %s", tableOID, projectOID) + response.NotFound(w, r, "Table not found", nil) + return + } + //check if the table belongs to the project + ok, err := utils.CheckOwnershipQueryTable(r.Context(), tableOID, projectId.(int64), config.DB) + if err != nil { + response.InternalServerError(w, r, err.Error(), err) + return + } + + if !ok { + response.NotFound(w, r, "Table not found", nil) + return + } + } next.ServeHTTP(w, r) }) } func EnableCORS(next http.Handler) http.Handler { - return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - origin := r.Header.Get("Origin") - if strings.HasPrefix(origin, "http://localhost") { - w.Header().Set("Access-Control-Allow-Origin", origin) - w.Header().Set("Access-Control-Allow-Methods", "GET, POST, OPTIONS") - w.Header().Set("Access-Control-Allow-Headers", "Content-Type, Authorization") - w.Header().Set("Access-Control-Allow-Credentials", "true") - } + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + // Allow all origins + w.Header().Set("Access-Control-Allow-Origin", "*") + + // Allow all common HTTP methods used by the API + w.Header().Set("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE, PATCH, OPTIONS") + + // Allow common headers used by the API + w.Header().Set("Access-Control-Allow-Headers", "Content-Type, Authorization, X-Requested-With, Accept, Origin") - if r.Method == "OPTIONS" { - w.WriteHeader(http.StatusNoContent) - return - } + // Note: When using "*" for Allow-Origin, we cannot use Allow-Credentials: true + // If you need credentials, you'll need to specify specific origins instead of "*" + // w.Header().Set("Access-Control-Allow-Credentials", "true") - next.ServeHTTP(w, r) - }) -} \ No newline at end of file + // Handle preflight OPTIONS requests + if r.Method == "OPTIONS" { + w.WriteHeader(http.StatusNoContent) + return + } + + next.ServeHTTP(w, r) + }) +} diff --git a/middleware/rateLimiter.go b/middleware/rateLimiter.go index 81d18ee..d500864 100644 --- a/middleware/rateLimiter.go +++ b/middleware/rateLimiter.go @@ -17,7 +17,7 @@ func LimitMiddleware(next http.Handler) http.Handler { // Check if this request is allowed if !client.Limiter.Allow() { - response.TooManyRequests(w, "Rate limit exceeded", errors.New("rate limit exceeded")) + response.TooManyRequests(w, r, "Rate limit exceeded", errors.New("rate limit exceeded")) return } diff --git a/middleware/token.go b/middleware/token.go index c23bf71..3f5d9eb 100644 --- a/middleware/token.go +++ b/middleware/token.go @@ -28,26 +28,26 @@ func JwtAuthMiddleware(next http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { authToken := utils.ExtractToken(r) if authToken == "" { - response.UnAuthorized(w, "Authorization failed", fmt.Errorf("JWT token is empty")) + response.UnAuthorized(w, r, "Authorization failed", fmt.Errorf("JWT token is empty")) return } err := token.IsAuthorized(authToken) if err != nil { - response.UnAuthorized(w, "Authorization failed", err) + response.UnAuthorized(w, r, "Authorization failed", err) return } fields, err := token.GetData(authToken, "oid", "username") if err != nil { - response.UnAuthorized(w, "Authorization failed", err) + response.UnAuthorized(w, r, "Authorization failed", err) return } if len(fields) >= 2 { userID, err := GetUserByOid(r.Context(), fields[0].(string)) if err != nil { - response.UnAuthorized(w, "Authorization failed", errors.New("No user found for this token")) + response.UnAuthorized(w, r, "Authorization failed", errors.New("No user found for this token")) return } ctx := utils.AddToContext(r.Context(), map[string]interface{}{ diff --git a/projects/handlers.go b/projects/handlers.go index f7dbf38..f6cb11a 100644 --- a/projects/handlers.go +++ b/projects/handlers.go @@ -28,7 +28,7 @@ func CreateProject(app *config.Application) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { project := Project{} if err := json.NewDecoder(r.Body).Decode(&project); err != nil { - response.BadRequest(w, "Invalid Input", err) + response.BadRequest(w, r, "Invalid Input", err) return } @@ -36,21 +36,21 @@ func CreateProject(app *config.Application) http.HandlerFunc { if err != nil { app.ErrorLog.Println("Project creation failed:", err) if err.Error() == "Project already exists" { - response.BadRequest(w, "Project already exists", errors.New("Project creation failed")) + response.BadRequest(w, r, "Project already exists", errors.New("Project creation failed")) } else if err.Error() == "database name must start with a letter or underscore and contain only letters, numbers, underscores, or $" { - response.BadRequest(w, "database name must start with a letter or underscore and contain only letters, numbers, underscores, or $", errors.New("Project creation failed")) + response.BadRequest(w, r, "database name must start with a letter or underscore and contain only letters, numbers, underscores, or $", errors.New("Project creation failed")) } else { - response.InternalServerError(w, "Internal Server Error", errors.New("Project creation failed")) + response.InternalServerError(w, r, "Internal Server Error", err) } return } if has { - response.BadRequest(w, "Project with this name already exists", nil) + response.BadRequest(w, r, "Project with this name already exists", nil) return } - response.Created(w, "Project Created Successfully", ProjectData) + response.Created(w, r, "Project Created Successfully", ProjectData) } } @@ -73,7 +73,7 @@ func DeleteProject(app *config.Application) http.HandlerFunc { urlVariables := mux.Vars(r) projectOid := urlVariables["project_id"] if projectOid == "" { - response.BadRequest(w, "Project Id is required", nil) + response.BadRequest(w, r, "Project Id is required", nil) return } @@ -84,15 +84,15 @@ func DeleteProject(app *config.Application) http.HandlerFunc { switch err.Error() { case "Project not found": - response.NotFound(w, "Project not found", err) + response.NotFound(w, r, "Project not found", err) case "Unauthorized": - response.UnAuthorized(w, "Unauthorized", err) + response.UnAuthorized(w, r, "Unauthorized", err) default: - response.InternalServerError(w, "Internal Server Error", errors.New("Project deletion failed")) + response.InternalServerError(w, r, "Internal Server Error", errors.New("Project deletion failed")) } return } - response.OK(w, "Project Deleted Successfully", nil) + response.OK(w, r, "Project Deleted Successfully", nil) } } @@ -113,11 +113,11 @@ func GetProjects(app *config.Application) http.HandlerFunc { data, err := getUserProjects(r.Context(), config.DB, userId) if err != nil { app.ErrorLog.Println(err) - response.InternalServerError(w, "Internal Server Error", nil) + response.InternalServerError(w, r, "Internal Server Error", nil) return } - response.OK(w, "Projects Retrieved Successfully", data) + response.OK(w, r, "Projects Retrieved Successfully", data) } } @@ -140,22 +140,22 @@ func getSpecificProject(app *config.Application) http.HandlerFunc { projectOid := urlVariables["project_id"] if projectOid == "" { - response.BadRequest(w, "Project Id is required", nil) + response.BadRequest(w, r, "Project Id is required", nil) return } data, err := GetUserSpecificProject(r.Context(), config.DB, userId, projectOid) if err != nil { if errors.Is(err, ErrorProjectNotFound) { - response.NotFound(w, "Project not found", nil) + response.NotFound(w, r, "Project not found", nil) return } app.ErrorLog.Println(err) - response.InternalServerError(w, "Internal Server Error", nil) + response.InternalServerError(w, r, "Internal Server Error", nil) return } - response.OK(w, "Project Retrieved Successfully", data) + response.OK(w, r, "Project Retrieved Successfully", data) } } @@ -179,20 +179,20 @@ func updateProject(app *config.Application) http.HandlerFunc { projectOid := urlVariables["project_id"] if projectOid == "" { - response.BadRequest(w, "Project Id is required", nil) + response.BadRequest(w, r, "Project Id is required", nil) return } var data updateProjectDataModel if err := json.NewDecoder(r.Body).Decode(&data); err != nil { - response.BadRequest(w, "Invalid Input", errors.New("The request body is empty")) + response.BadRequest(w, r, "Invalid Input", errors.New("The request body is empty")) return } defer r.Body.Close() fieldsToUpdate, Values, err := utils.GetNonZeroFieldsFromStruct(&data) if err != nil { - response.BadRequest(w, "Invalid Input", err) + response.BadRequest(w, r, "Invalid Input", err) return } @@ -202,7 +202,7 @@ func updateProject(app *config.Application) http.HandlerFunc { if field == "name" { err := validateProjectData(r.Context(), config.DB, Values[idx].(string), userId) if err != nil { - response.BadRequest(w, "Invalid Input Data", err) + response.BadRequest(w, r, "Invalid Input Data", err) return } } @@ -211,37 +211,37 @@ func updateProject(app *config.Application) http.HandlerFunc { query, err := BuildProjectUpdateQuery(projectOid, fieldsToUpdate) if err != nil { app.ErrorLog.Println(err) - response.InternalServerError(w, "Internal Server Error", errors.New("error in generating the updating query")) + response.InternalServerError(w, r, "Internal Server Error", errors.New("error in generating the updating query")) return } transaction, err := config.DB.Begin(r.Context()) if err != nil { app.ErrorLog.Println(err) - response.InternalServerError(w, "Internal Server Error", errors.New("Cannot begin transaction")) + response.InternalServerError(w, r, "Internal Server Error", errors.New("Cannot begin transaction")) return } err = updateProjectData(r.Context(), transaction, query, Values) if err != nil { app.ErrorLog.Println(err) - response.InternalServerError(w, "Internal Server Error", errors.New("Cannot update project data")) + response.InternalServerError(w, r, "Internal Server Error", errors.New("Cannot update project data")) return } projectData, err := GetUserSpecificProject(r.Context(), transaction, userId, projectOid) if err != nil { app.ErrorLog.Println(err) - response.InternalServerError(w, "Internal Server Error", nil) + response.InternalServerError(w, r, "Internal Server Error", nil) return } if err := transaction.Commit(r.Context()); err != nil { app.ErrorLog.Println(err) - response.InternalServerError(w, "Internal Server Error", errors.New("Cannot commit transaction")) + response.InternalServerError(w, r, "Internal Server Error", errors.New("Cannot commit transaction")) return } - response.OK(w, "Project Retrieved Successfully", projectData) + response.OK(w, r, "Project Retrieved Successfully", projectData) } } diff --git a/projects/repository.go b/projects/repository.go index 308c108..758a4e9 100644 --- a/projects/repository.go +++ b/projects/repository.go @@ -56,6 +56,16 @@ func getUserSpecificProjectFromDatabase(ctx context.Context, db utils.Querier, u return &project, nil } + +func GetProjectNameID(ctx context.Context, projectId string, db utils.Querier) (interface{}, interface{}, error) { + var name, id interface{} + err := db.QueryRow(ctx, "SELECT id, name FROM projects WHERE oid = $1", projectId).Scan(&id, &name) + if err != nil { + return nil, nil, err + } + return name, id, nil +} + func GetProjectID(ctx context.Context, db utils.Querier, userId int64, projectOid string) (int64, error) { var projectID int64 err := pgxscan.Get(ctx, db, &projectID, RetrieveProjectID, userId, projectOid) diff --git a/projects/routes.go b/projects/routes.go index ac1cfda..14562f3 100644 --- a/projects/routes.go +++ b/projects/routes.go @@ -9,13 +9,15 @@ import ( func DefineURLs() { router := config.Router.PathPrefix("/api/projects").Subrouter() router.Use(middleware.JwtAuthMiddleware) - + router.Handle("", middleware.Route(map[string]http.HandlerFunc{ http.MethodPost: CreateProject(config.App), http.MethodGet: GetProjects(config.App), })) - - router.Handle("/{project_id}", middleware.Route(map[string]http.HandlerFunc{ + + singleTablerouter := config.Router.PathPrefix("/api/projects/{project_id}").Subrouter() + singleTablerouter.Use(middleware.JwtAuthMiddleware, middleware.CheckOwnership) + singleTablerouter.Handle("", middleware.Route(map[string]http.HandlerFunc{ http.MethodGet: getSpecificProject(config.App), http.MethodPatch: updateProject(config.App), http.MethodDelete: DeleteProject(config.App), diff --git a/response/errors.go b/response/errors.go index ca200e8..7678c22 100644 --- a/response/errors.go +++ b/response/errors.go @@ -7,33 +7,34 @@ import ( var ( ErrUnauthorized = errors.New("Unauthorized") + ErrBadRequest = errors.New("BadRequest") ) // you can use one of these frequently used response for more code readability // -func BadRequest(w http.ResponseWriter, message string, err error) { - CreateResponse(w, http.StatusBadRequest, message, err, nil, nil) +func BadRequest(w http.ResponseWriter, r *http.Request, message string, err error) { + CreateResponse(w, r, http.StatusBadRequest, message, err, nil, nil) } -func NotFound(w http.ResponseWriter, message string, err error) { - CreateResponse(w, http.StatusNotFound, message, err, nil, nil) +func NotFound(w http.ResponseWriter, r *http.Request, message string, err error) { + CreateResponse(w, r, http.StatusNotFound, message, err, nil, nil) } -func InternalServerError(w http.ResponseWriter, message string, err error) { - CreateResponse(w, http.StatusInternalServerError, message, err, nil, nil) +func InternalServerError(w http.ResponseWriter, r *http.Request, message string, err error) { + CreateResponse(w, r, http.StatusInternalServerError, message, err, nil, nil) } -func UnAuthorized(w http.ResponseWriter, message string, err error) { - CreateResponse(w, http.StatusUnauthorized, message, err, nil, nil) +func UnAuthorized(w http.ResponseWriter, r *http.Request, message string, err error) { + CreateResponse(w, r, http.StatusUnauthorized, message, err, nil, nil) } -func MethodNotAllowed(w http.ResponseWriter, allowed string, message string, err error) { - CreateResponse(w, http.StatusMethodNotAllowed, message, err, nil, map[string]string{ +func MethodNotAllowed(w http.ResponseWriter, r *http.Request, allowed string, message string, err error) { + CreateResponse(w, r, http.StatusMethodNotAllowed, message, err, nil, map[string]string{ "Allow": allowed, }) } -func TooManyRequests(w http.ResponseWriter, message string, err error) { - CreateResponse(w, http.StatusTooManyRequests, message, err, nil, nil) +func TooManyRequests(w http.ResponseWriter, r *http.Request, message string, err error) { + CreateResponse(w, r, http.StatusTooManyRequests, message, err, nil, nil) } diff --git a/response/pagination.go b/response/pagination.go deleted file mode 100644 index f093728..0000000 --- a/response/pagination.go +++ /dev/null @@ -1,23 +0,0 @@ -package response - -//type PaginatedResponse struct { -// Status int `json:"status"` -// Message string `json:"message"` -// Data interface{} `json:"data,omitempty"` -// Page int `json:"page"` -// PageSize int `json:"pageSize"` -// Total int64 `json:"total"` -//} - -// we may need to change the pagination response in the future plans -//func PaginatedSuccessResponse(w http.ResponseWriter, status int, message string, data interface{}, page, pageSize int, total int64) { -// //response := PaginatedResponse{ -// // Status: status, -// // Message: message, -// // Data: data, -// // Page: page, -// // PageSize: pageSize, -// // Total: total, -// //} -// //sendResponse(w, status, response) -//} diff --git a/response/response.go b/response/response.go index b23e6a6..272b1df 100644 --- a/response/response.go +++ b/response/response.go @@ -1,8 +1,15 @@ package response import ( + "DBHS/config" "encoding/json" + "fmt" + "io" "net/http" + "time" + + "github.com/axiomhq/axiom-go/axiom" + "github.com/axiomhq/axiom-go/axiom/ingest" ) type Response struct { @@ -28,21 +35,57 @@ func SendResponse(w http.ResponseWriter, status int, headers map[string]string, json.NewEncoder(w).Encode(response) } -func CreateResponse(w http.ResponseWriter, status int, message string, err error, data interface{}, headers map[string]string) { +func CreateResponse(w http.ResponseWriter, r *http.Request, status int, message string, err error, data interface{}, headers map[string]string) { var response *Response + event := axiom.Event{ + ingest.TimestampField: time.Now(), + "user-id": r.Context().Value("user-id"), + "user-oid": r.Context().Value("user-oid"), + "user-name": r.Context().Value("user-name"), + "status-code": status, + "method": r.Method, + "URI": r.RequestURI, + "request-header": r.Header, + "request-body": r.Context().Value("body"), + } if err != nil { response = &Response{ Status: status, Error: err.Error(), } + // log the error to axiom + event["error"] = err.Error() + event["level"] = "error" } else { response = &Response{ Status: status, Data: data, } + event["response"] = response } if message != "" { response.Message = message } + event["massage"] = response.Message + config.AxiomLogger.IngestEvents(r.Context(), "api", []axiom.Event{event}) SendResponse(w, status, headers, response) } + +func JsonString(body io.ReadCloser) (string, error) { + defer body.Close() + + // Try to seek back to the beginning if possible + if seeker, ok := body.(io.ReadSeeker); ok { + _, err := seeker.Seek(0, io.SeekStart) + if err != nil { + return "", fmt.Errorf("failed to seek to beginning: %w", err) + } + } + + data, err := io.ReadAll(body) + if err != nil { + return "", err + } + + return string(data), nil +} \ No newline at end of file diff --git a/response/success.go b/response/success.go index aeeb9e1..e66975b 100644 --- a/response/success.go +++ b/response/success.go @@ -4,14 +4,14 @@ import "net/http" // you can use one of these frequently used response for more code readability -func OK(w http.ResponseWriter, message string, data interface{}) { - CreateResponse(w, http.StatusOK, message, nil, data, nil) +func OK(w http.ResponseWriter, r *http.Request, message string, data interface{}) { + CreateResponse(w, r, http.StatusOK, message, nil, data, nil) } -func Created(w http.ResponseWriter, message string, data interface{}) { - CreateResponse(w, http.StatusCreated, message, nil, data, nil) +func Created(w http.ResponseWriter, r *http.Request, message string, data interface{}) { + CreateResponse(w, r, http.StatusCreated, message, nil, data, nil) } -func Redirect(w http.ResponseWriter, message string, data interface{}) { - CreateResponse(w, http.StatusFound, message, nil, data, nil) // 302 +func Redirect(w http.ResponseWriter, r *http.Request, message string, data interface{}) { + CreateResponse(w, r, http.StatusFound, message, nil, data, nil) // 302 } diff --git a/schemas/handlers.go b/schemas/handlers.go index cd54fe2..630c07d 100644 --- a/schemas/handlers.go +++ b/schemas/handlers.go @@ -31,18 +31,18 @@ func GetDatabaseSchema(app *config.Application) http.HandlerFunc { projectOid := urlVariables["project-id"] if projectOid == "" { - response.BadRequest(w, "Project Id is required", nil) + response.BadRequest(w, r, "Project Id is required", nil) return } project, err := projects.GetUserSpecificProject(r.Context(), config.DB, userId, projectOid) if err != nil { if errors.Is(err, projects.ErrorProjectNotFound) { - response.BadRequest(w, "Project is not found", err) + response.BadRequest(w, r, "Project is not found", err) return } config.App.ErrorLog.Println(err) - response.InternalServerError(w, "Internal Server Error", nil) + response.InternalServerError(w, r, "Internal Server Error", nil) return } @@ -52,18 +52,18 @@ func GetDatabaseSchema(app *config.Application) http.HandlerFunc { databaseConn, err := config.ConfigManager.GetDbConnection(r.Context(), projectName) if err != nil { config.App.ErrorLog.Println(err) - response.InternalServerError(w, "Internal Server Error", nil) + response.InternalServerError(w, r, "Internal Server Error", nil) return } schema, err := getDatabaseSchema(r.Context(), databaseConn) if err != nil && !errors.Is(err, pgx.ErrNoRows) { config.App.ErrorLog.Println(err) - response.InternalServerError(w, "Internal Server Error", nil) + response.InternalServerError(w, r, "Internal Server Error", nil) return } - response.OK(w, "Schema Fetched successfully", schema) + response.OK(w, r, "Schema Fetched successfully", schema) } } @@ -87,18 +87,18 @@ func GetDatabaseTableSchema(app *config.Application) http.HandlerFunc { projectOid := urlVariables["project-id"] if projectOid == "" { - response.BadRequest(w, "Project Id is required", nil) + response.BadRequest(w, r, "Project Id is required", nil) return } project, err := projects.GetUserSpecificProject(r.Context(), config.DB, userId, projectOid) if err != nil { if errors.Is(err, projects.ErrorProjectNotFound) { - response.BadRequest(w, "Project is not found", err) + response.BadRequest(w, r, "Project is not found", err) return } config.App.ErrorLog.Println(err) - response.InternalServerError(w, "Internal Server Error", nil) + response.InternalServerError(w, r, "Internal Server Error", nil) return } @@ -108,7 +108,7 @@ func GetDatabaseTableSchema(app *config.Application) http.HandlerFunc { databaseConn, err := config.ConfigManager.GetDbConnection(r.Context(), projectName) if err != nil { config.App.ErrorLog.Println(err) - response.InternalServerError(w, "Internal Server Error", nil) + response.InternalServerError(w, r, "Internal Server Error", nil) return } @@ -116,17 +116,17 @@ func GetDatabaseTableSchema(app *config.Application) http.HandlerFunc { tableName, err := getDatabaseTableName(r.Context(), config.DB, tableOID) if err != nil { print(err.Error()) - response.BadRequest(w, "Invalid Table Id", err) + response.BadRequest(w, r, "Invalid Table Id", err) return } schema, err := GetTableSchema(r.Context(), databaseConn, tableName) if err != nil && !errors.Is(err, pgx.ErrNoRows) { config.App.ErrorLog.Println(err) - response.InternalServerError(w, "Internal Server Error", nil) + response.InternalServerError(w, r, "Internal Server Error", nil) return } - response.OK(w, "Schema Fetched successfully", schema) + response.OK(w, r, "Schema Fetched successfully", schema) } } diff --git a/schemas/repository.go b/schemas/repository.go index 14f313c..f0e8ee0 100644 --- a/schemas/repository.go +++ b/schemas/repository.go @@ -1,7 +1,6 @@ package schemas import ( - "DBHS/projects" "DBHS/utils" "context" "database/sql" @@ -10,15 +9,6 @@ import ( "github.com/georgysavva/scany/v2/pgxscan" ) -func getDatabaseByName(ctx context.Context, DB utils.Querier, name string) (projects.DatabaseConfig, error) { - var database projects.DatabaseConfig - err := pgxscan.Get(ctx, DB, &database, GetDatabaseByName, name) - if err != nil { - return projects.DatabaseConfig{}, err - } - return database, nil -} - func getDatabaseTableName(ctx context.Context, DB utils.Querier, tableOID string) (string, error) { var tableName string err := pgxscan.Get(ctx, DB, &tableName, GetTableNameByOID, tableOID) diff --git a/schemas/services.go b/schemas/services.go deleted file mode 100644 index faeaab6..0000000 --- a/schemas/services.go +++ /dev/null @@ -1 +0,0 @@ -package schemas diff --git a/tables/SqlQueries.go b/tables/SqlQueries.go index 1b106db..603f920 100644 --- a/tables/SqlQueries.go +++ b/tables/SqlQueries.go @@ -50,4 +50,7 @@ const ( AND c.table_schema = 'public' ORDER BY c.column_name;` + InsertNewRowStmt = ` + INSERT INTO "%s"(%s) VALUES(%s) + ` ) diff --git a/tables/handlers.go b/tables/handlers.go index cd992d1..fa6f794 100644 --- a/tables/handlers.go +++ b/tables/handlers.go @@ -3,32 +3,17 @@ package tables import ( "DBHS/config" "DBHS/response" + "DBHS/utils" "encoding/json" "errors" + "io" + "log" "net/http" "github.com/gorilla/mux" ) -/* - body - tableName: "", - cols: [ - { - name: "", - type: "", - isUnique: "", - isNullable: "", - isPrimaryKey: "", - foriegnKey: { - tableName: "", - columnName: "", - }, - } - ] - -*/ -// GetAllTablesHanlder godoc +// GetAllTablesHandler godoc // @Summary Get all tables in a project // @Description Get a list of all tables in the specified project // @Tags tables @@ -36,60 +21,114 @@ import ( // @Param project_id path string true "Project ID" // @Security BearerAuth // @Success 200 {object} response.SuccessResponse{data=[]Table} "List of tables" -// @Failure 400 {object} response.ErrorResponse "Project ID is required" -// @Failure 401 {object} response.ErrorResponse "Unauthorized" -// @Failure 500 {object} response.ErrorResponse "Internal server error" +// @Failure 404 {object} response.ErrorResponse404 "Project not found" +// @Failure 401 {object} response.ErrorResponse401 "Unauthorized" +// @Failure 500 {object} response.ErrorResponse500 "Internal server error" // @Router /api/projects/{project_id}/tables [get] -func GetAllTablesHanlder(app *config.Application) http.HandlerFunc { +func GetAllTablesHandler(app *config.Application) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { urlVariables := mux.Vars(r) projectId := urlVariables["project_id"] if projectId == "" { - response.BadRequest(w, "Project ID is required", nil) + response.NotFound(w, r, "Project ID is required", nil) return } + data, err := GetAllTables(r.Context(), projectId, config.DB) if err != nil { if errors.Is(err, response.ErrUnauthorized) { - response.UnAuthorized(w, "Unauthorized", nil) + response.UnAuthorized(w, r, "Unauthorized", nil) return } app.ErrorLog.Println("Tables reading failed:", err) - response.InternalServerError(w, "Failed to read tables", err) + response.InternalServerError(w, r, "Failed to read tables", err) + return + } + if data == nil { + data = []Table{} // Ensure data is an empty slice if no tables found + } + response.OK(w, r, "", data) + } +} + +// GetTableSchemaHandler godoc +// @Summary Get the schema of a table +// @Description Get the schema of the specified table in the project +// @Tags tables +// @Produce json +// @Param project_id path string true "Project ID" +// @Param table_id path string true "Table ID" +// @Security BearerAuth +// @Success 200 {object} response.SuccessResponse{data=Table} "Table schema" +// @Failure 400 {object} response.ErrorResponse400 "Bad request" +// @Failure 401 {object} response.ErrorResponse401 "Unauthorized" +// @Failure 404 {object} response.ErrorResponse404 "Project not found" +// @Failure 500 {object} response.ErrorResponse500 "Internal server error" +// @Router /api/projects/{project_id}/tables/{table_id}/schema [get] +func GetTableSchemaHandler(app *config.Application) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + urlVariables := mux.Vars(r) + projectId := urlVariables["project_id"] + tableId := urlVariables["table_id"] + if projectId == "" || tableId == "" { + response.BadRequest(w, r, "Project ID and Table ID are required", nil) return } - response.OK(w, "", data) + data, err := GetTableSchema(r.Context(), projectId, tableId, config.DB) + if err != nil { + if errors.Is(err, response.ErrUnauthorized) { + response.UnAuthorized(w, r, "Unauthorized", nil) + return + } + app.ErrorLog.Println("Could not read table schema:", err) + response.InternalServerError(w, r, "Could not read table schema", err) + return + } + + response.OK(w, r, "Table Schema Read Successfully", data) } } // CreateTableHandler godoc -// @Summary Create a new table -// @Description Create a new table in the specified project +// @Summary Create new table +// @Description Create new table in the specified project // @Tags tables // @Accept json // @Produce json // @Param project_id path string true "Project ID" -// @Param table body ClientTable true "Table information" +// @Param table body Table true "Table information" // @Security BearerAuth // @Success 201 {object} response.SuccessResponse -// @Failure 400 {object} response.ErrorResponse -// @Failure 401 {object} response.ErrorResponse -// @Failure 500 {object} response.ErrorResponse +// @Failure 400 {object} response.ErrorResponse400 "Bad request" +// @Failure 401 {object} response.ErrorResponse401 "Unauthorized" +// @Failure 404 {object} response.ErrorResponse404 "Project not found" +// @Failure 500 {object} response.ErrorResponse500 "Internal server error" // @Router /api/projects/{project_id}/tables [post] func CreateTableHandler(app *config.Application) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { // Handler logic for creating a table - table := ClientTable{} + table := Table{} + bodyData, err := io.ReadAll(r.Body) + if err != nil { + response.BadRequest(w, r, "Invalid request body", err) + return + } + + ctx := utils.AddToContext(r.Context(), map[string]interface{}{ + "body": string(bodyData), + }) + r = r.WithContext(ctx) + // Parse the request body to populate the table struct - if err := json.NewDecoder(r.Body).Decode(&table); err != nil { - response.BadRequest(w, "Invalid request body", err) + if err := json.Unmarshal(bodyData, &table); err != nil { + response.BadRequest(w, r, "Invalid request body", err) return } // Validate the table struct if !CheckForValidTable(&table) { - response.BadRequest(w, "Invalid table definition", nil) + response.BadRequest(w, r, "Invalid table definition", nil) return } @@ -97,48 +136,27 @@ func CreateTableHandler(app *config.Application) http.HandlerFunc { urlVariables := mux.Vars(r) projectId := urlVariables["project_id"] if projectId == "" { - response.BadRequest(w, "Project ID is required", nil) + response.BadRequest(w, r, "Project ID is required", nil) return } - // Call the service function to create the table tableOID, err := CreateTable(r.Context(), projectId, &table, config.DB) if err != nil { if errors.Is(err, response.ErrUnauthorized) { - response.UnAuthorized(w, "Unauthorized", nil) + response.UnAuthorized(w, r, "Unauthorized", nil) return } app.ErrorLog.Println("Table creation failed:", err) - response.InternalServerError(w, "Failed to create table", err) + response.InternalServerError(w, r, "Failed to create table", err) return } // Return a success response - response.Created(w, "Table created successfully", map[string]string{ + response.Created(w, r, "Table created successfully", map[string]string{ "oid": tableOID, }) } } -/* - "insert": { - "columns" : [ - - ] - }, - "update": [ - { - "oldName": "oldName", - "columns": [ - // Only include the changed parts - ] - } - ], - "delete": [ - "columnName1", - "columnName2" - ] -*/ - // UpdateTableHandler godoc // @Summary Update an existing table // @Description Update table structure by adding, modifying, or deleting columns @@ -147,44 +165,45 @@ func CreateTableHandler(app *config.Application) http.HandlerFunc { // @Produce json // @Param project_id path string true "Project ID" // @Param table_id path string true "Table ID" -// @Param updates body TableUpdate true "Table update information" +// @Param updates body UpdateTableSchema true "new table schema updates" // @Security BearerAuth // @Success 200 {object} response.SuccessResponse -// @Failure 400 {object} response.ErrorResponse -// @Failure 401 {object} response.ErrorResponse -// @Failure 500 {object} response.ErrorResponse +// @Failure 400 {object} response.ErrorResponse400 +// @Failure 401 {object} response.ErrorResponse401 +// @Failure 404 {object} response.ErrorResponse404 +// @Failure 500 {object} response.ErrorResponse500 // @Router /api/projects/{project_id}/tables/{table_id} [put] func UpdateTableHandler(app *config.Application) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { - updates := TableUpdate{} + updates := UpdateTableSchema{} // Parse the request body to populate the UpdateTable struct if err := json.NewDecoder(r.Body).Decode(&updates); err != nil { - response.BadRequest(w, "Invalid request body", err) + response.BadRequest(w, r, "Invalid request body", err) return } // Get the project ID and Table id from the URL urlVariables := mux.Vars(r) - projectId := urlVariables["project_id"] + projectOID := urlVariables["project_id"] tableId := urlVariables["table_id"] - if projectId == "" || tableId == "" { - response.BadRequest(w, "Project ID and Table ID are required", nil) + if projectOID == "" || tableId == "" { + response.BadRequest(w, r, "Project ID and Table ID are required", nil) return } // Call the service function to update the table - if err := UpdateTable(r.Context(), projectId, tableId, &updates, config.DB); err != nil { + if err := UpdateTable(r.Context(), projectOID, tableId, &updates, config.DB); err != nil { if errors.Is(err, response.ErrUnauthorized) { - response.UnAuthorized(w, "Unauthorized", nil) + response.UnAuthorized(w, r, "Unauthorized", nil) return } app.ErrorLog.Println("Table update failed:", err) - response.InternalServerError(w, "Failed to update table", err) + response.InternalServerError(w, r, "Failed to update table", err) return } // Return a success response - response.OK(w, "Table updated successfully", nil) + response.OK(w, r, "Table updated successfully", nil) } } @@ -197,31 +216,32 @@ func UpdateTableHandler(app *config.Application) http.HandlerFunc { // @Param table_id path string true "Table ID" // @Security BearerAuth // @Success 200 {object} response.SuccessResponse -// @Failure 400 {object} response.ErrorResponse -// @Failure 401 {object} response.ErrorResponse -// @Failure 500 {object} response.ErrorResponse +// @Failure 400 {object} response.ErrorResponse400 +// @Failure 401 {object} response.ErrorResponse401 +// @Failure 404 {object} response.ErrorResponse404 +// @Failure 500 {object} response.ErrorResponse500 // @Router /api/projects/{project_id}/tables/{table_id} [delete] func DeleteTableHandler(app *config.Application) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { urlVariables := mux.Vars(r) - projectId := urlVariables["project_id"] - tableId := urlVariables["table_id"] - if projectId == "" || tableId == "" { - response.BadRequest(w, "Project ID and Table ID are required", nil) + projectOID := urlVariables["project_id"] + tableOID := urlVariables["table_id"] + if projectOID == "" || tableOID == "" { + response.BadRequest(w, r, "Project ID and Table ID are required", nil) return } // Call the service function to delete the table - if err := DeletTable(r.Context(), projectId, tableId, config.DB); err != nil { + if err := DeleteTable(r.Context(), projectOID, tableOID, config.DB); err != nil { if errors.Is(err, response.ErrUnauthorized) { - response.UnAuthorized(w, "Unauthorized", nil) + response.UnAuthorized(w, r, "Unauthorized", nil) return } app.ErrorLog.Println("Table deletion failed:", err) - response.InternalServerError(w, "Failed to delete table", err) + response.InternalServerError(w, r, "Failed to delete table", err) return } // Return a success response - response.OK(w, "Table deleted successfully", nil) + response.OK(w, r, "Table deleted successfully", nil) } } @@ -234,14 +254,14 @@ func DeleteTableHandler(app *config.Application) http.HandlerFunc { // @Param table_id path string true "Table ID" // @Param page query int true "Page number" // @Param limit query int true "Number of records per page" -// @Param order_by query string false "Column to order by" -// @Param order query string false "Sort order (asc or desc)" -// @Param filter query string false "Filter condition (e.g. name=value)" +// @Param order query string false "Sort order example: ?order=id:asc&order=name:desc , this sort first by id then name" +// @Param filter query string false "Filter condition example: ?filter=id:gt:2&filter=name:like:ragnar, this gets records with ids greater than 2 and with name equal ragnar, valid operators [eq: =, neq: !=, lt: <, lte: <=, gt: >, gte: >=, like: LIKE]" // @Security BearerAuth // @Success 200 {object} response.SuccessResponse{data=Data} -// @Failure 400 {object} response.ErrorResponse -// @Failure 401 {object} response.ErrorResponse -// @Failure 500 {object} response.ErrorResponse +// @Failure 400 {object} response.ErrorResponse400 +// @Failure 401 {object} response.ErrorResponse401 +// @Failure 404 {object} response.ErrorResponse404 +// @Failure 500 {object} response.ErrorResponse500 // @Router /api/projects/{project_id}/tables/{table_id} [get] func ReadTableHandler(app *config.Application) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { @@ -250,13 +270,23 @@ func ReadTableHandler(app *config.Application) http.HandlerFunc { projectId := urlVariables["project_id"] tableId := urlVariables["table_id"] if projectId == "" || tableId == "" { - response.BadRequest(w, "Project ID and Table ID are required", nil) + response.BadRequest(w, r, "Project ID and Table ID are required", nil) return } // query parameters parameters := r.URL.Query() if parameters == nil || parameters["page"] == nil || parameters["limit"] == nil { - response.BadRequest(w, "Page and Limit are required", nil) + response.BadRequest(w, r, "Page and Limit are required", nil) + return + } + log.Println(parameters) + + if err := CheckForNonNegativeNumber(parameters["page"][0]); err != nil { + response.BadRequest(w, r, "enter a valid page number", nil) + return + } + if err := CheckForNonNegativeNumber(parameters["limit"][0]); err != nil { + response.BadRequest(w, r, "enter a valid limit number", nil) return } @@ -264,14 +294,75 @@ func ReadTableHandler(app *config.Application) http.HandlerFunc { data, err := ReadTable(r.Context(), projectId, tableId, parameters, config.DB) if err != nil { if errors.Is(err, response.ErrUnauthorized) { - response.UnAuthorized(w, "Unauthorized", nil) + response.UnAuthorized(w, r, "Unauthorized", nil) return } app.ErrorLog.Println("Could not read table:", err) - response.InternalServerError(w, "Could not read table", err) + response.InternalServerError(w, r, "Could not read table", err) + return + } + + response.OK(w, r, "Table Read Succesfully", data) + } +} + +// InsertRowHandler godoc +// @Summary insert new row +// @Description insert new row in the specified project table +// @Tags tables +// @Accept json +// @Produce json +// @Param project_id path string true "Project ID" +// @Param table_id path string true "Table ID" +// @Param row body RowValue true "Row information" +// @Security BearerAuth +// @Success 200 {object} response.SuccessResponse +// @Failure 400 {object} response.ErrorResponse400 +// @Failure 401 {object} response.ErrorResponse401 +// @Failure 404 {object} response.ErrorResponse404 +// @Failure 500 {object} response.ErrorResponse500 +// @Router /api/projects/{project_id}/tables/{table_id} [post] +func InsertRowHandler(app *config.Application) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + // url variables + urlVariables := mux.Vars(r) + projectOid := urlVariables["project_id"] + tableOid := urlVariables["table_id"] + if projectOid == "" || tableOid == "" { + response.BadRequest(w, r, "Project ID and Table ID are required", nil) + return + } + bodyData, err := io.ReadAll(r.Body) + if err != nil { + response.BadRequest(w, r, "Invalid request body", err) + return + } + + ctx := utils.AddToContext(r.Context(), map[string]interface{}{ + "body": string(bodyData), + }) + r = r.WithContext(ctx) + row := make(map[string]interface{}) + if err := json.Unmarshal(bodyData, &row); err != nil { + response.BadRequest(w, r, "bad request body", nil) + return + } + + if err := InserNewRow(r.Context(), projectOid, tableOid, row, config.DB); err != nil { + if err == response.ErrBadRequest { + response.BadRequest(w, r, "bad request body", nil) + return + } + + if err == response.ErrUnauthorized { + response.UnAuthorized(w, r, "Unauthorized", nil) + return + } + + response.InternalServerError(w, r, "Could not insert row", err) return } - response.OK(w, "Table Read Succesfully", data) + response.Created(w, r, "row created succefully", nil) } } diff --git a/tables/middleware.go b/tables/middleware.go new file mode 100644 index 0000000..bf2b754 --- /dev/null +++ b/tables/middleware.go @@ -0,0 +1,41 @@ +package tables + +import ( + "DBHS/config" + "DBHS/response" + "DBHS/utils" + "net/http" + + "github.com/gorilla/mux" +) + +func SyncTables(next http.Handler) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + // Sync table schemas between old and new + requestVars := mux.Vars(r) + projectOID := requestVars["project_id"] + if projectOID == "" { + response.NotFound(w, r, "Project ID is required", nil) + return + } + // Extract user ID from context + userId, ok := r.Context().Value("user-id").(int64) + if !ok || userId == 0 { + response.UnAuthorized(w, r, "Unauthorized", nil) + return + } + + // Get user database connection + projectId, userDb, err := utils.ExtractDb(r.Context(), projectOID, userId, config.DB) + if err != nil { + response.InternalServerError(w, r, "Failed to extract database connection", err) + return + } + + if err := SyncTableSchemas(r.Context(), projectId, config.DB, userDb); err != nil { + response.InternalServerError(w, r, err.Error(), err) + return + } + next.ServeHTTP(w, r) + }) +} \ No newline at end of file diff --git a/tables/models.go b/tables/models.go index bde1879..7e7240f 100644 --- a/tables/models.go +++ b/tables/models.go @@ -1,14 +1,26 @@ package tables +import ( + "DBHS/utils" +) + // Table struct is a row record of the tables table in the database type Table struct { - ID int `json:"id" db:"id"` - ProjectID int64 `json:"project_id" db:"project_id"` - OID string `json:"oid" db:"oid"` - Name string `json:"name" db:"name"` - Description string `json:"description" db:"description"` + ID int64 `json:"id" db:"id"` + ProjectID int64 `json:"project_id" db:"project_id"` + OID string `json:"oid" db:"oid"` + Name string `json:"name" db:"name" validate:"required"` + Description string `json:"description" db:"description"` + Schema *utils.Table `json:"schema" validate:"required"` +} + +type UpdateTableSchema struct { + Table + Renames []utils.RenameRelation `json:"renames"` } + + type ShortTable struct { OID string `json:"oid" db:"oid"` Name string `json:"name" db:"name"` @@ -82,3 +94,5 @@ type Data struct { Columns []ShowColumn `json:"columns"` Rows []map[string]interface{} `json:"rows"` } + +type RowValue []map[string]interface{} diff --git a/tables/repository.go b/tables/repository.go index 21e025c..783b3dc 100644 --- a/tables/repository.go +++ b/tables/repository.go @@ -5,32 +5,68 @@ import ( "DBHS/utils" "context" "fmt" + "log" + "slices" "strconv" "strings" "github.com/georgysavva/scany/v2/pgxscan" ) -func GetProjectNameID(ctx context.Context, projectId string, db utils.Querier) (interface{}, interface{}, error) { - var name, id interface{} - err := db.QueryRow(ctx, "SELECT id, name FROM projects WHERE oid = $1", projectId).Scan(&id, &name) +func GetAllTablesRepository(ctx context.Context, projectId int64, userDb utils.Querier, servDb utils.Querier) ([]Table, error) { + var tables []Table + err := pgxscan.Select(ctx, servDb, &tables, `SELECT id, oid, name FROM "Ptable" WHERE project_id = $1`, projectId) if err != nil { - return nil, nil, err + return nil, err } - return name, id, nil -} -func GetAllTablesNameOid(ctx context.Context, projectId int64, db pgxscan.Querier) ([]ShortTable, error) { - var tables []ShortTable - err := pgxscan.Select(ctx, db, &tables, `SELECT oid, name FROM "Ptable" WHERE project_id = $1`, projectId) + // extract the table schema + tableSchema, err := utils.GetTables(ctx, userDb) if err != nil { return nil, err } + presentTables := make(map[string]bool) + // delete the table recored if they are not present in the schema + for i := 0; i < len(tables); i++ { + presentTables[tables[i].Name] = true + if _, ok := tableSchema[tables[i].Name]; !ok { + // delete the table record from the database + if err := DeleteTableRecord(ctx, tables[i].ID, servDb); err != nil { + config.App.ErrorLog.Printf("Failed to delete table record %s: %v", tables[i].OID, err) + } + // remove the table from the list + tables = slices.Delete(tables, i, i+1) + i-- // adjust index after removal + } + } + + // insert new table entries if they are present in the schema but not in the database + for name, _ := range tableSchema { + if presentTables[name] { + continue // skip if the table is already present + } + // create a new table record + newTable := &Table{ + Name: name, + ProjectID: projectId, + OID: utils.GenerateOID(), + } + if err := InsertNewTable(ctx, newTable, &newTable.ID, servDb); err != nil { + config.App.ErrorLog.Printf("Failed to insert new table %s: %v", name, err) + } + tables = append(tables, *newTable) + } + + // convert the table schema to the table model + for i := range tables { + tables[i].Schema = tableSchema[tables[i].Name] + } + return tables, err } -func InsertNewTable(ctx context.Context, table *Table, TableId *int, db utils.Querier) error { +func InsertNewTable(ctx context.Context, table *Table, TableId *int64, db utils.Querier) error { err := db.QueryRow(ctx, InsertNewTableRecordStmt, table.OID, table.Name, table.Description, table.ProjectID).Scan(TableId) if err != nil { return fmt.Errorf("failed to insert new table: %w", err) @@ -38,7 +74,7 @@ func InsertNewTable(ctx context.Context, table *Table, TableId *int, db utils.Qu return nil } -func DeleteTableRecord(ctx context.Context, tableId int, db utils.Querier) error { +func DeleteTableRecord(ctx context.Context, tableId int64, db utils.Querier) error { _, err := db.Exec(ctx, fmt.Sprintf(DeleteTableStmt, "id"), tableId) if err != nil { return fmt.Errorf("failed to delete table record: %w", err) @@ -107,6 +143,7 @@ func ReadTableData(ctx context.Context, tableName string, parameters map[string] if err != nil { return nil, err } + log.Println(query) if err != nil { return nil, err @@ -122,6 +159,7 @@ func ReadTableData(ctx context.Context, tableName string, parameters map[string] if columns == nil { return nil, err } + data := Data{ Columns: make([]ShowColumn, len(columns)), } @@ -140,8 +178,8 @@ func ReadTableData(ctx context.Context, tableName string, parameters map[string] ptr[i] = &values[i] } - row := make(map[string]interface{}) for rows.Next() { + row := make(map[string]interface{}) if err := rows.Scan(ptr...); err != nil { return nil, err } @@ -155,7 +193,7 @@ func ReadTableData(ctx context.Context, tableName string, parameters map[string] } func PrepareQuery(tableName string, parameters map[string][]string) (string, error) { - query := fmt.Sprintf("SELECT * FROM %s", tableName) + query := fmt.Sprintf(`SELECT * FROM "%s"`, tableName) query, err := AddFilters(query, parameters["filter"]) if err != nil { return "", err @@ -176,6 +214,7 @@ func PrepareQuery(tableName string, parameters map[string][]string) (string, err return "", err } + page-- query = query + fmt.Sprintf(" LIMIT %d OFFSET %d;", limit, page*limit) return query, nil @@ -183,7 +222,7 @@ func PrepareQuery(tableName string, parameters map[string][]string) (string, err // filter will be a string in the format "column:op:value" func AddFilters(query string, filters []string) (string, error) { - if filters == nil || len(filters) == 0 { + if len(filters) == 0 { return query, nil } query = query + " WHERE " @@ -202,7 +241,7 @@ func AddFilters(query string, filters []string) (string, error) { parts := strings.Split(filter, ":") column, op, value := parts[0], parts[1], parts[2] if op == "like" { - predicates = append(predicates, fmt.Sprintf("%s %s %s", column, opMap[op], value)) + predicates = append(predicates, fmt.Sprintf("%s %s '%s'", column, opMap[op], value)) } else { intV, err := strconv.Atoi(value) if err != nil { @@ -237,3 +276,25 @@ func AddOrder(query string, orders []string) (string, error) { return query + strings.Join(predicates, ", "), nil } + +func InserRow(ctx context.Context, tableNmae string, data map[string]interface{}, db utils.Querier) error { + columns := make([]string, 0, len(data)) + placeholders := make([]string, 0, len(data)) + values := make([]interface{}, 0, len(data)) + i := 1 + for k, v := range data { + columns = append(columns, "\""+k+"\"") + placeholders = append(placeholders, fmt.Sprintf("$%d", i)) // Use ? if not PostgreSQL + values = append(values, v) + i++ + } + + query := fmt.Sprintf(InsertNewRowStmt, + tableNmae, + strings.Join(columns, ", "), + strings.Join(placeholders, ","), + ) + + _, err := db.Exec(ctx, query, values...) + return err +} \ No newline at end of file diff --git a/tables/routes.go b/tables/routes.go index 257f254..75f9596 100644 --- a/tables/routes.go +++ b/tables/routes.go @@ -8,6 +8,7 @@ import ( /* POST /api/projects/{project_id}/tables + GET /api/projects/{project_id}/tables/{table_id}/schema PUT /api/projects/{project_id}/tables/{table_id} DELETE /api/projects/{project_id}/tables/{table_id} GET /api/projects/{project_id}/tables/{table_id}? @@ -20,16 +21,22 @@ import ( func DefineURLs() { router := config.Router.PathPrefix("/api/projects/{project_id}/tables").Subrouter() - router.Use(middleware.JwtAuthMiddleware, middleware.CheckOwnership) + router.Use(middleware.JwtAuthMiddleware, middleware.CheckOwnership, SyncTables, middleware.CheckOTableExist) router.Handle("", middleware.Route(map[string]http.HandlerFunc{ http.MethodPost: CreateTableHandler(config.App), - http.MethodGet: GetAllTablesHanlder(config.App), + http.MethodGet: GetAllTablesHandler(config.App), })) + router.Handle("/{table_id}", middleware.Route(map[string]http.HandlerFunc{ http.MethodGet: ReadTableHandler(config.App), http.MethodPut: UpdateTableHandler(config.App), http.MethodDelete: DeleteTableHandler(config.App), + http.MethodPost: InsertRowHandler(config.App), + })) + + router.Handle("/{table_id}/schema", middleware.Route(map[string]http.HandlerFunc{ + http.MethodGet: GetTableSchemaHandler(config.App), })) } diff --git a/tables/service.go b/tables/service.go index 1cd180f..84f41a3 100644 --- a/tables/service.go +++ b/tables/service.go @@ -5,23 +5,22 @@ import ( "DBHS/response" "DBHS/utils" "context" - "errors" "github.com/jackc/pgx/v5/pgxpool" ) -func GetAllTables(ctx context.Context, projectOID string, servDb *pgxpool.Pool) ([]ShortTable, error) { - userId, ok := ctx.Value("user-id").(int) +func GetAllTables(ctx context.Context, projectOID string, servDb *pgxpool.Pool) ([]Table, error) { + userId, ok := ctx.Value("user-id").(int64) if !ok || userId == 0 { - return nil, errors.New("Unauthorized") + return nil, response.ErrUnauthorized } - _, projectId, err := GetProjectNameID(ctx, projectOID, servDb) + projectId, userDb, err := utils.ExtractDb(ctx, projectOID, userId, servDb) if err != nil { return nil, err } - tables, err := GetAllTablesNameOid(ctx, projectId.(int64), servDb) + tables, err := GetAllTablesRepository(ctx, projectId, userDb, servDb) if err != nil { return nil, err } @@ -29,13 +28,41 @@ func GetAllTables(ctx context.Context, projectOID string, servDb *pgxpool.Pool) return tables, nil } -func CreateTable(ctx context.Context, projectOID string, table *ClientTable, servDb *pgxpool.Pool) (string, error) { - userId, ok := ctx.Value("user-id").(int) +func GetTableSchema(ctx context.Context, projectOID string, tableOID string, servDb *pgxpool.Pool) (*Table, error) { + userId, ok := ctx.Value("user-id").(int64) if !ok || userId == 0 { - return "", errors.New("Unauthorized") + return nil, response.ErrUnauthorized + } + + _, userDb, err := utils.ExtractDb(ctx, projectOID, userId, servDb) + if err != nil { + return nil, err + } + + tableName, err := GetTableName(ctx, tableOID, servDb) + if err != nil { + return nil, err } - projectId, userDb, err := ExtractDb(ctx, projectOID, userId, servDb) + schema, err := utils.GetTable(ctx, tableName, userDb) + if err != nil { + return nil, err + } + + return &Table{ + Schema: schema, + OID: tableOID, + Name: schema.TableName, + }, nil +} + +func CreateTable(ctx context.Context, projectOID string, table *Table, servDb *pgxpool.Pool) (string, error) { + userId, ok := ctx.Value("user-id").(int64) + if !ok || userId == 0 { + return "", response.ErrUnauthorized + } + + projectId, userDb, err := utils.ExtractDb(ctx, projectOID, userId, servDb) if err != nil { return "", err } @@ -49,42 +76,40 @@ func CreateTable(ctx context.Context, projectOID string, table *ClientTable, ser if err := CreateTableIntoHostingServer(ctx, table, tx); err != nil { return "", err } - tableRecord := Table{ - Name: table.TableName, - ProjectID: projectId, - OID: utils.GenerateOID(), - } - var tableId int + + table.OID = utils.GenerateOID() + table.ProjectID = projectId + var tableId int64 // insert table row into the tables table - if err := InsertNewTable(ctx, &tableRecord, &tableId, servDb); err != nil { + if err := InsertNewTable(ctx, table, &tableId, servDb); err != nil { return "", err } + if err := tx.Commit(ctx); err != nil { DeleteTableRecord(ctx, tableId, servDb) return "", err } - config.App.InfoLog.Printf("Table %s created successfully in project %s by user %s", table.TableName, projectOID, ctx.Value("user-name").(string)) - return tableRecord.OID, nil + config.App.InfoLog.Printf("Table %s created successfully in project %s by user %s", table.Name, projectOID, ctx.Value("user-name").(string)) + return table.OID, nil } -func UpdateTable(ctx context.Context, projectOID string, tableOID string, updates *TableUpdate, servDb *pgxpool.Pool) error { - userId, ok := ctx.Value("user-id").(int) +func UpdateTable(ctx context.Context, projectOID string, tableOID string, newSchema *UpdateTableSchema, servDb *pgxpool.Pool) error { + userId, ok := ctx.Value("user-id").(int64) if !ok || userId == 0 { - return errors.New("Unauthorized") + return response.ErrUnauthorized } - _, userDb, err := ExtractDb(ctx, projectOID, userId, servDb) + _, userDb, err := utils.ExtractDb(ctx, projectOID, userId, servDb) if err != nil { return err } - tableName, err := GetTableName(ctx, tableOID, servDb) + oldSchema, err := utils.GetTableSchema(ctx, tableOID, servDb) if err != nil { return err } - // Call the service function to read the table - table, err := ReadTableColumns(ctx, tableName, userDb) + DDLUpdate, err := utils.CompareTableSchemas(oldSchema, newSchema.Schema, newSchema.Renames) if err != nil { return err } @@ -94,9 +119,19 @@ func UpdateTable(ctx context.Context, projectOID string, tableOID string, update return err } defer tx.Rollback(ctx) - if err := ExecuteUpdate(tableName, table, updates, tx); err != nil { + + if _, err := tx.Exec(ctx, DDLUpdate); err != nil { return err } + + // Update the table name in the service database + if oldSchema.TableName != newSchema.Schema.TableName { + if _, err := servDb.Exec(ctx, "UPDATE \"Ptable\" SET name = $1 WHERE oid = $2", + newSchema.Schema.TableName, tableOID); err != nil { + return err + } + } + if err := tx.Commit(ctx); err != nil { return err } @@ -104,13 +139,13 @@ func UpdateTable(ctx context.Context, projectOID string, tableOID string, update return nil } -func DeletTable(ctx context.Context, projectOID, tableOID string, servDb *pgxpool.Pool) error { - userId, ok := ctx.Value("user-id").(int) +func DeleteTable(ctx context.Context, projectOID, tableOID string, servDb *pgxpool.Pool) error { + userId, ok := ctx.Value("user-id").(int64) if !ok || userId == 0 { - return errors.New("Unauthorized") + return response.ErrUnauthorized } - _, userDb, err := ExtractDb(ctx, projectOID, userId, servDb) + _, userDb, err := utils.ExtractDb(ctx, projectOID, userId, servDb) if err != nil { return err } @@ -119,6 +154,7 @@ func DeletTable(ctx context.Context, projectOID, tableOID string, servDb *pgxpoo if err != nil { return err } + usertx, err := userDb.Begin(ctx) if err != nil { return err @@ -134,6 +170,7 @@ func DeletTable(ctx context.Context, projectOID, tableOID string, servDb *pgxpoo return err } defer servtx.Rollback(ctx) + if err := DeleteTableFromServerDb(ctx, tableOID, servtx); err != nil { return err } @@ -175,12 +212,12 @@ func DeletTable(ctx context.Context, projectOID, tableOID string, servDb *pgxpoo */ func ReadTable(ctx context.Context, projectOID, tableOID string, parameters map[string][]string, servDb *pgxpool.Pool) (*Data, error) { - userId, ok := ctx.Value("user-id").(int) + userId, ok := ctx.Value("user-id").(int64) if !ok || userId == 0 { return nil, response.ErrUnauthorized } - _, userDb, err := ExtractDb(ctx, projectOID, userId, servDb) + _, userDb, err := utils.ExtractDb(ctx, projectOID, userId, servDb) if err != nil { return nil, err } @@ -195,5 +232,43 @@ func ReadTable(ctx context.Context, projectOID, tableOID string, parameters map[ return nil, err } + if data.Rows == nil { + data.Rows = make([]map[string]interface{}, 0) + } + return data, nil } + +func InserNewRow(ctx context.Context, projectOID, tableOID string, data map[string]interface{}, servDb *pgxpool.Pool) (error) { + userId, ok := ctx.Value("user-id").(int64) + if !ok || userId == 0 { + return response.ErrUnauthorized + } + + _, userDb, err := utils.ExtractDb(ctx, projectOID, userId, servDb) + if err != nil { + return err + } + + tableName, err := GetTableName(ctx, tableOID, servDb) + if err != nil { + return err + } + + tableColumns, err := ReadTableColumns(ctx, tableName, userDb) + if err != nil { + return err + } + + for column := range tableColumns { + if _, ok := data[column]; !ok { + data[column] = nil + } + } + + if len(data) > len(tableColumns) { + return response.ErrBadRequest + } + + return InserRow(ctx, tableName, data, userDb) +} diff --git a/tables/utils.go b/tables/utils.go index bb6c90a..ef1c867 100644 --- a/tables/utils.go +++ b/tables/utils.go @@ -4,12 +4,15 @@ import ( "DBHS/config" "DBHS/utils" "context" + "errors" "fmt" "log" + "slices" + "strconv" "strings" + "github.com/georgysavva/scany/v2/pgxscan" "github.com/jackc/pgx/v5" - "github.com/jackc/pgx/v5/pgxpool" ) func CreateColumnDefinition(column *Column) string { @@ -38,8 +41,8 @@ func ParseTableIntoSQLCreate(table *ClientTable) (string, error) { return createTableSQL, nil } -func CreateTableIntoHostingServer(ctx context.Context, table *ClientTable, tx pgx.Tx) error { - DDLQuery, err := ParseTableIntoSQLCreate(table) +func CreateTableIntoHostingServer(ctx context.Context, table *Table, tx pgx.Tx) error { + DDLQuery, err := utils.GenerateCreateTableDDL(table.Schema) if err != nil { return err } @@ -51,33 +54,18 @@ func CreateTableIntoHostingServer(ctx context.Context, table *ClientTable, tx pg return nil } -func CheckForValidTable(table *ClientTable) bool { - if table.TableName == "" || len(table.Columns) == 0 { +func CheckForValidTable(table *Table) bool { + if table.Name == "" || len(table.Schema.Columns) == 0 { return false } - for _, column := range table.Columns { - if column.Name == "" || column.Type == "" { + for _, column := range table.Schema.Columns { + if column.ColumnName == "" || column.DataType == "" { return false } } return true } -func ExtractDb(ctx context.Context, projectOID string, UserID int, servDb *pgxpool.Pool) (int64, *pgxpool.Pool, error) { - // get the dbname to connect to - dbName, projectId, err := GetProjectNameID(ctx, projectOID, servDb) - if err != nil { - return 0, nil, err - } - // get the db connection - userDb, err := config.ConfigManager.GetDbConnection(ctx, utils.UserServerDbFormat(dbName.(string), UserID)) - if err != nil { - return 0, nil, err - } - - return projectId.(int64), userDb, nil -} - func ExecuteUpdate(tableName string, table map[string]DbColumn, updates *TableUpdate, db utils.Querier) error { // inserts insertStmt := "ALTER TABLE %s ADD COLUMN %s" @@ -149,3 +137,65 @@ func ExecuteUpdate(tableName string, table map[string]DbColumn, updates *TableUp func CreateUniqueConstraintName(tableName string, columnName string) string { return fmt.Sprintf("%s_%s_key", tableName, columnName) } + + +func SyncTableSchemas(ctx context.Context, projectId int64, servDb utils.Querier, userDb utils.Querier) error { + var tables []Table + err := pgxscan.Select(ctx, servDb, &tables, `SELECT id, oid, name FROM "Ptable" WHERE project_id = $1`, projectId) + if err != nil { + return err + } + + // extract the table schema + tableSchema, err := utils.GetTables(ctx, userDb) + if err != nil { + return err + } + + presentTables := make(map[string]bool) + // delete the table recored if they are not present in the schema + for i := 0; i < len(tables); i++ { + presentTables[tables[i].Name] = true + if _, ok := tableSchema[tables[i].Name]; !ok { + // delete the table record from the database + if err := DeleteTableRecord(ctx, tables[i].ID, servDb); err != nil { + config.App.ErrorLog.Printf("Failed to delete table record %s: %v", tables[i].OID, err) + } + // remove the table from the list + tables = slices.Delete(tables, i, i+1) + i-- // adjust index after removal + } + } + + // insert new table entries if they are present in the schema but not in the database + for name, _ := range tableSchema { + if presentTables[name] { + continue // skip if the table is already present + } + // create a new table record + newTable := &Table{ + Name: name, + ProjectID: projectId, + OID: utils.GenerateOID(), + } + if err := InsertNewTable(ctx, newTable, &newTable.ID, servDb); err != nil { + config.App.ErrorLog.Printf("Failed to insert new table %s: %v", name, err) + } + tables = append(tables, *newTable) + } + + return nil +} + +func CheckForNonNegativeNumber(s string) (error) { + num, err := strconv.Atoi(s); + if err != nil { + return err + } + + if num < 0 { + return errors.New("out of bound") + } + + return nil +} \ No newline at end of file diff --git a/test/tables/repository_test.go b/test/tables/repository_test.go index 421afa2..2817700 100644 --- a/test/tables/repository_test.go +++ b/test/tables/repository_test.go @@ -3,6 +3,7 @@ package tables_test import ( "DBHS/config" "DBHS/tables" + "DBHS/utils" "context" "log" "os" @@ -106,30 +107,12 @@ func (suite *RepositoryTestSuite) TearDownTest() { func (suite *RepositoryTestSuite) TestGetProjectNameID() { // Test getting project name and ID - name, id, err := tables.GetProjectNameID(context.Background(), existingProjectOID, suite.db) + name, id, err := utils.GetProjectNameID(context.Background(), existingProjectOID, suite.db) require.NoError(suite.T(), err) assert.Equal(suite.T(), "ragnardbtest", name) assert.NotNil(suite.T(), id) } -func (suite *RepositoryTestSuite) TestGetAllTablesNameOid() { - // Test getting all tables for a project - projectID := int64(129) // This should match the ID in the test data - tablesList, err := tables.GetAllTablesNameOid(context.Background(), projectID, suite.db) - - require.NoError(suite.T(), err) - assert.GreaterOrEqual(suite.T(), len(tablesList), 0) - - // Check if our test table is in the list - found := false - for _, table := range tablesList { - if table.OID == "test-table-1" && table.Name == "test_table_1" { - found = true - break - } - } - assert.True(suite.T(), found, "Test table not found in the list") -} func (suite *RepositoryTestSuite) TestGetTableName() { // Test getting table name by OID @@ -147,7 +130,7 @@ func (suite *RepositoryTestSuite) TestInsertAndDeleteTableRecord() { ProjectID: 129, } - var tableID int + var tableID int64 err := tables.InsertNewTable(context.Background(), table, &tableID, suite.db) require.NoError(suite.T(), err) assert.Greater(suite.T(), tableID, 0) diff --git a/test/tables/service_test.go b/test/tables/service_test.go index 8aef72e..babe784 100644 --- a/test/tables/service_test.go +++ b/test/tables/service_test.go @@ -51,7 +51,7 @@ func mockCreateTable(ctx context.Context, projectOID string, table *tables.Clien OID: tableOID, } - var tableId int + var tableId int64 if err := tables.InsertNewTable(ctx, &tableRecord, &tableId, db); err != nil { return "", err } diff --git a/tmp/air_errors.log b/tmp/air_errors.log index 2f95017..e69de29 100644 --- a/tmp/air_errors.log +++ b/tmp/air_errors.log @@ -1 +0,0 @@ -exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1 \ No newline at end of file diff --git a/utils/database.go b/utils/database.go index 11c03f8..c5c55bc 100644 --- a/utils/database.go +++ b/utils/database.go @@ -1,12 +1,18 @@ package utils import ( + "DBHS/config" "context" "fmt" + + "github.com/jackc/pgx/v5/pgxpool" ) var ( CheckOwnershipStmt = `SELECT COUNT(*) FROM "projects" WHERE oid = $1 AND owner_id = $2;` + CheckProjectExistStmt = `SELECT COUNT(*) FROM "projects" WHERE oid = $1;` + CheckOwnershipTableStmt = `SELECT COUNT(*) FROM "Ptable" WHERE oid = $1 AND project_id = $2;` + CheckTableExistStmt = `SELECT COUNT(*) FROM "Ptable" WHERE oid = $1;` ) func UpdateDataInDatabase(ctx context.Context, db Querier, query string, dest ...interface{}) error { @@ -14,11 +20,63 @@ func UpdateDataInDatabase(ctx context.Context, db Querier, query string, dest .. return err } -func CheckOwnershipQuery(ctx context.Context, projectId string, userId int, db Querier) (bool, error) { +func CheckOwnershipQuery(ctx context.Context, projectOID string, userId int64, db Querier) (bool, error) { + + var count int + err := db.QueryRow(ctx, CheckOwnershipStmt, projectOID, userId).Scan(&count) + if err != nil { + return false, fmt.Errorf("failed to check ownership: %w", err) + } + return count > 0, nil +} + +func CheckProjectExist(ctx context.Context, projectOID string, db Querier) (bool, error) { + var count int + err := db.QueryRow(ctx, CheckProjectExistStmt, projectOID).Scan(&count) + if err != nil { + return false, fmt.Errorf("failed to check project existence: %w", err) + } + return count > 0, nil +} + +func CheckOwnershipQueryTable(ctx context.Context, tableOID string, projectID int64, db Querier) (bool, error) { var count int - err := db.QueryRow(ctx, CheckOwnershipStmt, projectId, userId).Scan(&count) + err := db.QueryRow(ctx, CheckOwnershipTableStmt, tableOID, projectID).Scan(&count) if err != nil { return false, fmt.Errorf("failed to check ownership: %w", err) } return count > 0, nil } + +func CheckTableExist(ctx context.Context, tableOID string, db Querier) (bool, error) { + var count int + err := db.QueryRow(ctx, CheckTableExistStmt, tableOID).Scan(&count) + if err != nil { + return false, fmt.Errorf("failed to check table existence: %w", err) + } + return count > 0, nil +} + +func GetProjectNameID(ctx context.Context, projectOID string, db Querier) (interface{}, interface{}, error) { + var name, id interface{} + err := db.QueryRow(ctx, "SELECT id, name FROM projects WHERE oid = $1", projectOID).Scan(&id, &name) + if err != nil { + return nil, nil, err + } + return name, id, nil +} + +func ExtractDb(ctx context.Context, projectOID string, UserID int64, servDb *pgxpool.Pool) (int64, *pgxpool.Pool, error) { + // get the dbname to connect to + dbName, projectId, err := GetProjectNameID(ctx, projectOID, servDb) + if err != nil { + return 0, nil, err + } + // get the db connection + userDb, err := config.ConfigManager.GetDbConnection(ctx, UserServerDbFormat(dbName.(string), UserID)) + if err != nil { + return 0, nil, err + } + + return projectId.(int64), userDb, nil +} \ No newline at end of file diff --git a/utils/schema.go b/utils/schema.go new file mode 100644 index 0000000..fd68ed7 --- /dev/null +++ b/utils/schema.go @@ -0,0 +1,853 @@ +package utils + +import ( + "context" + "fmt" + "strings" + + "github.com/georgysavva/scany/v2/pgxscan" +) + +// TableColumn represents a database column with its properties +type TableColumn struct { + TableName string `db:"table_name" json:"TableName"` + ColumnName string `db:"column_name" json:"ColumnName"` + DataType string `db:"data_type" json:"DataType"` + IsNullable bool `db:"is_nullable" json:"IsNullable"` + ColumnDefault *string `db:"column_default" json:"ColumnDefault"` + CharacterMaximumLength *int `db:"character_maximum_length" json:"CharacterMaximumLength"` + NumericPrecision *int `db:"numeric_precision" json:"NumericPrecision"` + NumericScale *int `db:"numeric_scale" json:"NumericScale"` + OrdinalPosition int `db:"ordinal_position" json:"OrdinalPosition"` +} + +// ConstraintInfo represents database constraints +type ConstraintInfo struct { + TableName string `db:"table_name" json:"TableName"` + ConstraintName string `db:"constraint_name" json:"ConstraintName"` + ConstraintType string `db:"constraint_type" json:"ConstraintType"` + ColumnName *string `db:"column_name" json:"ColumnName"` + ForeignTableName *string `db:"foreign_table_name" json:"ForeignTableName"` + ForeignColumnName *string `db:"foreign_column_name" json:"ForeignColumnName"` + CheckClause *string `db:"check_clause" json:"CheckClause"` + OrdinalPosition *int `db:"ordinal_position" json:"OrdinalPosition"` +} + +// IndexInfo represents database indexes +type IndexInfo struct { + TableName string `db:"table_name" json:"TableName"` + IndexName string `db:"index_name" json:"IndexName"` + ColumnName string `db:"column_name" json:"ColumnName"` + IsUnique bool `db:"is_unique" json:"IsUnique"` + IndexType string `db:"index_type" json:"IndexType"` + IsPrimary bool `db:"is_primary" json:"IsPrimary"` +} + +type Table struct { + TableName string `db:"table_name" json:"TableName"` + Columns []TableColumn `db:"columns" json:"Columns"` + Constraints []ConstraintInfo `db:"constraints" json:"Constraints"` + Indexes []IndexInfo `db:"indexes" json:"Indexes"` +} + +type RenameRelation struct { + OldName string `json:"oldName"` + NewName string `json:"newName"` +} + +const ( + // Query to get all tables and their columns with detailed information + getTablesAndColumnsQuery = ` + SELECT + t.table_name AS table_name, + c.column_name AS column_name, + c.data_type AS data_type, + c.is_nullable = 'YES' AS is_nullable, + c.column_default AS column_default, + c.character_maximum_length AS character_maximum_length, + c.numeric_precision AS numeric_precision, + c.numeric_scale AS numeric_scale, + c.ordinal_position AS ordinal_position + FROM + information_schema.tables t + JOIN + information_schema.columns c ON t.table_name = c.table_name + AND t.table_schema = c.table_schema + WHERE + t.table_schema = 'public' + AND t.table_type = 'BASE TABLE' + ORDER BY + t.table_name, c.ordinal_position;` + + // Query to get all constraints (PRIMARY KEY, FOREIGN KEY, UNIQUE, CHECK) + getConstraintsQuery = ` + SELECT + tc.table_name AS table_name, + tc.constraint_name AS constraint_name, + tc.constraint_type AS constraint_type, + kcu.column_name AS column_name, + ccu.table_name AS foreign_table_name, + ccu.column_name AS foreign_column_name, + cc.check_clause AS check_clause, + kcu.ordinal_position AS ordinal_position + FROM + information_schema.table_constraints tc + LEFT JOIN + information_schema.key_column_usage kcu + ON tc.constraint_name = kcu.constraint_name + AND tc.table_schema = kcu.table_schema + LEFT JOIN + information_schema.constraint_column_usage ccu + ON tc.constraint_name = ccu.constraint_name + AND tc.table_schema = ccu.table_schema + LEFT JOIN + information_schema.check_constraints cc + ON tc.constraint_name = cc.constraint_name + AND tc.table_schema = cc.constraint_schema + WHERE + tc.table_schema = 'public' + AND tc.constraint_type IN ('PRIMARY KEY', 'FOREIGN KEY', 'UNIQUE', 'CHECK') + ORDER BY + tc.table_name, tc.constraint_type, kcu.ordinal_position;` + + // Query to get all indexes (excluding those created by constraints) + getIndexesQuery = ` + SELECT + t.relname AS table_name, + i.relname AS index_name, + a.attname AS column_name, + ix.indisunique AS is_unique, + am.amname AS index_type, + ix.indisprimary AS is_primary + FROM + pg_class t, + pg_class i, + pg_index ix, + pg_attribute a, + pg_am am + WHERE + t.oid = ix.indrelid + AND i.oid = ix.indexrelid + AND a.attrelid = t.oid + AND a.attnum = ANY(ix.indkey) + AND t.relkind = 'r' + AND am.oid = i.relam + AND t.relnamespace = (SELECT oid FROM pg_namespace WHERE nspname = 'public') + AND NOT ix.indisprimary -- Exclude primary key indexes (handled by constraints) + AND NOT EXISTS ( + SELECT 1 FROM information_schema.table_constraints tc + WHERE tc.table_name = t.relname + AND tc.constraint_type IN ('UNIQUE', 'FOREIGN KEY') + AND tc.table_schema = 'public' + ) + ORDER BY + t.relname, i.relname;` + + // Query to get the table schema by name + getTableSchemaQuery = ` + SELECT + t.table_name AS table_name, + c.column_name AS column_name, + c.data_type AS data_type, + c.is_nullable = 'YES' AS is_nullable, + c.column_default AS column_default, + c.character_maximum_length AS character_maximum_length, + c.numeric_precision AS numeric_precision, + c.numeric_scale AS numeric_scale, + c.ordinal_position AS ordinal_position + FROM + information_schema.tables t + JOIN + information_schema.columns c ON t.table_name = c.table_name + AND t.table_schema = c.table_schema + WHERE + t.table_schema = 'public' + AND t.table_type = 'BASE TABLE' + AND t.table_name = $1 + ORDER BY + t.table_name, c.ordinal_position;` + + // Query to get constraints for a specific table + getTableConstraintsQuery = ` + SELECT + tc.table_name AS table_name, + tc.constraint_name AS constraint_name, + tc.constraint_type AS constraint_type, + kcu.column_name AS column_name, + ccu.table_name AS foreign_table_name, + ccu.column_name AS foreign_column_name, + cc.check_clause AS check_clause, + kcu.ordinal_position AS ordinal_position + FROM + information_schema.table_constraints tc + LEFT JOIN + information_schema.key_column_usage kcu + ON tc.constraint_name = kcu.constraint_name + AND tc.table_schema = kcu.table_schema + LEFT JOIN + information_schema.constraint_column_usage ccu + ON tc.constraint_name = ccu.constraint_name + AND tc.table_schema = ccu.table_schema + LEFT JOIN + information_schema.check_constraints cc + ON tc.constraint_name = cc.constraint_name + AND tc.table_schema = cc.constraint_schema + WHERE + tc.table_schema = 'public' + AND tc.table_name = $1 + AND tc.constraint_type IN ('PRIMARY KEY', 'FOREIGN KEY', 'UNIQUE', 'CHECK') + ORDER BY + tc.table_name, tc.constraint_type, kcu.ordinal_position;` + + // Query to get indexes for a specific table + getTableIndexesQuery = ` + SELECT + t.relname AS table_name, + i.relname AS index_name, + a.attname AS column_name, + ix.indisunique AS is_unique, + am.amname AS index_type, + ix.indisprimary AS is_primary + FROM + pg_class t, + pg_class i, + pg_index ix, + pg_attribute a, + pg_am am + WHERE + t.oid = ix.indrelid + AND i.oid = ix.indexrelid + AND a.attrelid = t.oid + AND a.attnum = ANY(ix.indkey) + AND t.relkind = 'r' + AND am.oid = i.relam + AND t.relnamespace = (SELECT oid FROM pg_namespace WHERE nspname = 'public') + AND t.relname = $1 + ORDER BY + t.relname, i.relname;` +) + +/* +the schema format for the tables is: + + table_name: { + columns: [ + { + column_name: "", + data_type: "", + is_nullable: "", + column_default: "", + character_maximum_length: "", + numeric_precision: "", + numeric_scale: "", + ordinal_position: "", + } + ], + constraints: [ + { + constraint_name: "", + constraint_type: "", + } + ], + indexes: [ + { + index_name: "", + index_type: "", + } + ] + } +*/ +func GetTables(ctx context.Context, db Querier) (map[string]*Table, error) { + // Get all tables and columns + columnsRows, err := db.Query(ctx, getTablesAndColumnsQuery) + if err != nil { + return nil, fmt.Errorf("failed to query table columns: %w", err) + } + defer columnsRows.Close() + + var columns []TableColumn + err = pgxscan.ScanAll(&columns, columnsRows) + if err != nil { + return nil, fmt.Errorf("failed to scan table columns: %w", err) + } + + tableColumns := make(map[string][]TableColumn) + for _, col := range columns { + tableColumns[col.TableName] = append(tableColumns[col.TableName], col) + } + + // Get all constraints + constraints, err := GetConstraints(ctx, db) + if err != nil { + return nil, fmt.Errorf("failed to get constraints: %w", err) + } + + tableConstraints := make(map[string][]ConstraintInfo) + for _, constraint := range constraints { + tableConstraints[constraint.TableName] = append(tableConstraints[constraint.TableName], constraint) + } + + // Get all indexes + indexes, err := GetIndexes(ctx, db) + if err != nil { + return nil, fmt.Errorf("failed to get indexes: %w", err) + } + + tableIndexes := make(map[string][]IndexInfo) + for _, index := range indexes { + tableIndexes[index.TableName] = append(tableIndexes[index.TableName], index) + } + + tablesMap := make(map[string]*Table) + for tableName, columns := range tableColumns { + tablesMap[tableName] = &Table{ + TableName: tableName, + Columns: columns, + Constraints: tableConstraints[tableName], + Indexes: tableIndexes[tableName], + } + } + + return tablesMap, nil +} + +func GetTableSchema(ctx context.Context, tableName string, db Querier) (*Table, error) { + // Get the table schema + schemaRows, err := db.Query(ctx, getTableSchemaQuery, tableName) + if err != nil { + return nil, fmt.Errorf("failed to query table schema: %w", err) + } + defer schemaRows.Close() + + var columns []TableColumn + err = pgxscan.ScanAll(&columns, schemaRows) + if err != nil { + return nil, fmt.Errorf("failed to scan table schema: %w", err) + } + + if len(columns) == 0 { + return nil, fmt.Errorf("table %s does not exist", tableName) + } + + return &Table{ + TableName: tableName, + Columns: columns, + }, nil +} + +func GetTableConstraints(ctx context.Context, tableName string, db Querier) ([]ConstraintInfo, error) { + // Get the table constraints + constraintsRows, err := db.Query(ctx, getTableConstraintsQuery, tableName) + if err != nil { + return nil, fmt.Errorf("failed to query table constraints: %w", err) + } + defer constraintsRows.Close() + + var constraints []ConstraintInfo + err = pgxscan.ScanAll(&constraints, constraintsRows) + if err != nil { + return nil, fmt.Errorf("failed to scan table constraints: %w", err) + } + + for i := range len(constraints) { + if constraints[i].ConstraintType == "CHECK" { + constraints[i].ColumnName = &strings.Split(*constraints[i].CheckClause, " ")[0] + if strings.Contains(*constraints[i].CheckClause, "IS NOT NULL") { + constraints[i].ConstraintType = "NOT NULL" + } + } + } + + return constraints, nil +} + +func GetTableIndexes(ctx context.Context, tableName string, db Querier) ([]IndexInfo, error) { + // Get the table indexes + indexesRows, err := db.Query(ctx, getTableIndexesQuery, tableName) + if err != nil { + return nil, fmt.Errorf("failed to query table indexes: %w", err) + } + defer indexesRows.Close() + + var indexes []IndexInfo + err = pgxscan.ScanAll(&indexes, indexesRows) + if err != nil { + return nil, fmt.Errorf("failed to scan table indexes: %w", err) + } + + return indexes, nil +} + +func GetTable(ctx context.Context, tableName string, db Querier) (*Table, error) { + // Get the table schema + tableSchema, err := GetTableSchema(ctx, tableName, db) + if err != nil { + return nil, err + } + + // Get the table constraints + constraints, err := GetTableConstraints(ctx, tableName, db) + if err != nil { + return nil, err + } + + // Get the table indexes + indexes, err := GetTableIndexes(ctx, tableName, db) + if err != nil { + return nil, err + } + + return &Table{ + TableName: tableName, + Columns: tableSchema.Columns, + Constraints: constraints, + Indexes: indexes, + }, nil +} + +func GetConstraints(ctx context.Context, db Querier) ([]ConstraintInfo, error) { + // Get all constraints + constraintsRows, err := db.Query(ctx, getConstraintsQuery) + if err != nil { + return nil, fmt.Errorf("failed to query constraints: %w", err) + } + defer constraintsRows.Close() + + var constraints []ConstraintInfo + err = pgxscan.ScanAll(&constraints, constraintsRows) + if err != nil { + return nil, fmt.Errorf("failed to scan constraints: %w", err) + } + + return constraints, nil +} + +func GetIndexes(ctx context.Context, db Querier) ([]IndexInfo, error) { + // Get all indexes + indexesRows, err := db.Query(ctx, getIndexesQuery) + if err != nil { + return nil, fmt.Errorf("failed to query indexes: %w", err) + } + defer indexesRows.Close() + + var indexes []IndexInfo + err = pgxscan.ScanAll(&indexes, indexesRows) + if err != nil { + return nil, fmt.Errorf("failed to scan indexes: %w", err) + } + + return indexes, nil +} + +// ExtractDatabaseSchema extracts the complete database schema as DDL statements +func ExtractDatabaseSchema(ctx context.Context, db Querier) (string, error) { + var ddlStatements strings.Builder + ddlStatements.WriteString("-- Database Schema DDL Export\n") + ddlStatements.WriteString("-- Generated automatically\n\n") + + // Get all tables and columns + columnsRows, err := db.Query(ctx, getTablesAndColumnsQuery) + if err != nil { + return "", fmt.Errorf("failed to query table columns: %w", err) + } + defer columnsRows.Close() + + var columns []TableColumn + err = pgxscan.ScanAll(&columns, columnsRows) + if err != nil { + return "", fmt.Errorf("failed to scan table columns: %w", err) + } + + // Get all constraints + constraintsRows, err := db.Query(ctx, getConstraintsQuery) + if err != nil { + return "", fmt.Errorf("failed to query constraints: %w", err) + } + defer constraintsRows.Close() + + var constraints []ConstraintInfo + err = pgxscan.ScanAll(&constraints, constraintsRows) + if err != nil { + return "", fmt.Errorf("failed to scan constraints: %w", err) + } + + // Get all indexes + indexesRows, err := db.Query(ctx, getIndexesQuery) + if err != nil { + return "", fmt.Errorf("failed to query indexes: %w", err) + } + defer indexesRows.Close() + + var indexes []IndexInfo + err = pgxscan.ScanAll(&indexes, indexesRows) + if err != nil { + return "", fmt.Errorf("failed to scan indexes: %w", err) + } + + // Group data by table + tableColumns := make(map[string][]TableColumn) + tableConstraints := make(map[string][]ConstraintInfo) + tableIndexes := make(map[string][]IndexInfo) + + for _, col := range columns { + tableColumns[col.TableName] = append(tableColumns[col.TableName], col) + } + + for _, constraint := range constraints { + tableConstraints[constraint.TableName] = append(tableConstraints[constraint.TableName], constraint) + } + + for _, index := range indexes { + tableIndexes[index.TableName] = append(tableIndexes[index.TableName], index) + } + + // Generate CREATE TABLE statements + for tableName, cols := range tableColumns { + ddlStatements.WriteString(generateCreateTableStatement(tableName, cols, tableConstraints[tableName])) + ddlStatements.WriteString("\n") + + // Add indexes for this table + if idxs, exists := tableIndexes[tableName]; exists { + for _, index := range idxs { + ddlStatements.WriteString(generateCreateIndexStatement(&index)) + ddlStatements.WriteString("\n") + } + } + ddlStatements.WriteString("\n") + } + + return ddlStatements.String(), nil +} + +func GenerateCreateTableDDL(table *Table) (string, error) { + var ddlStatements strings.Builder + ddlStatements.WriteString("-- Database Schema DDL Export\n") + ddlStatements.WriteString("-- Generated automatically\n\n") + ddlStatements.WriteString(generateCreateTableStatement(table.TableName, table.Columns, table.Constraints)) + ddlStatements.WriteString("\n") + // Add indexes for this table + ddlStatements.WriteString("\n-- Indexes\n") + for _, index := range table.Indexes { + ddlStatements.WriteString(generateCreateIndexStatement(&index)) + ddlStatements.WriteString("\n") + } + return ddlStatements.String(), nil +} + +// generateCreateTableStatement creates a CREATE TABLE DDL statement +func generateCreateTableStatement(tableName string, columns []TableColumn, constraints []ConstraintInfo) string { + var stmt strings.Builder + stmt.WriteString(fmt.Sprintf("CREATE TABLE \"%s\" (\n", tableName)) + + // Add columns + columnDefs := make([]string, 0, len(columns)) + for _, col := range columns { + columnDef := fmt.Sprintf(" \"%s\" %s", col.ColumnName, formatDataType(col)) + + if !col.IsNullable { + columnDef += " NOT NULL" + } + + if col.ColumnDefault != nil { + columnDef += fmt.Sprintf(" DEFAULT %s", *col.ColumnDefault) + } + + columnDefs = append(columnDefs, columnDef) + } + + // Group constraints by type + primaryKeys := make([]string, 0) + uniqueConstraints := make(map[string][]string) + foreignKeys := make([]ConstraintInfo, 0) + checkConstraints := make([]ConstraintInfo, 0) + + for _, constraint := range constraints { + switch constraint.ConstraintType { + case "PRIMARY KEY": + if constraint.ColumnName != nil { + primaryKeys = append(primaryKeys, *constraint.ColumnName) + } + case "UNIQUE": + if constraint.ColumnName != nil { + uniqueConstraints[constraint.ConstraintName] = append(uniqueConstraints[constraint.ConstraintName], *constraint.ColumnName) + } + case "FOREIGN KEY": + foreignKeys = append(foreignKeys, constraint) + case "CHECK": + checkConstraints = append(checkConstraints, constraint) + } + } + + // Add PRIMARY KEY constraint + if len(primaryKeys) > 0 { + columnDefs = append(columnDefs, fmt.Sprintf(" PRIMARY KEY (\"%s\")", strings.Join(primaryKeys, "\", \""))) + } + + // Add UNIQUE constraints + for constraintName, cols := range uniqueConstraints { + columnDefs = append(columnDefs, fmt.Sprintf(" CONSTRAINT \"%s\" UNIQUE (\"%s\")", constraintName, strings.Join(cols, "\", \""))) + } + + // Add FOREIGN KEY constraints + for _, fk := range foreignKeys { + if fk.ColumnName != nil && fk.ForeignTableName != nil && fk.ForeignColumnName != nil { + columnDefs = append(columnDefs, fmt.Sprintf(" CONSTRAINT \"%s\" FOREIGN KEY (\"%s\") REFERENCES \"%s\" (\"%s\")", + fk.ConstraintName, *fk.ColumnName, *fk.ForeignTableName, *fk.ForeignColumnName)) + } + } + + // Add CHECK constraints + for _, check := range checkConstraints { + if check.CheckClause != nil { + columnDefs = append(columnDefs, fmt.Sprintf(" CONSTRAINT \"%s\" CHECK (%s)", check.ConstraintName, *check.CheckClause)) + } + } + + stmt.WriteString(strings.Join(columnDefs, ",\n")) + stmt.WriteString("\n);") + + return stmt.String() +} + +// generateCreateIndexStatement creates a CREATE INDEX DDL statement +func generateCreateIndexStatement(index *IndexInfo) string { + indexType := "" + if index.IsUnique { + indexType = "UNIQUE " + } + + return fmt.Sprintf("CREATE %sINDEX \"%s\" ON \"%s\" USING %s (\"%s\");", + indexType, index.IndexName, index.TableName, index.IndexType, index.ColumnName) +} + +// formatDataType formats the PostgreSQL data type with precision/scale if applicable +func formatDataType(col TableColumn) string { + dataType := strings.ToUpper(col.DataType) + + switch dataType { + case "CHARACTER VARYING", "VARCHAR": + if col.CharacterMaximumLength != nil { + return fmt.Sprintf("VARCHAR(%d)", *col.CharacterMaximumLength) + } + return "VARCHAR" + case "CHARACTER", "CHAR": + if col.CharacterMaximumLength != nil { + return fmt.Sprintf("CHAR(%d)", *col.CharacterMaximumLength) + } + return "CHAR" + case "NUMERIC", "DECIMAL": + if col.NumericPrecision != nil && col.NumericScale != nil { + return fmt.Sprintf("NUMERIC(%d,%d)", *col.NumericPrecision, *col.NumericScale) + } else if col.NumericPrecision != nil { + return fmt.Sprintf("NUMERIC(%d)", *col.NumericPrecision) + } + return "NUMERIC" + default: + return dataType + } +} + +func formatConstraint(constraint *ConstraintInfo) string { + if constraint == nil { + return "" + } + + var sb strings.Builder + sb.WriteString(fmt.Sprintf("CONSTRAINT \"%s\"", constraint.ConstraintName)) + switch constraint.ConstraintType { + case "PRIMARY KEY": + sb.WriteString(fmt.Sprintf(" PRIMARY KEY (\"%s\")", *constraint.ColumnName)) + case "UNIQUE": + sb.WriteString(fmt.Sprintf(" UNIQUE (\"%s\")", *constraint.ColumnName)) + case "FOREIGN KEY": + sb.WriteString(fmt.Sprintf(" FOREIGN KEY (\"%s\") REFERENCES \"%s\" (\"%s\")", + *constraint.ColumnName, *constraint.ForeignTableName, *constraint.ForeignColumnName)) + case "CHECK": + sb.WriteString(fmt.Sprintf(" CHECK (%s)", *constraint.CheckClause)) + } + return sb.String() +} + +// function that compares two tables schema and returns the DDL statements to update the schema to turn old -> new +func CompareTableSchemas(oldTable, newTable *Table, renames []RenameRelation) (string, error) { + // each of the three aspects of the schema (columns, constraints, indexes) will be compared separately + // the result will be three actions: add, drop and alter + // and a spiecial case for the renaming of the table itself + var ddlStatements strings.Builder + ddlStatements.WriteString(fmt.Sprintf("-- Comparing table schema for %s\n", oldTable.TableName)) + ddlStatements.WriteString("-- Generated automatically\n\n") + // Compare names + if oldTable.TableName != newTable.TableName { + ddlStatements.WriteString(fmt.Sprintf("ALTER TABLE \"%s\" RENAME TO \"%s\";\n", + oldTable.TableName, newTable.TableName)) + } + // Compare columns + oldColumns := make(map[string]*TableColumn) + for _, col := range oldTable.Columns { + oldColumns[col.ColumnName] = &col + } + newColumns := make(map[string]*TableColumn) + for _, col := range newTable.Columns { + newColumns[col.ColumnName] = &col + } + // renames + renamedColumns := make(map[string]string) + newRenamedColumns := make(map[string]bool) + for _, rename := range renames { + renamedColumns[rename.OldName] = rename.NewName + newRenamedColumns[rename.NewName] = true + } + + // drop columns that are in old but not in new onl + for colName, oldCol := range oldColumns { + // check for renames first + if newColName, exists := renamedColumns[colName]; exists { + ddlStatements.WriteString(fmt.Sprintf("ALTER TABLE \"%s\" RENAME COLUMN \"%s\" TO \"%s\";\n", + newTable.TableName, colName, newColName)) + continue + } + + // if the column is not in the new table, drop it + if _, exists := newColumns[colName]; !exists { + ddlStatements.WriteString(fmt.Sprintf("ALTER TABLE \"%s\" DROP COLUMN \"%s\";\n", + newTable.TableName, oldCol.ColumnName)) + continue + } + } + // add columns that are in new but not in old + for colName, newCol := range newColumns { + if _, exists := newRenamedColumns[colName]; exists { + continue // skip renamed columns + } + // check if the column is in the old table and add it if not + if _, exists := oldColumns[colName]; !exists { + ddlStatements.WriteString(fmt.Sprintf("ALTER TABLE \"%s\" ADD COLUMN \"%s\" %s;\n", + newTable.TableName, colName, formatDataType(*newCol))) + continue + } + + // check for changes in column properties + oldColumn := oldColumns[colName] + // type changes + if oldColumn.DataType != newCol.DataType { + ddlStatements.WriteString(fmt.Sprintf("ALTER TABLE \"%s\" ALTER COLUMN \"%s\" TYPE %s;\n", + newTable.TableName, colName, formatDataType(*newCol))) + } + // nullability changes + if oldColumn.IsNullable != newCol.IsNullable { + if newCol.IsNullable { + ddlStatements.WriteString(fmt.Sprintf("ALTER TABLE \"%s\" ALTER COLUMN \"%s\" DROP NOT NULL;\n", + newTable.TableName, colName)) + } else { + ddlStatements.WriteString(fmt.Sprintf("ALTER TABLE \"%s\" ALTER COLUMN \"%s\" SET NOT NULL;\n", + newTable.TableName, colName)) + } + } + // default value changes + if oldColumn.ColumnDefault != nil && newCol.ColumnDefault == nil { + ddlStatements.WriteString(fmt.Sprintf("ALTER TABLE \"%s\" ALTER COLUMN \"%s\" DROP DEFAULT;\n", + newTable.TableName, colName)) + } else if oldColumn.ColumnDefault == nil && newCol.ColumnDefault != nil { + ddlStatements.WriteString(fmt.Sprintf("ALTER TABLE \"%s\" ALTER COLUMN \"%s\" SET DEFAULT %s;\n", + newTable.TableName, colName, *newCol.ColumnDefault)) + } else if oldColumn.ColumnDefault != nil && newCol.ColumnDefault != nil && *oldColumn.ColumnDefault != *newCol.ColumnDefault { + ddlStatements.WriteString(fmt.Sprintf("ALTER TABLE \"%s\" ALTER COLUMN \"%s\" SET DEFAULT %s;\n", + newTable.TableName, colName, *newCol.ColumnDefault)) + } + // character maximum length changes + if oldColumn.CharacterMaximumLength != nil && newCol.CharacterMaximumLength == nil { + ddlStatements.WriteString(fmt.Sprintf("ALTER TABLE \"%s\" ALTER COLUMN \"%s\" TYPE %s;\n", + newTable.TableName, colName, formatDataType(*newCol))) + } else if oldColumn.CharacterMaximumLength == nil && newCol.CharacterMaximumLength != nil { + ddlStatements.WriteString(fmt.Sprintf("ALTER TABLE \"%s\" ALTER COLUMN \"%s\" TYPE %s;\n", + newTable.TableName, colName, formatDataType(*newCol))) + } else if oldColumn.CharacterMaximumLength != nil && newCol.CharacterMaximumLength != nil && + *oldColumn.CharacterMaximumLength != *newCol.CharacterMaximumLength { + ddlStatements.WriteString(fmt.Sprintf("ALTER TABLE \"%s\" ALTER COLUMN \"%s\" TYPE %s;\n", + newTable.TableName, colName, formatDataType(*newCol))) + } + + // numeric precision changes + if oldColumn.NumericPrecision != nil && newCol.NumericPrecision == nil { + ddlStatements.WriteString(fmt.Sprintf("ALTER TABLE \"%s\" ALTER COLUMN \"%s\" TYPE %s;\n", + newTable.TableName, colName, formatDataType(*newCol))) + } else if oldColumn.NumericPrecision == nil && newCol.NumericPrecision != nil { + ddlStatements.WriteString(fmt.Sprintf("ALTER TABLE \"%s\" ALTER COLUMN \"%s\" TYPE %s;\n", + newTable.TableName, colName, formatDataType(*newCol))) + } else if oldColumn.NumericPrecision != nil && newCol.NumericPrecision != nil && + *oldColumn.NumericPrecision != *newCol.NumericPrecision { + ddlStatements.WriteString(fmt.Sprintf("ALTER TABLE \"%s\" ALTER COLUMN \"%s\" TYPE %s;\n", + newTable.TableName, colName, formatDataType(*newCol))) + } + + // numeric scale changes + if oldColumn.NumericScale != nil && newCol.NumericScale == nil { + ddlStatements.WriteString(fmt.Sprintf("ALTER TABLE \"%s\" ALTER COLUMN \"%s\" TYPE %s;\n", + newTable.TableName, colName, formatDataType(*newCol))) + } else if oldColumn.NumericScale == nil && newCol.NumericScale != nil { + ddlStatements.WriteString(fmt.Sprintf("ALTER TABLE \"%s\" ALTER COLUMN \"%s\" TYPE %s;\n", + newTable.TableName, colName, formatDataType(*newCol))) + } else if oldColumn.NumericScale != nil && newCol.NumericScale != nil && + *oldColumn.NumericScale != *newCol.NumericScale { + ddlStatements.WriteString(fmt.Sprintf("ALTER TABLE \"%s\" ALTER COLUMN \"%s\" TYPE %s;\n", + newTable.TableName, colName, formatDataType(*newCol))) + } + } + + // Compare constraints + oldConstraints := make(map[string]*ConstraintInfo) + for _, constraint := range oldTable.Constraints { + oldConstraints[constraint.ConstraintName] = &constraint + } + newConstraints := make(map[string]*ConstraintInfo) + for _, constraint := range newTable.Constraints { + newConstraints[constraint.ConstraintName] = &constraint + } + + // drop constraints that are in old but not in new add executed with IF EXISTS to avoid errors + for constraintName, oldConstraint := range oldConstraints { + if _, exists := newConstraints[constraintName]; !exists { + ddlStatements.WriteString(fmt.Sprintf("ALTER TABLE \"%s\" DROP CONSTRAINT IF EXISTS \"%s\";\n", + newTable.TableName, oldConstraint.ConstraintName)) + continue + } + } + + // add constraints that are in new but not in old + for constraintName, newConstraint := range newConstraints { + if _, exists := oldConstraints[constraintName]; !exists { + ddlStatements.WriteString(fmt.Sprintf("ALTER TABLE \"%s\" ADD %s;\n", + newTable.TableName, formatConstraint(newConstraint))) + } + } + + // compare indexes + oldIndexes := make(map[string]*IndexInfo) + for _, index := range oldTable.Indexes { + oldIndexes[index.IndexName] = &index + } + newIndexes := make(map[string]*IndexInfo) + for _, index := range newTable.Indexes { + newIndexes[index.IndexName] = &index + } + // drop indexes that are in old but not in new + for indexName, oldIndex := range oldIndexes { + if _, exists := newIndexes[indexName]; !exists { + ddlStatements.WriteString(fmt.Sprintf("DROP INDEX IF EXISTS \"%s\";\n", oldIndex.IndexName)) + continue + } + } + + // add indexes that are in new but not in old + for indexName, newIndex := range newIndexes { + if _, exists := oldIndexes[indexName]; !exists { + ddlStatements.WriteString(generateCreateIndexStatement(newIndex)) + } + } + + // return the DDL statements + if ddlStatements.Len() == 0 { + return "", fmt.Errorf("no changes detected between old and new table schemas") + } + return ddlStatements.String(), nil +} \ No newline at end of file diff --git a/utils/utils.go b/utils/utils.go index 15d6d95..a28414b 100644 --- a/utils/utils.go +++ b/utils/utils.go @@ -21,19 +21,19 @@ import ( func ResponseHandler(w http.ResponseWriter, r *http.Request, simulatedError api.ApiError) { switch simulatedError.StatusCode { case http.StatusBadRequest: // 400 - response.BadRequest(w, simulatedError.Message, simulatedError.Error()) + response.BadRequest(w, r, simulatedError.Message, simulatedError.Error()) case http.StatusUnauthorized: // 401 - response.UnAuthorized(w, simulatedError.Message, simulatedError.Error()) + response.UnAuthorized(w, r, simulatedError.Message, simulatedError.Error()) case http.StatusNotFound: // 404 - response.NotFound(w, simulatedError.Message, simulatedError.Error()) + response.NotFound(w, r, simulatedError.Message, simulatedError.Error()) case http.StatusTooManyRequests: // 429 - response.TooManyRequests(w, simulatedError.Message, simulatedError.Error()) + response.TooManyRequests(w, r, simulatedError.Message, simulatedError.Error()) // default: - // response.InternalServerError(w, simulatedError.Message, simulatedError.Error()) + // response.InternalServerError(w, r, simulatedError.Message, simulatedError.Error()) case http.StatusInternalServerError: // 500 - response.InternalServerError(w, simulatedError.Message, simulatedError.Error()) + response.InternalServerError(w, r, simulatedError.Message, simulatedError.Error()) default: - response.CreateResponse(w, simulatedError.StatusCode, simulatedError.Message, simulatedError.Error(), nil, nil) + response.CreateResponse(w, r, simulatedError.StatusCode, simulatedError.Message, simulatedError.Error(), nil, nil) } } @@ -129,7 +129,7 @@ func ReplaceWhiteSpacesWithUnderscore(str string) string { return replaced } -func UserServerDbFormat(dbname string, userId int) string { +func UserServerDbFormat(dbname string, userId int64) string { dbname = strings.ToLower(dbname) - return dbname + "_" + strconv.Itoa(userId) -} + return dbname + "_" + strconv.FormatInt(userId, 10) +} \ No newline at end of file