From 7e9c2189fb26b55174b5e9fd4f106126b8f1a3cd Mon Sep 17 00:00:00 2001 From: rag-nar1 Date: Thu, 26 Jun 2025 11:10:44 +0300 Subject: [PATCH 001/101] Refactor user ID type from int to int64 and add new agent endpoint for handling requests --- AI/handlers.go | 45 +++++++++++++++++++++++++++++++++++++++- AI/routes.go | 1 + AI/services.go | 34 +++++++++++++++++++++++++++++- middleware/middleware.go | 2 +- tables/service.go | 10 ++++----- tables/utils.go | 2 +- utils/database.go | 2 +- utils/utils.go | 4 ++-- 8 files changed, 88 insertions(+), 12 deletions(-) diff --git a/AI/handlers.go b/AI/handlers.go index 6b7c758..d50043c 100644 --- a/AI/handlers.go +++ b/AI/handlers.go @@ -3,7 +3,10 @@ package ai import ( "DBHS/config" "DBHS/response" + "encoding/json" + "io" "net/http" + "github.com/gorilla/mux" ) @@ -29,7 +32,7 @@ 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 @@ -42,4 +45,44 @@ func Report(app *config.Application) http.HandlerFunc { response.OK(w, "Report generated successfully", report) } +} + +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, "Failed to read request body", err) + return + } + + // parse the request body + var requestBody map[string]interface{} + err = json.Unmarshal(body, &requestBody) + if err != nil { + response.BadRequest(w, "Failed to parse request body", err) + return + } + // check if the request body is valid + if requestBody["prompt"] == nil { + response.BadRequest(w, "Prompt is required", nil) + return + } + + prompt := requestBody["prompt"].(string) + // 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, prompt, config.AI) + if err != nil { + response.InternalServerError(w, "error while querying agent", err) + return + } + + response.OK(w, "Agent query successful", AIresponse) + } } \ No newline at end of file diff --git a/AI/routes.go b/AI/routes.go index 2928855..4739689 100644 --- a/AI/routes.go +++ b/AI/routes.go @@ -11,4 +11,5 @@ func DefineURLs() { AIProtected.Use(middleware.JwtAuthMiddleware, middleware.CheckOwnership) AIProtected.Handle("/report", middleware.MethodsAllowed(http.MethodGet)(Report(config.App))) + AIProtected.Handle("/agent", middleware.MethodsAllowed(http.MethodPost)(Agent(config.App))) } \ No newline at end of file diff --git a/AI/services.go b/AI/services.go index 839806c..6b78606 100644 --- a/AI/services.go +++ b/AI/services.go @@ -5,10 +5,12 @@ import ( "DBHS/tables" "context" "encoding/json" + "time" + "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) if err != nil { @@ -35,3 +37,33 @@ func getReport(projectUUID string, userID int, analytics Analytics, AI RAG.RAGmo return report, nil } + +func AgentQuery(projectUUID string, userID int64, prompt string, AI RAG.RAGmodel) (*RAG.AgentResponse, error) { + // get project name and connection + _, userDb, err := tables.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 + } + + // 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 +} \ No newline at end of file diff --git a/middleware/middleware.go b/middleware/middleware.go index ebe1637..2dfa39c 100644 --- a/middleware/middleware.go +++ b/middleware/middleware.go @@ -41,7 +41,7 @@ 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) + userId := r.Context().Value("user-id").(int64) ok, err := utils.CheckOwnershipQuery(r.Context(), projectId, userId, config.DB) if err != nil { response.InternalServerError(w, err.Error(), err) diff --git a/tables/service.go b/tables/service.go index 1cd180f..3d3da1d 100644 --- a/tables/service.go +++ b/tables/service.go @@ -11,7 +11,7 @@ import ( ) func GetAllTables(ctx context.Context, projectOID string, servDb *pgxpool.Pool) ([]ShortTable, error) { - userId, ok := ctx.Value("user-id").(int) + userId, ok := ctx.Value("user-id").(int64) if !ok || userId == 0 { return nil, errors.New("Unauthorized") } @@ -30,7 +30,7 @@ func GetAllTables(ctx context.Context, projectOID string, servDb *pgxpool.Pool) } func CreateTable(ctx context.Context, projectOID string, table *ClientTable, servDb *pgxpool.Pool) (string, error) { - userId, ok := ctx.Value("user-id").(int) + userId, ok := ctx.Value("user-id").(int64) if !ok || userId == 0 { return "", errors.New("Unauthorized") } @@ -68,7 +68,7 @@ func CreateTable(ctx context.Context, projectOID string, table *ClientTable, ser } func UpdateTable(ctx context.Context, projectOID string, tableOID string, updates *TableUpdate, servDb *pgxpool.Pool) error { - userId, ok := ctx.Value("user-id").(int) + userId, ok := ctx.Value("user-id").(int64) if !ok || userId == 0 { return errors.New("Unauthorized") } @@ -105,7 +105,7 @@ func UpdateTable(ctx context.Context, projectOID string, tableOID string, update } func DeletTable(ctx context.Context, projectOID, tableOID string, servDb *pgxpool.Pool) error { - userId, ok := ctx.Value("user-id").(int) + userId, ok := ctx.Value("user-id").(int64) if !ok || userId == 0 { return errors.New("Unauthorized") } @@ -175,7 +175,7 @@ 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 } diff --git a/tables/utils.go b/tables/utils.go index bb6c90a..cecd99a 100644 --- a/tables/utils.go +++ b/tables/utils.go @@ -63,7 +63,7 @@ func CheckForValidTable(table *ClientTable) bool { return true } -func ExtractDb(ctx context.Context, projectOID string, UserID int, servDb *pgxpool.Pool) (int64, *pgxpool.Pool, error) { +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 { diff --git a/utils/database.go b/utils/database.go index 11c03f8..c263eb9 100644 --- a/utils/database.go +++ b/utils/database.go @@ -14,7 +14,7 @@ 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, projectId string, userId int64, db Querier) (bool, error) { var count int err := db.QueryRow(ctx, CheckOwnershipStmt, projectId, userId).Scan(&count) if err != nil { diff --git a/utils/utils.go b/utils/utils.go index 15d6d95..97174c9 100644 --- a/utils/utils.go +++ b/utils/utils.go @@ -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) } From dd317b23844148f2f13549b32a9a152341978e23 Mon Sep 17 00:00:00 2001 From: rag-nar1 Date: Thu, 26 Jun 2025 11:54:37 +0300 Subject: [PATCH 002/101] Update go.mod to require AI-Agent v1.0.2 and add response.md to .gitignore --- .gitignore | 3 ++- go.mod | 2 +- go.sum | 4 ++++ 3 files changed, 7 insertions(+), 2 deletions(-) diff --git a/.gitignore b/.gitignore index c27266f..64e46af 100644 --- a/.gitignore +++ b/.gitignore @@ -8,4 +8,5 @@ tmp/main* curl.sh .qodo *.log -pr.md \ No newline at end of file +pr.md +response.md \ No newline at end of file diff --git a/go.mod b/go.mod index a124ac6..1651f1b 100644 --- a/go.mod +++ b/go.mod @@ -6,7 +6,7 @@ go 1.24.0 // 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.2 github.com/MarceloPetrucio/go-scalar-api-reference v0.0.0-20240521013641-ce5d2efe0e06 github.com/georgysavva/scany/v2 v2.1.4 github.com/golang-jwt/jwt/v5 v5.2.1 diff --git a/go.sum b/go.sum index 0bee2fe..f4edff9 100644 --- a/go.sum +++ b/go.sum @@ -14,6 +14,10 @@ cloud.google.com/go/longrunning v0.5.7/go.mod h1:8GClkudohy1Fxm3owmBGid8W0pSgodE 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.1 h1:nx7cwZs9KTUdje2sTcRM0+ZNOblnVxX8z6GL+WE1JUA= +github.com/Database-Hosting-Services/AI-Agent v1.0.1/go.mod h1:4JoX4wIMVtU9ydEOFS6MiDQKnuRfCxSmuesGhmCokAY= +github.com/Database-Hosting-Services/AI-Agent v1.0.2 h1:lAf3mendjBP+1Q6G1bHSUNP7HsoSS+lZLF4bL8b/zkk= +github.com/Database-Hosting-Services/AI-Agent v1.0.2/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= From b29c71010f83f904639903b6e1c8bc0bad378e86 Mon Sep 17 00:00:00 2001 From: rag-nar1 Date: Fri, 27 Jun 2025 09:47:48 +0300 Subject: [PATCH 003/101] feat: refactor project and table handling, update deployment flag and enhance Swagger documentation --- AI/repository.go | 129 +++++++++++++++++++++++++++++++++++++++++ AI/services.go | 4 +- config/application.go | 2 +- config/utils.go | 4 +- docs/docs.go | 105 +++++++++++++++++++++++++++++++++ docs/swagger.json | 105 +++++++++++++++++++++++++++++++++ docs/swagger.yaml | 69 ++++++++++++++++++++++ projects/repository.go | 9 +++ tables/handlers.go | 8 ++- tables/models.go | 3 + tables/repository.go | 9 --- tables/service.go | 10 ++-- tables/utils.go | 17 ------ tmp/air_errors.log | 2 +- utils/database.go | 27 +++++++++ 15 files changed, 464 insertions(+), 39 deletions(-) diff --git a/AI/repository.go b/AI/repository.go index c900730..7bdb69b 100644 --- a/AI/repository.go +++ b/AI/repository.go @@ -45,6 +45,13 @@ type IndexInfo struct { IsPrimary bool `db:"is_primary"` } +type Table struct { + TableName string `db:"table_name"` + Columns []TableColumn `db:"columns"` + Constraints []ConstraintInfo `db:"constraints"` + Indexes []IndexInfo `db:"indexes"` +} + const ( // Query to get all tables and their columns with detailed information getTablesAndColumnsQuery = ` @@ -133,6 +140,128 @@ const ( 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 utils.Querier) ([]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], + } + } + + tables := make([]Table, 0) + for _, table := range tablesMap { + tables = append(tables, table) + } + + return tables, nil +} + +func GetConstraints(ctx context.Context, db utils.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 utils.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 utils.Querier) (string, error) { diff --git a/AI/services.go b/AI/services.go index 839806c..a62046d 100644 --- a/AI/services.go +++ b/AI/services.go @@ -2,7 +2,7 @@ package ai import ( "DBHS/config" - "DBHS/tables" + "DBHS/utils" "context" "encoding/json" "github.com/Database-Hosting-Services/AI-Agent/RAG" @@ -10,7 +10,7 @@ import ( func getReport(projectUUID string, userID int, 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 } diff --git a/config/application.go b/config/application.go index fcf65be..76f66e8 100644 --- a/config/application.go +++ b/config/application.go @@ -71,7 +71,7 @@ func loadEnv() { } } -const deploy = true +const deploy = false func Init(infoLog, errorLog *log.Logger) { 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 9f1ffd0..578488c 100644 --- a/docs/docs.go +++ b/docs/docs.go @@ -1946,6 +1946,90 @@ const docTemplate = `{ } } }, + "ai.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" + } + } + }, + "ai.IndexInfo": { + "type": "object", + "properties": { + "columnName": { + "type": "string" + }, + "indexName": { + "type": "string" + }, + "indexType": { + "type": "string" + }, + "isPrimary": { + "type": "boolean" + }, + "isUnique": { + "type": "boolean" + }, + "tableName": { + "type": "string" + } + } + }, + "ai.TableColumn": { + "type": "object", + "properties": { + "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" + } + } + }, "indexes.IndexData": { "type": "object", "properties": { @@ -2213,12 +2297,30 @@ const docTemplate = `{ "tables.Table": { "type": "object", "properties": { + "columns": { + "type": "array", + "items": { + "$ref": "#/definitions/ai.TableColumn" + } + }, + "constraints": { + "type": "array", + "items": { + "$ref": "#/definitions/ai.ConstraintInfo" + } + }, "description": { "type": "string" }, "id": { "type": "integer" }, + "indexes": { + "type": "array", + "items": { + "$ref": "#/definitions/ai.IndexInfo" + } + }, "name": { "type": "string" }, @@ -2227,6 +2329,9 @@ const docTemplate = `{ }, "project_id": { "type": "integer" + }, + "tableName": { + "type": "string" } } }, diff --git a/docs/swagger.json b/docs/swagger.json index 3a53122..d8a8a19 100644 --- a/docs/swagger.json +++ b/docs/swagger.json @@ -1939,6 +1939,90 @@ } } }, + "ai.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" + } + } + }, + "ai.IndexInfo": { + "type": "object", + "properties": { + "columnName": { + "type": "string" + }, + "indexName": { + "type": "string" + }, + "indexType": { + "type": "string" + }, + "isPrimary": { + "type": "boolean" + }, + "isUnique": { + "type": "boolean" + }, + "tableName": { + "type": "string" + } + } + }, + "ai.TableColumn": { + "type": "object", + "properties": { + "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" + } + } + }, "indexes.IndexData": { "type": "object", "properties": { @@ -2206,12 +2290,30 @@ "tables.Table": { "type": "object", "properties": { + "columns": { + "type": "array", + "items": { + "$ref": "#/definitions/ai.TableColumn" + } + }, + "constraints": { + "type": "array", + "items": { + "$ref": "#/definitions/ai.ConstraintInfo" + } + }, "description": { "type": "string" }, "id": { "type": "integer" }, + "indexes": { + "type": "array", + "items": { + "$ref": "#/definitions/ai.IndexInfo" + } + }, "name": { "type": "string" }, @@ -2220,6 +2322,9 @@ }, "project_id": { "type": "integer" + }, + "tableName": { + "type": "string" } } }, diff --git a/docs/swagger.yaml b/docs/swagger.yaml index 6e41f7d..f73942d 100644 --- a/docs/swagger.yaml +++ b/docs/swagger.yaml @@ -171,6 +171,61 @@ definitions: example: User verified successfully type: string type: object + ai.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 + ai.IndexInfo: + properties: + columnName: + type: string + indexName: + type: string + indexType: + type: string + isPrimary: + type: boolean + isUnique: + type: boolean + tableName: + type: string + type: object + ai.TableColumn: + properties: + 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 + type: object indexes.IndexData: properties: columns: @@ -346,16 +401,30 @@ definitions: type: object tables.Table: properties: + columns: + items: + $ref: '#/definitions/ai.TableColumn' + type: array + constraints: + items: + $ref: '#/definitions/ai.ConstraintInfo' + type: array description: type: string id: type: integer + indexes: + items: + $ref: '#/definitions/ai.IndexInfo' + type: array name: type: string oid: type: string project_id: type: integer + tableName: + type: string type: object tables.TableUpdate: properties: diff --git a/projects/repository.go b/projects/repository.go index 823eaa6..2ad4b55 100644 --- a/projects/repository.go +++ b/projects/repository.go @@ -55,3 +55,12 @@ 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 +} \ No newline at end of file diff --git a/tables/handlers.go b/tables/handlers.go index cd992d1..b7c6adb 100644 --- a/tables/handlers.go +++ b/tables/handlers.go @@ -9,7 +9,7 @@ import ( "github.com/gorilla/mux" ) - +// old body /* body tableName: "", @@ -28,6 +28,9 @@ import ( ] */ + +// new body + // GetAllTablesHanlder godoc // @Summary Get all tables in a project // @Description Get a list of all tables in the specified project @@ -45,9 +48,10 @@ func GetAllTablesHanlder(app *config.Application) http.HandlerFunc { urlVariables := mux.Vars(r) projectId := urlVariables["project_id"] if projectId == "" { - response.BadRequest(w, "Project ID is required", nil) + response.NotFound(w, "Project ID is required", nil) return } + data, err := GetAllTables(r.Context(), projectId, config.DB) if err != nil { if errors.Is(err, response.ErrUnauthorized) { diff --git a/tables/models.go b/tables/models.go index bde1879..5e05436 100644 --- a/tables/models.go +++ b/tables/models.go @@ -1,7 +1,10 @@ package tables +import ai "DBHS/AI" + // Table struct is a row record of the tables table in the database type Table struct { + ai.Table ID int `json:"id" db:"id"` ProjectID int64 `json:"project_id" db:"project_id"` OID string `json:"oid" db:"oid"` diff --git a/tables/repository.go b/tables/repository.go index 21e025c..ca22042 100644 --- a/tables/repository.go +++ b/tables/repository.go @@ -11,15 +11,6 @@ import ( "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) - if err != nil { - return nil, 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) diff --git a/tables/service.go b/tables/service.go index 1cd180f..fcce4b0 100644 --- a/tables/service.go +++ b/tables/service.go @@ -16,7 +16,7 @@ func GetAllTables(ctx context.Context, projectOID string, servDb *pgxpool.Pool) return nil, errors.New("Unauthorized") } - _, projectId, err := GetProjectNameID(ctx, projectOID, servDb) + _, projectId, err := utils.GetProjectNameID(ctx, projectOID, servDb) if err != nil { return nil, err } @@ -35,7 +35,7 @@ func CreateTable(ctx context.Context, projectOID string, table *ClientTable, ser return "", errors.New("Unauthorized") } - projectId, userDb, err := ExtractDb(ctx, projectOID, userId, servDb) + projectId, userDb, err := utils.ExtractDb(ctx, projectOID, userId, servDb) if err != nil { return "", err } @@ -73,7 +73,7 @@ func UpdateTable(ctx context.Context, projectOID string, tableOID string, update return errors.New("Unauthorized") } - _, userDb, err := ExtractDb(ctx, projectOID, userId, servDb) + _, userDb, err := utils.ExtractDb(ctx, projectOID, userId, servDb) if err != nil { return err } @@ -110,7 +110,7 @@ func DeletTable(ctx context.Context, projectOID, tableOID string, servDb *pgxpoo return errors.New("Unauthorized") } - _, userDb, err := ExtractDb(ctx, projectOID, userId, servDb) + _, userDb, err := utils.ExtractDb(ctx, projectOID, userId, servDb) if err != nil { return err } @@ -180,7 +180,7 @@ func ReadTable(ctx context.Context, projectOID, tableOID string, parameters map[ 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 } diff --git a/tables/utils.go b/tables/utils.go index bb6c90a..d3b7f96 100644 --- a/tables/utils.go +++ b/tables/utils.go @@ -1,7 +1,6 @@ package tables import ( - "DBHS/config" "DBHS/utils" "context" "fmt" @@ -9,7 +8,6 @@ import ( "strings" "github.com/jackc/pgx/v5" - "github.com/jackc/pgx/v5/pgxpool" ) func CreateColumnDefinition(column *Column) string { @@ -63,21 +61,6 @@ func CheckForValidTable(table *ClientTable) bool { 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" diff --git a/tmp/air_errors.log b/tmp/air_errors.log index 481923f..4b52cb8 100644 --- a/tmp/air_errors.log +++ b/tmp/air_errors.log @@ -1 +1 @@ -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 1 \ No newline at end of file +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 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 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..09b2ae5 100644 --- a/utils/database.go +++ b/utils/database.go @@ -1,8 +1,11 @@ package utils import ( + "DBHS/config" "context" "fmt" + + "github.com/jackc/pgx/v5/pgxpool" ) var ( @@ -22,3 +25,27 @@ func CheckOwnershipQuery(ctx context.Context, projectId string, userId int, db Q } return count > 0, nil } + +func GetProjectNameID(ctx context.Context, projectId string, db 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 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, UserServerDbFormat(dbName.(string), UserID)) + if err != nil { + return 0, nil, err + } + + return projectId.(int64), userDb, nil +} \ No newline at end of file From 1ac671be78bf9acff0108d4984d979b43a459f26 Mon Sep 17 00:00:00 2001 From: rag-nar1 Date: Fri, 27 Jun 2025 09:57:01 +0300 Subject: [PATCH 004/101] refactor: the returned data from the get all tables handler --- AI/db_test.go | 4 +- AI/repository.go | 458 +------------------------------------------ AI/services.go | 2 +- docs/docs.go | 188 +++++++++--------- docs/swagger.json | 188 +++++++++--------- docs/swagger.yaml | 128 ++++++------ tables/handlers.go | 6 +- tables/models.go | 6 +- tables/repository.go | 15 +- tables/service.go | 13 +- tmp/air_errors.log | 2 +- utils/schema.go | 450 ++++++++++++++++++++++++++++++++++++++++++ 12 files changed, 733 insertions(+), 727 deletions(-) create mode 100644 utils/schema.go 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/repository.go b/AI/repository.go index 7bdb69b..a7ca234 100644 --- a/AI/repository.go +++ b/AI/repository.go @@ -1,457 +1 @@ -package ai - -import ( - "context" - "fmt" - "strings" - - "DBHS/utils" - - "github.com/georgysavva/scany/v2/pgxscan" -) - -// TableColumn represents a database column with its properties -type TableColumn struct { - TableName string `db:"table_name"` - ColumnName string `db:"column_name"` - DataType string `db:"data_type"` - IsNullable bool `db:"is_nullable"` - ColumnDefault *string `db:"column_default"` - CharacterMaximumLength *int `db:"character_maximum_length"` - NumericPrecision *int `db:"numeric_precision"` - NumericScale *int `db:"numeric_scale"` - OrdinalPosition int `db:"ordinal_position"` -} - -// ConstraintInfo represents database constraints -type ConstraintInfo struct { - TableName string `db:"table_name"` - ConstraintName string `db:"constraint_name"` - ConstraintType string `db:"constraint_type"` - ColumnName *string `db:"column_name"` - ForeignTableName *string `db:"foreign_table_name"` - ForeignColumnName *string `db:"foreign_column_name"` - CheckClause *string `db:"check_clause"` - OrdinalPosition *int `db:"ordinal_position"` -} - -// IndexInfo represents database indexes -type IndexInfo struct { - TableName string `db:"table_name"` - IndexName string `db:"index_name"` - ColumnName string `db:"column_name"` - IsUnique bool `db:"is_unique"` - IndexType string `db:"index_type"` - IsPrimary bool `db:"is_primary"` -} - -type Table struct { - TableName string `db:"table_name"` - Columns []TableColumn `db:"columns"` - Constraints []ConstraintInfo `db:"constraints"` - Indexes []IndexInfo `db:"indexes"` -} - -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;` -) -/* - 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 utils.Querier) ([]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], - } - } - - tables := make([]Table, 0) - for _, table := range tablesMap { - tables = append(tables, table) - } - - return tables, nil -} - -func GetConstraints(ctx context.Context, db utils.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 utils.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 utils.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 -} - -// 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 - } -} +package ai \ No newline at end of file diff --git a/AI/services.go b/AI/services.go index a62046d..dde2bae 100644 --- a/AI/services.go +++ b/AI/services.go @@ -16,7 +16,7 @@ func getReport(projectUUID string, userID int, analytics Analytics, AI RAG.RAGmo } // get database schema - databaseSchema, err := ExtractDatabaseSchema(context.Background(), userDb) + databaseSchema, err := utils.ExtractDatabaseSchema(context.Background(), userDb) if err != nil { return "", err } diff --git a/docs/docs.go b/docs/docs.go index 578488c..92e2f55 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" } } } @@ -1946,90 +1946,6 @@ const docTemplate = `{ } } }, - "ai.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" - } - } - }, - "ai.IndexInfo": { - "type": "object", - "properties": { - "columnName": { - "type": "string" - }, - "indexName": { - "type": "string" - }, - "indexType": { - "type": "string" - }, - "isPrimary": { - "type": "boolean" - }, - "isUnique": { - "type": "boolean" - }, - "tableName": { - "type": "string" - } - } - }, - "ai.TableColumn": { - "type": "object", - "properties": { - "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" - } - } - }, "indexes.IndexData": { "type": "object", "properties": { @@ -2300,13 +2216,13 @@ const docTemplate = `{ "columns": { "type": "array", "items": { - "$ref": "#/definitions/ai.TableColumn" + "$ref": "#/definitions/utils.TableColumn" } }, "constraints": { "type": "array", "items": { - "$ref": "#/definitions/ai.ConstraintInfo" + "$ref": "#/definitions/utils.ConstraintInfo" } }, "description": { @@ -2318,7 +2234,7 @@ const docTemplate = `{ "indexes": { "type": "array", "items": { - "$ref": "#/definitions/ai.IndexInfo" + "$ref": "#/definitions/utils.IndexInfo" } }, "name": { @@ -2365,6 +2281,90 @@ const docTemplate = `{ "$ref": "#/definitions/tables.Column" } } + }, + "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.TableColumn": { + "type": "object", + "properties": { + "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" + } + } } }, "securityDefinitions": { diff --git a/docs/swagger.json b/docs/swagger.json index d8a8a19..29f9b1b 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" } } } @@ -1939,90 +1939,6 @@ } } }, - "ai.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" - } - } - }, - "ai.IndexInfo": { - "type": "object", - "properties": { - "columnName": { - "type": "string" - }, - "indexName": { - "type": "string" - }, - "indexType": { - "type": "string" - }, - "isPrimary": { - "type": "boolean" - }, - "isUnique": { - "type": "boolean" - }, - "tableName": { - "type": "string" - } - } - }, - "ai.TableColumn": { - "type": "object", - "properties": { - "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" - } - } - }, "indexes.IndexData": { "type": "object", "properties": { @@ -2293,13 +2209,13 @@ "columns": { "type": "array", "items": { - "$ref": "#/definitions/ai.TableColumn" + "$ref": "#/definitions/utils.TableColumn" } }, "constraints": { "type": "array", "items": { - "$ref": "#/definitions/ai.ConstraintInfo" + "$ref": "#/definitions/utils.ConstraintInfo" } }, "description": { @@ -2311,7 +2227,7 @@ "indexes": { "type": "array", "items": { - "$ref": "#/definitions/ai.IndexInfo" + "$ref": "#/definitions/utils.IndexInfo" } }, "name": { @@ -2358,6 +2274,90 @@ "$ref": "#/definitions/tables.Column" } } + }, + "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.TableColumn": { + "type": "object", + "properties": { + "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" + } + } } }, "securityDefinitions": { diff --git a/docs/swagger.yaml b/docs/swagger.yaml index f73942d..c92bc62 100644 --- a/docs/swagger.yaml +++ b/docs/swagger.yaml @@ -171,61 +171,6 @@ definitions: example: User verified successfully type: string type: object - ai.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 - ai.IndexInfo: - properties: - columnName: - type: string - indexName: - type: string - indexType: - type: string - isPrimary: - type: boolean - isUnique: - type: boolean - tableName: - type: string - type: object - ai.TableColumn: - properties: - 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 - type: object indexes.IndexData: properties: columns: @@ -403,11 +348,11 @@ definitions: properties: columns: items: - $ref: '#/definitions/ai.TableColumn' + $ref: '#/definitions/utils.TableColumn' type: array constraints: items: - $ref: '#/definitions/ai.ConstraintInfo' + $ref: '#/definitions/utils.ConstraintInfo' type: array description: type: string @@ -415,7 +360,7 @@ definitions: type: integer indexes: items: - $ref: '#/definitions/ai.IndexInfo' + $ref: '#/definitions/utils.IndexInfo' type: array name: type: string @@ -446,6 +391,61 @@ definitions: update: $ref: '#/definitions/tables.Column' 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 + utils.IndexInfo: + properties: + columnName: + type: string + indexName: + type: string + indexType: + type: string + isPrimary: + type: boolean + isUnique: + type: boolean + tableName: + type: string + type: object + utils.TableColumn: + properties: + 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 + type: object info: contact: {} description: API for DBHS application @@ -547,18 +547,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 diff --git a/tables/handlers.go b/tables/handlers.go index b7c6adb..4dc225e 100644 --- a/tables/handlers.go +++ b/tables/handlers.go @@ -39,9 +39,9 @@ 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 { return func(w http.ResponseWriter, r *http.Request) { diff --git a/tables/models.go b/tables/models.go index 5e05436..81fcddf 100644 --- a/tables/models.go +++ b/tables/models.go @@ -1,10 +1,12 @@ package tables -import ai "DBHS/AI" +import ( + "DBHS/utils" +) // Table struct is a row record of the tables table in the database type Table struct { - ai.Table + utils.Table ID int `json:"id" db:"id"` ProjectID int64 `json:"project_id" db:"project_id"` OID string `json:"oid" db:"oid"` diff --git a/tables/repository.go b/tables/repository.go index ca22042..c0a74a4 100644 --- a/tables/repository.go +++ b/tables/repository.go @@ -11,13 +11,24 @@ import ( "github.com/georgysavva/scany/v2/pgxscan" ) -func GetAllTablesNameOid(ctx context.Context, projectId int64, db pgxscan.Querier) ([]ShortTable, error) { - var tables []ShortTable +func GetAllTablesRepository(ctx context.Context, projectId int64, db utils.Querier) ([]Table, error) { + var tables []Table err := pgxscan.Select(ctx, db, &tables, `SELECT oid, name FROM "Ptable" WHERE project_id = $1`, projectId) if err != nil { return nil, err } + // extract the table schema + tableSchema, err := utils.GetTables(ctx, db) + if err != nil { + return nil, err + } + + // convert the table schema to the table model + for _, table := range tables { + table.Table = tableSchema[table.Name] + } + return tables, err } diff --git a/tables/service.go b/tables/service.go index fcce4b0..debb160 100644 --- a/tables/service.go +++ b/tables/service.go @@ -5,15 +5,14 @@ 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) { +func GetAllTables(ctx context.Context, projectOID string, servDb *pgxpool.Pool) ([]Table, error) { userId, ok := ctx.Value("user-id").(int) if !ok || userId == 0 { - return nil, errors.New("Unauthorized") + return nil, response.ErrUnauthorized } _, projectId, err := utils.GetProjectNameID(ctx, projectOID, servDb) @@ -21,7 +20,7 @@ func GetAllTables(ctx context.Context, projectOID string, servDb *pgxpool.Pool) return nil, err } - tables, err := GetAllTablesNameOid(ctx, projectId.(int64), servDb) + tables, err := GetAllTablesRepository(ctx, projectId.(int64), servDb) if err != nil { return nil, err } @@ -32,7 +31,7 @@ func GetAllTables(ctx context.Context, projectOID string, servDb *pgxpool.Pool) func CreateTable(ctx context.Context, projectOID string, table *ClientTable, servDb *pgxpool.Pool) (string, error) { userId, ok := ctx.Value("user-id").(int) if !ok || userId == 0 { - return "", errors.New("Unauthorized") + return "", response.ErrUnauthorized } projectId, userDb, err := utils.ExtractDb(ctx, projectOID, userId, servDb) @@ -70,7 +69,7 @@ func CreateTable(ctx context.Context, projectOID string, table *ClientTable, ser func UpdateTable(ctx context.Context, projectOID string, tableOID string, updates *TableUpdate, servDb *pgxpool.Pool) error { userId, ok := ctx.Value("user-id").(int) if !ok || userId == 0 { - return errors.New("Unauthorized") + return response.ErrUnauthorized } _, userDb, err := utils.ExtractDb(ctx, projectOID, userId, servDb) @@ -107,7 +106,7 @@ func UpdateTable(ctx context.Context, projectOID string, tableOID string, update func DeletTable(ctx context.Context, projectOID, tableOID string, servDb *pgxpool.Pool) error { userId, ok := ctx.Value("user-id").(int) if !ok || userId == 0 { - return errors.New("Unauthorized") + return response.ErrUnauthorized } _, userDb, err := utils.ExtractDb(ctx, projectOID, userId, servDb) diff --git a/tmp/air_errors.log b/tmp/air_errors.log index 4b52cb8..9893abd 100644 --- a/tmp/air_errors.log +++ b/tmp/air_errors.log @@ -1 +1 @@ -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 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 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 +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 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 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 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/schema.go b/utils/schema.go new file mode 100644 index 0000000..a49d414 --- /dev/null +++ b/utils/schema.go @@ -0,0 +1,450 @@ +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"` + ColumnName string `db:"column_name"` + DataType string `db:"data_type"` + IsNullable bool `db:"is_nullable"` + ColumnDefault *string `db:"column_default"` + CharacterMaximumLength *int `db:"character_maximum_length"` + NumericPrecision *int `db:"numeric_precision"` + NumericScale *int `db:"numeric_scale"` + OrdinalPosition int `db:"ordinal_position"` +} + +// ConstraintInfo represents database constraints +type ConstraintInfo struct { + TableName string `db:"table_name"` + ConstraintName string `db:"constraint_name"` + ConstraintType string `db:"constraint_type"` + ColumnName *string `db:"column_name"` + ForeignTableName *string `db:"foreign_table_name"` + ForeignColumnName *string `db:"foreign_column_name"` + CheckClause *string `db:"check_clause"` + OrdinalPosition *int `db:"ordinal_position"` +} + +// IndexInfo represents database indexes +type IndexInfo struct { + TableName string `db:"table_name"` + IndexName string `db:"index_name"` + ColumnName string `db:"column_name"` + IsUnique bool `db:"is_unique"` + IndexType string `db:"index_type"` + IsPrimary bool `db:"is_primary"` +} + +type Table struct { + TableName string `db:"table_name"` + Columns []TableColumn `db:"columns"` + Constraints []ConstraintInfo `db:"constraints"` + Indexes []IndexInfo `db:"indexes"` +} + +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;` +) +/* + 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 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 +} + +// 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 + } +} From 68a7251acaaaad91f212c396abdabe01f972fc63 Mon Sep 17 00:00:00 2001 From: rag-nar1 Date: Fri, 27 Jun 2025 10:06:18 +0300 Subject: [PATCH 005/101] feat: add schema property to tables and update documentation --- .air.toml | 3 ++- docs/docs.go | 48 ++++++++++++++++++++++++++------------------ docs/swagger.json | 48 ++++++++++++++++++++++++++------------------ docs/swagger.yaml | 33 +++++++++++++++++------------- tables/models.go | 12 +++++------ tables/repository.go | 2 +- tmp/air_errors.log | 2 +- 7 files changed, 85 insertions(+), 63 deletions(-) diff --git a/.air.toml b/.air.toml index abf3818..da4cb18 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"] exclude_dir = ["tmp", "vendor", "testdata", ".git"] kill_delay = "1s" diff --git a/docs/docs.go b/docs/docs.go index 92e2f55..0121284 100644 --- a/docs/docs.go +++ b/docs/docs.go @@ -2213,30 +2213,12 @@ const docTemplate = `{ "tables.Table": { "type": "object", "properties": { - "columns": { - "type": "array", - "items": { - "$ref": "#/definitions/utils.TableColumn" - } - }, - "constraints": { - "type": "array", - "items": { - "$ref": "#/definitions/utils.ConstraintInfo" - } - }, "description": { "type": "string" }, "id": { "type": "integer" }, - "indexes": { - "type": "array", - "items": { - "$ref": "#/definitions/utils.IndexInfo" - } - }, "name": { "type": "string" }, @@ -2246,8 +2228,8 @@ const docTemplate = `{ "project_id": { "type": "integer" }, - "tableName": { - "type": "string" + "schema": { + "$ref": "#/definitions/utils.Table" } } }, @@ -2334,6 +2316,32 @@ const docTemplate = `{ } } }, + "utils.Table": { + "type": "object", + "properties": { + "columns": { + "type": "array", + "items": { + "$ref": "#/definitions/utils.TableColumn" + } + }, + "constraints": { + "type": "array", + "items": { + "$ref": "#/definitions/utils.ConstraintInfo" + } + }, + "indexes": { + "type": "array", + "items": { + "$ref": "#/definitions/utils.IndexInfo" + } + }, + "tableName": { + "type": "string" + } + } + }, "utils.TableColumn": { "type": "object", "properties": { diff --git a/docs/swagger.json b/docs/swagger.json index 29f9b1b..8c78cbb 100644 --- a/docs/swagger.json +++ b/docs/swagger.json @@ -2206,30 +2206,12 @@ "tables.Table": { "type": "object", "properties": { - "columns": { - "type": "array", - "items": { - "$ref": "#/definitions/utils.TableColumn" - } - }, - "constraints": { - "type": "array", - "items": { - "$ref": "#/definitions/utils.ConstraintInfo" - } - }, "description": { "type": "string" }, "id": { "type": "integer" }, - "indexes": { - "type": "array", - "items": { - "$ref": "#/definitions/utils.IndexInfo" - } - }, "name": { "type": "string" }, @@ -2239,8 +2221,8 @@ "project_id": { "type": "integer" }, - "tableName": { - "type": "string" + "schema": { + "$ref": "#/definitions/utils.Table" } } }, @@ -2327,6 +2309,32 @@ } } }, + "utils.Table": { + "type": "object", + "properties": { + "columns": { + "type": "array", + "items": { + "$ref": "#/definitions/utils.TableColumn" + } + }, + "constraints": { + "type": "array", + "items": { + "$ref": "#/definitions/utils.ConstraintInfo" + } + }, + "indexes": { + "type": "array", + "items": { + "$ref": "#/definitions/utils.IndexInfo" + } + }, + "tableName": { + "type": "string" + } + } + }, "utils.TableColumn": { "type": "object", "properties": { diff --git a/docs/swagger.yaml b/docs/swagger.yaml index c92bc62..7824882 100644 --- a/docs/swagger.yaml +++ b/docs/swagger.yaml @@ -346,30 +346,18 @@ definitions: type: object tables.Table: properties: - columns: - items: - $ref: '#/definitions/utils.TableColumn' - type: array - constraints: - items: - $ref: '#/definitions/utils.ConstraintInfo' - type: array description: type: string id: type: integer - indexes: - items: - $ref: '#/definitions/utils.IndexInfo' - type: array name: type: string oid: type: string project_id: type: integer - tableName: - type: string + schema: + $ref: '#/definitions/utils.Table' type: object tables.TableUpdate: properties: @@ -425,6 +413,23 @@ definitions: tableName: type: string type: object + utils.Table: + properties: + columns: + items: + $ref: '#/definitions/utils.TableColumn' + type: array + constraints: + items: + $ref: '#/definitions/utils.ConstraintInfo' + type: array + indexes: + items: + $ref: '#/definitions/utils.IndexInfo' + type: array + tableName: + type: string + type: object utils.TableColumn: properties: characterMaximumLength: diff --git a/tables/models.go b/tables/models.go index 81fcddf..502d6d7 100644 --- a/tables/models.go +++ b/tables/models.go @@ -6,12 +6,12 @@ import ( // Table struct is a row record of the tables table in the database type Table struct { - utils.Table - 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 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"` + Schema utils.Table `json:"schema"` } type ShortTable struct { diff --git a/tables/repository.go b/tables/repository.go index c0a74a4..61cc430 100644 --- a/tables/repository.go +++ b/tables/repository.go @@ -26,7 +26,7 @@ func GetAllTablesRepository(ctx context.Context, projectId int64, db utils.Queri // convert the table schema to the table model for _, table := range tables { - table.Table = tableSchema[table.Name] + table.Schema = tableSchema[table.Name] } return tables, err diff --git a/tmp/air_errors.log b/tmp/air_errors.log index 9893abd..efad4cb 100644 --- a/tmp/air_errors.log +++ b/tmp/air_errors.log @@ -1 +1 @@ -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 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 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 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 +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 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 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 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 From c58604b0cb282906cfedcccbcc506cee306808cf Mon Sep 17 00:00:00 2001 From: rag-nar1 Date: Sat, 28 Jun 2025 11:49:25 +0300 Subject: [PATCH 006/101] refactor: update user ID type to int64 across multiple files and enhance database handling --- .gitignore | 2 +- AI/handlers.go | 2 +- AI/services.go | 2 +- config/application.go | 4 ++- middleware/middleware.go | 5 ++-- tables/handlers.go | 2 +- tables/models.go | 2 +- tables/repository.go | 50 +++++++++++++++++++++++++++++----- tables/service.go | 16 +++++------ test/tables/repository_test.go | 23 ++-------------- test/tables/service_test.go | 2 +- tmp/air_errors.log | 2 +- utils/database.go | 4 +-- utils/utils.go | 4 +-- 14 files changed, 71 insertions(+), 49 deletions(-) diff --git a/.gitignore b/.gitignore index 64e46af..42ff839 100644 --- a/.gitignore +++ b/.gitignore @@ -9,4 +9,4 @@ curl.sh .qodo *.log pr.md -response.md \ No newline at end of file +response.* \ No newline at end of file diff --git a/AI/handlers.go b/AI/handlers.go index 6b7c758..757f734 100644 --- a/AI/handlers.go +++ b/AI/handlers.go @@ -29,7 +29,7 @@ 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 diff --git a/AI/services.go b/AI/services.go index dde2bae..e05affd 100644 --- a/AI/services.go +++ b/AI/services.go @@ -8,7 +8,7 @@ import ( "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 := utils.ExtractDb(context.Background(), projectUUID, userID, config.DB) if err != nil { diff --git a/config/application.go b/config/application.go index 76f66e8..c063d5c 100644 --- a/config/application.go +++ b/config/application.go @@ -71,7 +71,9 @@ func loadEnv() { } } -const deploy = false +const ( + deploy = false +) func Init(infoLog, errorLog *log.Logger) { diff --git a/middleware/middleware.go b/middleware/middleware.go index c190bd0..4664201 100644 --- a/middleware/middleware.go +++ b/middleware/middleware.go @@ -41,13 +41,14 @@ 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) + userId := r.Context().Value("user-id").(int64) + config.App.InfoLog.Printf("Checking ownership for project %s and user %d", projectId, userId) ok, err := utils.CheckOwnershipQuery(r.Context(), projectId, userId, config.DB) if err != nil { response.InternalServerError(w, err.Error(), err) return } - + config.App.InfoLog.Printf("Ownership check for project %s by user %d: %t", projectId, userId, ok) if !ok { response.UnAuthorized(w, "UnAuthorized", nil) return diff --git a/tables/handlers.go b/tables/handlers.go index 4dc225e..8a743ad 100644 --- a/tables/handlers.go +++ b/tables/handlers.go @@ -29,7 +29,7 @@ import ( */ -// new body +// new body Table struct // GetAllTablesHanlder godoc // @Summary Get all tables in a project diff --git a/tables/models.go b/tables/models.go index 502d6d7..ba205ec 100644 --- a/tables/models.go +++ b/tables/models.go @@ -6,7 +6,7 @@ import ( // Table struct is a row record of the tables table in the database type Table struct { - ID int `json:"id" db:"id"` + 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"` diff --git a/tables/repository.go b/tables/repository.go index 61cc430..879496d 100644 --- a/tables/repository.go +++ b/tables/repository.go @@ -11,28 +11,64 @@ import ( "github.com/georgysavva/scany/v2/pgxscan" ) -func GetAllTablesRepository(ctx context.Context, projectId int64, db utils.Querier) ([]Table, error) { +func GetAllTablesRepository(ctx context.Context, projectId int64, userDb utils.Querier, servDb utils.Querier) ([]Table, error) { var tables []Table - err := pgxscan.Select(ctx, db, &tables, `SELECT oid, name FROM "Ptable" WHERE project_id = $1`, projectId) + err := pgxscan.Select(ctx, servDb, &tables, `SELECT id, oid, name FROM "Ptable" WHERE project_id = $1`, projectId) if err != nil { return nil, err } + // get the project db connection + + // extract the table schema - tableSchema, err := utils.GetTables(ctx, db) + 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 = append(tables[:i], tables[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 _, table := range tables { - table.Schema = tableSchema[table.Name] + 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) @@ -40,7 +76,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) diff --git a/tables/service.go b/tables/service.go index debb160..56d1b94 100644 --- a/tables/service.go +++ b/tables/service.go @@ -10,17 +10,17 @@ import ( ) func GetAllTables(ctx context.Context, projectOID string, servDb *pgxpool.Pool) ([]Table, error) { - userId, ok := ctx.Value("user-id").(int) + userId, ok := ctx.Value("user-id").(int64) if !ok || userId == 0 { return nil, response.ErrUnauthorized } - _, projectId, err := utils.GetProjectNameID(ctx, projectOID, servDb) + projectId, userDb, err := utils.ExtractDb(ctx, projectOID, userId, servDb) if err != nil { return nil, err } - tables, err := GetAllTablesRepository(ctx, projectId.(int64), servDb) + tables, err := GetAllTablesRepository(ctx, projectId, userDb, servDb) if err != nil { return nil, err } @@ -29,7 +29,7 @@ func GetAllTables(ctx context.Context, projectOID string, servDb *pgxpool.Pool) } func CreateTable(ctx context.Context, projectOID string, table *ClientTable, servDb *pgxpool.Pool) (string, error) { - userId, ok := ctx.Value("user-id").(int) + userId, ok := ctx.Value("user-id").(int64) if !ok || userId == 0 { return "", response.ErrUnauthorized } @@ -53,7 +53,7 @@ func CreateTable(ctx context.Context, projectOID string, table *ClientTable, ser ProjectID: projectId, OID: utils.GenerateOID(), } - var tableId int + var tableId int64 // insert table row into the tables table if err := InsertNewTable(ctx, &tableRecord, &tableId, servDb); err != nil { return "", err @@ -67,7 +67,7 @@ func CreateTable(ctx context.Context, projectOID string, table *ClientTable, ser } func UpdateTable(ctx context.Context, projectOID string, tableOID string, updates *TableUpdate, servDb *pgxpool.Pool) error { - userId, ok := ctx.Value("user-id").(int) + userId, ok := ctx.Value("user-id").(int64) if !ok || userId == 0 { return response.ErrUnauthorized } @@ -104,7 +104,7 @@ func UpdateTable(ctx context.Context, projectOID string, tableOID string, update } func DeletTable(ctx context.Context, projectOID, tableOID string, servDb *pgxpool.Pool) error { - userId, ok := ctx.Value("user-id").(int) + userId, ok := ctx.Value("user-id").(int64) if !ok || userId == 0 { return response.ErrUnauthorized } @@ -174,7 +174,7 @@ 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 } 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 efad4cb..5e50066 100644 --- a/tmp/air_errors.log +++ b/tmp/air_errors.log @@ -1 +1 @@ -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 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 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 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 +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 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 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 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 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 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 09b2ae5..19a5ef4 100644 --- a/utils/database.go +++ b/utils/database.go @@ -17,7 +17,7 @@ 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, projectId string, userId int64, db Querier) (bool, error) { var count int err := db.QueryRow(ctx, CheckOwnershipStmt, projectId, userId).Scan(&count) if err != nil { @@ -35,7 +35,7 @@ func GetProjectNameID(ctx context.Context, projectId string, db Querier) (interf return name, id, nil } -func ExtractDb(ctx context.Context, projectOID string, UserID int, servDb *pgxpool.Pool) (int64, *pgxpool.Pool, error) { +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 { diff --git a/utils/utils.go b/utils/utils.go index 15d6d95..97174c9 100644 --- a/utils/utils.go +++ b/utils/utils.go @@ -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) } From f2df990bbb5156ffd500f8ed77ecbd0101839ffd Mon Sep 17 00:00:00 2001 From: rag-nar1 Date: Sat, 28 Jun 2025 14:10:36 +0300 Subject: [PATCH 007/101] refactor: update table schema references and enhance JSON struct tags for consistency --- docs/docs.go | 74 +++++++++++++++++++------------------------- docs/swagger.json | 74 +++++++++++++++++++------------------------- docs/swagger.yaml | 68 +++++++++++++++++++--------------------- tables/handlers.go | 4 +-- tables/models.go | 4 +-- tables/repository.go | 8 ++--- tmp/air_errors.log | 2 +- utils/schema.go | 65 +++++++++++++++++++------------------- 8 files changed, 136 insertions(+), 163 deletions(-) diff --git a/docs/docs.go b/docs/docs.go index 0121284..a085813 100644 --- a/docs/docs.go +++ b/docs/docs.go @@ -224,7 +224,7 @@ const docTemplate = `{ "in": "body", "required": true, "schema": { - "$ref": "#/definitions/tables.ClientTable" + "$ref": "#/definitions/tables.Table" } } ], @@ -2122,20 +2122,6 @@ const docTemplate = `{ } } }, - "tables.ClientTable": { - "type": "object", - "properties": { - "columns": { - "type": "array", - "items": { - "$ref": "#/definitions/tables.Column" - } - }, - "tableName": { - "type": "string" - } - } - }, "tables.Column": { "type": "object", "properties": { @@ -2212,6 +2198,10 @@ const docTemplate = `{ }, "tables.Table": { "type": "object", + "required": [ + "name", + "schema" + ], "properties": { "description": { "type": "string" @@ -2267,28 +2257,28 @@ const docTemplate = `{ "utils.ConstraintInfo": { "type": "object", "properties": { - "checkClause": { + "CheckClause": { "type": "string" }, - "columnName": { + "ColumnName": { "type": "string" }, - "constraintName": { + "ConstraintName": { "type": "string" }, - "constraintType": { + "ConstraintType": { "type": "string" }, - "foreignColumnName": { + "ForeignColumnName": { "type": "string" }, - "foreignTableName": { + "ForeignTableName": { "type": "string" }, - "ordinalPosition": { + "OrdinalPosition": { "type": "integer" }, - "tableName": { + "TableName": { "type": "string" } } @@ -2296,22 +2286,22 @@ const docTemplate = `{ "utils.IndexInfo": { "type": "object", "properties": { - "columnName": { + "ColumnName": { "type": "string" }, - "indexName": { + "IndexName": { "type": "string" }, - "indexType": { + "IndexType": { "type": "string" }, - "isPrimary": { + "IsPrimary": { "type": "boolean" }, - "isUnique": { + "IsUnique": { "type": "boolean" }, - "tableName": { + "TableName": { "type": "string" } } @@ -2319,25 +2309,25 @@ const docTemplate = `{ "utils.Table": { "type": "object", "properties": { - "columns": { + "Columns": { "type": "array", "items": { "$ref": "#/definitions/utils.TableColumn" } }, - "constraints": { + "Constraints": { "type": "array", "items": { "$ref": "#/definitions/utils.ConstraintInfo" } }, - "indexes": { + "Indexes": { "type": "array", "items": { "$ref": "#/definitions/utils.IndexInfo" } }, - "tableName": { + "TableName": { "type": "string" } } @@ -2345,31 +2335,31 @@ const docTemplate = `{ "utils.TableColumn": { "type": "object", "properties": { - "characterMaximumLength": { + "CharacterMaximumLength": { "type": "integer" }, - "columnDefault": { + "ColumnDefault": { "type": "string" }, - "columnName": { + "ColumnName": { "type": "string" }, - "dataType": { + "DataType": { "type": "string" }, - "isNullable": { + "IsNullable": { "type": "boolean" }, - "numericPrecision": { + "NumericPrecision": { "type": "integer" }, - "numericScale": { + "NumericScale": { "type": "integer" }, - "ordinalPosition": { + "OrdinalPosition": { "type": "integer" }, - "tableName": { + "TableName": { "type": "string" } } diff --git a/docs/swagger.json b/docs/swagger.json index 8c78cbb..135e6c6 100644 --- a/docs/swagger.json +++ b/docs/swagger.json @@ -217,7 +217,7 @@ "in": "body", "required": true, "schema": { - "$ref": "#/definitions/tables.ClientTable" + "$ref": "#/definitions/tables.Table" } } ], @@ -2115,20 +2115,6 @@ } } }, - "tables.ClientTable": { - "type": "object", - "properties": { - "columns": { - "type": "array", - "items": { - "$ref": "#/definitions/tables.Column" - } - }, - "tableName": { - "type": "string" - } - } - }, "tables.Column": { "type": "object", "properties": { @@ -2205,6 +2191,10 @@ }, "tables.Table": { "type": "object", + "required": [ + "name", + "schema" + ], "properties": { "description": { "type": "string" @@ -2260,28 +2250,28 @@ "utils.ConstraintInfo": { "type": "object", "properties": { - "checkClause": { + "CheckClause": { "type": "string" }, - "columnName": { + "ColumnName": { "type": "string" }, - "constraintName": { + "ConstraintName": { "type": "string" }, - "constraintType": { + "ConstraintType": { "type": "string" }, - "foreignColumnName": { + "ForeignColumnName": { "type": "string" }, - "foreignTableName": { + "ForeignTableName": { "type": "string" }, - "ordinalPosition": { + "OrdinalPosition": { "type": "integer" }, - "tableName": { + "TableName": { "type": "string" } } @@ -2289,22 +2279,22 @@ "utils.IndexInfo": { "type": "object", "properties": { - "columnName": { + "ColumnName": { "type": "string" }, - "indexName": { + "IndexName": { "type": "string" }, - "indexType": { + "IndexType": { "type": "string" }, - "isPrimary": { + "IsPrimary": { "type": "boolean" }, - "isUnique": { + "IsUnique": { "type": "boolean" }, - "tableName": { + "TableName": { "type": "string" } } @@ -2312,25 +2302,25 @@ "utils.Table": { "type": "object", "properties": { - "columns": { + "Columns": { "type": "array", "items": { "$ref": "#/definitions/utils.TableColumn" } }, - "constraints": { + "Constraints": { "type": "array", "items": { "$ref": "#/definitions/utils.ConstraintInfo" } }, - "indexes": { + "Indexes": { "type": "array", "items": { "$ref": "#/definitions/utils.IndexInfo" } }, - "tableName": { + "TableName": { "type": "string" } } @@ -2338,31 +2328,31 @@ "utils.TableColumn": { "type": "object", "properties": { - "characterMaximumLength": { + "CharacterMaximumLength": { "type": "integer" }, - "columnDefault": { + "ColumnDefault": { "type": "string" }, - "columnName": { + "ColumnName": { "type": "string" }, - "dataType": { + "DataType": { "type": "string" }, - "isNullable": { + "IsNullable": { "type": "boolean" }, - "numericPrecision": { + "NumericPrecision": { "type": "integer" }, - "numericScale": { + "NumericScale": { "type": "integer" }, - "ordinalPosition": { + "OrdinalPosition": { "type": "integer" }, - "tableName": { + "TableName": { "type": "string" } } diff --git a/docs/swagger.yaml b/docs/swagger.yaml index 7824882..7f879ee 100644 --- a/docs/swagger.yaml +++ b/docs/swagger.yaml @@ -287,15 +287,6 @@ definitions: example: Operation successful type: string type: object - tables.ClientTable: - properties: - columns: - items: - $ref: '#/definitions/tables.Column' - type: array - tableName: - type: string - type: object tables.Column: properties: foreignKey: @@ -358,6 +349,9 @@ definitions: type: integer schema: $ref: '#/definitions/utils.Table' + required: + - name + - schema type: object tables.TableUpdate: properties: @@ -381,74 +375,74 @@ definitions: type: object utils.ConstraintInfo: properties: - checkClause: + CheckClause: type: string - columnName: + ColumnName: type: string - constraintName: + ConstraintName: type: string - constraintType: + ConstraintType: type: string - foreignColumnName: + ForeignColumnName: type: string - foreignTableName: + ForeignTableName: type: string - ordinalPosition: + OrdinalPosition: type: integer - tableName: + TableName: type: string type: object utils.IndexInfo: properties: - columnName: + ColumnName: type: string - indexName: + IndexName: type: string - indexType: + IndexType: type: string - isPrimary: + IsPrimary: type: boolean - isUnique: + IsUnique: type: boolean - tableName: + TableName: type: string type: object utils.Table: properties: - columns: + Columns: items: $ref: '#/definitions/utils.TableColumn' type: array - constraints: + Constraints: items: $ref: '#/definitions/utils.ConstraintInfo' type: array - indexes: + Indexes: items: $ref: '#/definitions/utils.IndexInfo' type: array - tableName: + TableName: type: string type: object utils.TableColumn: properties: - characterMaximumLength: + CharacterMaximumLength: type: integer - columnDefault: + ColumnDefault: type: string - columnName: + ColumnName: type: string - dataType: + DataType: type: string - isNullable: + IsNullable: type: boolean - numericPrecision: + NumericPrecision: type: integer - numericScale: + NumericScale: type: integer - ordinalPosition: + OrdinalPosition: type: integer - tableName: + TableName: type: string type: object info: @@ -584,7 +578,7 @@ paths: name: table required: true schema: - $ref: '#/definitions/tables.ClientTable' + $ref: '#/definitions/tables.Table' produces: - application/json responses: diff --git a/tables/handlers.go b/tables/handlers.go index 8a743ad..1a4a64b 100644 --- a/tables/handlers.go +++ b/tables/handlers.go @@ -74,7 +74,7 @@ func GetAllTablesHanlder(app *config.Application) http.HandlerFunc { // @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 @@ -84,7 +84,7 @@ func GetAllTablesHanlder(app *config.Application) http.HandlerFunc { 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{} // 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) diff --git a/tables/models.go b/tables/models.go index ba205ec..3d3e7ef 100644 --- a/tables/models.go +++ b/tables/models.go @@ -9,9 +9,9 @@ type Table struct { 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"` + Name string `json:"name" db:"name" validate:"required"` Description string `json:"description" db:"description"` - Schema utils.Table `json:"schema"` + Schema utils.Table `json:"schema" validate:"required"` } type ShortTable struct { diff --git a/tables/repository.go b/tables/repository.go index 879496d..4ec4867 100644 --- a/tables/repository.go +++ b/tables/repository.go @@ -5,6 +5,7 @@ import ( "DBHS/utils" "context" "fmt" + "slices" "strconv" "strings" @@ -17,10 +18,7 @@ func GetAllTablesRepository(ctx context.Context, projectId int64, userDb utils.Q if err != nil { return nil, err } - - // get the project db connection - - + // extract the table schema tableSchema, err := utils.GetTables(ctx, userDb) if err != nil { @@ -37,7 +35,7 @@ func GetAllTablesRepository(ctx context.Context, projectId int64, userDb utils.Q config.App.ErrorLog.Printf("Failed to delete table record %s: %v", tables[i].OID, err) } // remove the table from the list - tables = append(tables[:i], tables[i+1:]...) + tables = slices.Delete(tables, i, i+1) i-- // adjust index after removal } } diff --git a/tmp/air_errors.log b/tmp/air_errors.log index 5e50066..c2e77b2 100644 --- a/tmp/air_errors.log +++ b/tmp/air_errors.log @@ -1 +1 @@ -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 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 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 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 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 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 +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 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 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 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 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 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 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/schema.go b/utils/schema.go index a49d414..f9bc0ca 100644 --- a/utils/schema.go +++ b/utils/schema.go @@ -10,44 +10,44 @@ import ( // TableColumn represents a database column with its properties type TableColumn struct { - TableName string `db:"table_name"` - ColumnName string `db:"column_name"` - DataType string `db:"data_type"` - IsNullable bool `db:"is_nullable"` - ColumnDefault *string `db:"column_default"` - CharacterMaximumLength *int `db:"character_maximum_length"` - NumericPrecision *int `db:"numeric_precision"` - NumericScale *int `db:"numeric_scale"` - OrdinalPosition int `db:"ordinal_position"` + 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"` - ConstraintName string `db:"constraint_name"` - ConstraintType string `db:"constraint_type"` - ColumnName *string `db:"column_name"` - ForeignTableName *string `db:"foreign_table_name"` - ForeignColumnName *string `db:"foreign_column_name"` - CheckClause *string `db:"check_clause"` - OrdinalPosition *int `db:"ordinal_position"` + 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"` - IndexName string `db:"index_name"` - ColumnName string `db:"column_name"` - IsUnique bool `db:"is_unique"` - IndexType string `db:"index_type"` - IsPrimary bool `db:"is_primary"` + 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"` - Columns []TableColumn `db:"columns"` - Constraints []ConstraintInfo `db:"constraints"` - Indexes []IndexInfo `db:"indexes"` + 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"` } const ( @@ -138,9 +138,10 @@ const ( ORDER BY t.relname, i.relname;` ) + /* - the schema format for the tables is: - +the schema format for the tables is: + table_name: { columns: [ { @@ -212,10 +213,10 @@ func GetTables(ctx context.Context, db Querier) (map[string]Table, error) { tablesMap := make(map[string]Table) for tableName, columns := range tableColumns { tablesMap[tableName] = Table{ - TableName: tableName, - Columns: columns, + TableName: tableName, + Columns: columns, Constraints: tableConstraints[tableName], - Indexes: tableIndexes[tableName], + Indexes: tableIndexes[tableName], } } From 985b6db2f4f3f16833fa07f87fcd8157f0f6760c Mon Sep 17 00:00:00 2001 From: rag-nar1 Date: Sat, 28 Jun 2025 17:45:30 +0300 Subject: [PATCH 008/101] refactor: update table handling to use new Table struct and enhance DDL generation --- .gitignore | 2 +- playground | 1 + tables/handlers.go | 1 - tables/service.go | 17 ++++++++--------- tables/utils.go | 12 ++++++------ tmp/air_errors.log | 2 +- utils/schema.go | 17 ++++++++++++++++- 7 files changed, 33 insertions(+), 19 deletions(-) create mode 160000 playground diff --git a/.gitignore b/.gitignore index 42ff839..89f186b 100644 --- a/.gitignore +++ b/.gitignore @@ -2,7 +2,7 @@ build/* *.env .idea .vscode -playground +playground/* tmp/main* *.log curl.sh diff --git a/playground b/playground new file mode 160000 index 0000000..d62f18f --- /dev/null +++ b/playground @@ -0,0 +1 @@ +Subproject commit d62f18fc45426366e2dff67b6d88b65b2cbb777b diff --git a/tables/handlers.go b/tables/handlers.go index 1a4a64b..f6bbf2f 100644 --- a/tables/handlers.go +++ b/tables/handlers.go @@ -104,7 +104,6 @@ func CreateTableHandler(app *config.Application) http.HandlerFunc { response.BadRequest(w, "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 { diff --git a/tables/service.go b/tables/service.go index 56d1b94..f30a5fe 100644 --- a/tables/service.go +++ b/tables/service.go @@ -28,7 +28,7 @@ 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) { +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 @@ -48,22 +48,21 @@ 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(), - } + + 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 { diff --git a/tables/utils.go b/tables/utils.go index d3b7f96..c62f086 100644 --- a/tables/utils.go +++ b/tables/utils.go @@ -36,8 +36,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 } @@ -49,12 +49,12 @@ 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 } } diff --git a/tmp/air_errors.log b/tmp/air_errors.log index c2e77b2..355be32 100644 --- a/tmp/air_errors.log +++ b/tmp/air_errors.log @@ -1 +1 @@ -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 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 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 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 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 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 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 +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 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 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 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 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 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 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 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 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/schema.go b/utils/schema.go index f9bc0ca..2dce96c 100644 --- a/utils/schema.go +++ b/utils/schema.go @@ -337,6 +337,21 @@ func ExtractDatabaseSchema(ctx context.Context, db Querier) (string, error) { 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 @@ -402,7 +417,7 @@ func generateCreateTableStatement(tableName string, columns []TableColumn, const // Add CHECK constraints for _, check := range checkConstraints { if check.CheckClause != nil { - columnDefs = append(columnDefs, fmt.Sprintf(" CONSTRAINT \"%s\" CHECK %s", check.ConstraintName, *check.CheckClause)) + columnDefs = append(columnDefs, fmt.Sprintf(" CONSTRAINT \"%s\" CHECK (%s)", check.ConstraintName, *check.CheckClause)) } } From b766c8f804b20aad4d6d1f1bd1f78a78bd8ea9d6 Mon Sep 17 00:00:00 2001 From: rag-nar1 Date: Sun, 29 Jun 2025 08:37:41 +0300 Subject: [PATCH 009/101] feat: added the DDL statement gnerator fot diff between two tables --- playground | 1 - 1 file changed, 1 deletion(-) delete mode 160000 playground diff --git a/playground b/playground deleted file mode 160000 index d62f18f..0000000 --- a/playground +++ /dev/null @@ -1 +0,0 @@ -Subproject commit d62f18fc45426366e2dff67b6d88b65b2cbb777b From a458abb226b7c3f1bfa88b577af88445edb42877 Mon Sep 17 00:00:00 2001 From: rag-nar1 Date: Sun, 29 Jun 2025 08:37:52 +0300 Subject: [PATCH 010/101] feat: add UpdateTableSchema struct and RenameRelation type for schema updates --- playground | 1 + tables/models.go | 7 ++ utils/schema.go | 216 ++++++++++++++++++++++++++++++++++++++++++++++- 3 files changed, 221 insertions(+), 3 deletions(-) create mode 160000 playground diff --git a/playground b/playground new file mode 160000 index 0000000..c338ac6 --- /dev/null +++ b/playground @@ -0,0 +1 @@ +Subproject commit c338ac6de7236514a0d175881dec22324a97c2f7 diff --git a/tables/models.go b/tables/models.go index 3d3e7ef..2eb29a0 100644 --- a/tables/models.go +++ b/tables/models.go @@ -14,6 +14,13 @@ type Table struct { 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"` diff --git a/utils/schema.go b/utils/schema.go index 2dce96c..9b46311 100644 --- a/utils/schema.go +++ b/utils/schema.go @@ -50,6 +50,11 @@ type Table struct { 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 = ` @@ -327,7 +332,7 @@ func ExtractDatabaseSchema(ctx context.Context, db Querier) (string, error) { // Add indexes for this table if idxs, exists := tableIndexes[tableName]; exists { for _, index := range idxs { - ddlStatements.WriteString(generateCreateIndexStatement(index)) + ddlStatements.WriteString(generateCreateIndexStatement(&index)) ddlStatements.WriteString("\n") } } @@ -346,7 +351,7 @@ func GenerateCreateTableDDL(table *Table) (string, error) { // Add indexes for this table ddlStatements.WriteString("\n-- Indexes\n") for _, index := range table.Indexes { - ddlStatements.WriteString(generateCreateIndexStatement(index)) + ddlStatements.WriteString(generateCreateIndexStatement(&index)) ddlStatements.WriteString("\n") } return ddlStatements.String(), nil @@ -428,7 +433,7 @@ func generateCreateTableStatement(tableName string, columns []TableColumn, const } // generateCreateIndexStatement creates a CREATE INDEX DDL statement -func generateCreateIndexStatement(index IndexInfo) string { +func generateCreateIndexStatement(index *IndexInfo) string { indexType := "" if index.IsUnique { indexType = "UNIQUE " @@ -464,3 +469,208 @@ func formatDataType(col TableColumn) string { 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 From 9200183e28137724236fc0fbe6944b402eae2959 Mon Sep 17 00:00:00 2001 From: rag-nar1 Date: Sun, 29 Jun 2025 08:38:22 +0300 Subject: [PATCH 011/101] remove playground directory --- playground | 1 - 1 file changed, 1 deletion(-) delete mode 160000 playground diff --git a/playground b/playground deleted file mode 160000 index c338ac6..0000000 --- a/playground +++ /dev/null @@ -1 +0,0 @@ -Subproject commit c338ac6de7236514a0d175881dec22324a97c2f7 From 742b5c51ebb453c9145e941a3c19dffccfbea570 Mon Sep 17 00:00:00 2001 From: rag-nar1 Date: Sun, 29 Jun 2025 09:51:05 +0300 Subject: [PATCH 012/101] feat: implement table schema synchronization and enhance ownership checks --- middleware/middleware.go | 23 +++++- tables/handlers.go | 36 +++------ tables/middleware.go | 41 ++++++++++ tables/routes.go | 2 +- tables/service.go | 19 +++-- tables/utils.go | 52 ++++++++++++ utils/database.go | 18 ++++- utils/schema.go | 170 ++++++++++++++++++++++++++++++++++++++- utils/utils.go | 2 +- 9 files changed, 320 insertions(+), 43 deletions(-) create mode 100644 tables/middleware.go diff --git a/middleware/middleware.go b/middleware/middleware.go index 4664201..9a6eac8 100644 --- a/middleware/middleware.go +++ b/middleware/middleware.go @@ -40,20 +40,35 @@ 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"] + projectOID := urlVariables["project_id"] userId := r.Context().Value("user-id").(int64) - config.App.InfoLog.Printf("Checking ownership for project %s and user %d", projectId, userId) - ok, err := utils.CheckOwnershipQuery(r.Context(), projectId, userId, config.DB) + config.App.InfoLog.Printf("Checking ownership for project %s and user %d", projectOID, userId) + ok, err := utils.CheckOwnershipQuery(r.Context(), projectOID, userId, config.DB) if err != nil { response.InternalServerError(w, err.Error(), err) return } - config.App.InfoLog.Printf("Ownership check for project %s by user %d: %t", projectId, userId, ok) + config.App.InfoLog.Printf("Ownership check for project %s by user %d: %t", projectOID, userId, ok) if !ok { response.UnAuthorized(w, "UnAuthorized", nil) return } + tableOID := urlVariables["table_id"] + if tableOID != "" { + //check if the table belongs to the project + ok, err = utils.CheckOwnershipQueryTable(r.Context(), tableOID, projectOID, config.DB) + if err != nil { + response.InternalServerError(w, err.Error(), err) + return + } + + if !ok { + response.NotFound(w, "Table not found", nil) + return + } + } + next.ServeHTTP(w, r) }) } diff --git a/tables/handlers.go b/tables/handlers.go index f6bbf2f..7ffadbf 100644 --- a/tables/handlers.go +++ b/tables/handlers.go @@ -122,25 +122,6 @@ func CreateTableHandler(app *config.Application) http.HandlerFunc { } } -/* - "insert": { - "columns" : [ - - ] - }, - "update": [ - { - "oldName": "oldName", - "columns": [ - // Only include the changed parts - ] - } - ], - "delete": [ - "columnName1", - "columnName2" - ] -*/ // UpdateTableHandler godoc // @Summary Update an existing table @@ -150,17 +131,18 @@ 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) @@ -169,15 +151,15 @@ func UpdateTableHandler(app *config.Application) http.HandlerFunc { // 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 == "" { + if projectOID == "" || tableId == "" { response.BadRequest(w, "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) return diff --git a/tables/middleware.go b/tables/middleware.go new file mode 100644 index 0000000..f005a6b --- /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, "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, "Unauthorized", nil) + return + } + + // Get user database connection + projectId, userDb, err := utils.ExtractDb(r.Context(), projectOID, userId, config.DB) + if err != nil { + response.InternalServerError(w, "Failed to extract database connection", err) + return + } + + if err := SyncTableSchemas(r.Context(), projectId, config.DB, userDb); err != nil { + response.InternalServerError(w, err.Error(), err) + return + } + next.ServeHTTP(w, r) + }) +} \ No newline at end of file diff --git a/tables/routes.go b/tables/routes.go index 257f254..12f33d0 100644 --- a/tables/routes.go +++ b/tables/routes.go @@ -20,7 +20,7 @@ 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) router.Handle("", middleware.Route(map[string]http.HandlerFunc{ http.MethodPost: CreateTableHandler(config.App), diff --git a/tables/service.go b/tables/service.go index f30a5fe..660407b 100644 --- a/tables/service.go +++ b/tables/service.go @@ -65,7 +65,7 @@ func CreateTable(ctx context.Context, projectOID string, table *Table, servDb *p return table.OID, nil } -func UpdateTable(ctx context.Context, projectOID string, tableOID string, updates *TableUpdate, servDb *pgxpool.Pool) error { +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 response.ErrUnauthorized @@ -76,13 +76,12 @@ func UpdateTable(ctx context.Context, projectOID string, tableOID string, update 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 } @@ -92,9 +91,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 } diff --git a/tables/utils.go b/tables/utils.go index c62f086..783f415 100644 --- a/tables/utils.go +++ b/tables/utils.go @@ -1,12 +1,15 @@ package tables import ( + "DBHS/config" "DBHS/utils" "context" "fmt" "log" + "slices" "strings" + "github.com/georgysavva/scany/v2/pgxscan" "github.com/jackc/pgx/v5" ) @@ -132,3 +135,52 @@ 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 +} \ No newline at end of file diff --git a/utils/database.go b/utils/database.go index 19a5ef4..76cf917 100644 --- a/utils/database.go +++ b/utils/database.go @@ -10,6 +10,7 @@ import ( var ( CheckOwnershipStmt = `SELECT COUNT(*) FROM "projects" WHERE oid = $1 AND owner_id = $2;` + CheckOwnershipTableStmt = `SELECT COUNT(*) FROM "Ptable" WHERE oid = $1 AND project_id = $2;` ) func UpdateDataInDatabase(ctx context.Context, db Querier, query string, dest ...interface{}) error { @@ -17,18 +18,27 @@ func UpdateDataInDatabase(ctx context.Context, db Querier, query string, dest .. return err } -func CheckOwnershipQuery(ctx context.Context, projectId string, userId int64, 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, projectId, userId).Scan(&count) + 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 GetProjectNameID(ctx context.Context, projectId string, db Querier) (interface{}, interface{}, error) { +func CheckOwnershipQueryTable(ctx context.Context, tableOID string, projectOID string, db Querier) (bool, error) { + var count int + err := db.QueryRow(ctx, CheckOwnershipTableStmt, tableOID, projectOID).Scan(&count) + if err != nil { + return false, fmt.Errorf("failed to check ownership: %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", projectId).Scan(&id, &name) + err := db.QueryRow(ctx, "SELECT id, name FROM projects WHERE oid = $1", projectOID).Scan(&id, &name) if err != nil { return nil, nil, err } diff --git a/utils/schema.go b/utils/schema.go index 9b46311..02ddec5 100644 --- a/utils/schema.go +++ b/utils/schema.go @@ -142,6 +142,89 @@ const ( ) 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;` ) /* @@ -228,6 +311,91 @@ func GetTables(ctx context.Context, db Querier) (map[string]Table, error) { 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) + } + + 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, db Querier, tableName string) (*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) @@ -492,7 +660,7 @@ func formatConstraint(constraint *ConstraintInfo) 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) { +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 diff --git a/utils/utils.go b/utils/utils.go index 97174c9..e6db941 100644 --- a/utils/utils.go +++ b/utils/utils.go @@ -132,4 +132,4 @@ func ReplaceWhiteSpacesWithUnderscore(str string) string { func UserServerDbFormat(dbname string, userId int64) string { dbname = strings.ToLower(dbname) return dbname + "_" + strconv.FormatInt(userId, 10) -} +} \ No newline at end of file From 5538ecc639420a26622f7c12a7bac2f7d1260fdb Mon Sep 17 00:00:00 2001 From: rag-nar1 Date: Sun, 29 Jun 2025 10:15:12 +0300 Subject: [PATCH 013/101] refactor: remove old body comments and update to new Table struct --- tables/handlers.go | 20 -------------------- 1 file changed, 20 deletions(-) diff --git a/tables/handlers.go b/tables/handlers.go index 7ffadbf..7cda309 100644 --- a/tables/handlers.go +++ b/tables/handlers.go @@ -9,27 +9,7 @@ import ( "github.com/gorilla/mux" ) -// old body -/* - body - tableName: "", - cols: [ - { - name: "", - type: "", - isUnique: "", - isNullable: "", - isPrimaryKey: "", - foriegnKey: { - tableName: "", - columnName: "", - }, - } - ] - -*/ -// new body Table struct // GetAllTablesHanlder godoc // @Summary Get all tables in a project From 8e25ae11d3dfd23732ea747c870d9b8bbea44316 Mon Sep 17 00:00:00 2001 From: OmarAlaraby Date: Sun, 29 Jun 2025 19:55:17 +0300 Subject: [PATCH 014/101] first version for the chatbot (not working) --- AI/handlers.go | 59 +++++++++++++++++++++++++++++++++++++++++++--- AI/models.go | 17 ++++++++++++- AI/repository.go | 49 ++++++++++++++++++++++++++++++++++++++ AI/routes.go | 1 + AI/services.go | 42 +++++++++++++++++++++++++++++++++ go.mod | 2 +- go.sum | 4 ++-- tmp/air_errors.log | 2 +- 8 files changed, 168 insertions(+), 8 deletions(-) diff --git a/AI/handlers.go b/AI/handlers.go index 6b7c758..ce93113 100644 --- a/AI/handlers.go +++ b/AI/handlers.go @@ -3,15 +3,15 @@ package ai import ( "DBHS/config" "DBHS/response" - "net/http" + "encoding/json" "github.com/gorilla/mux" + "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 @@ -42,4 +42,57 @@ func Report(app *config.Application) http.HandlerFunc { response.OK(w, "Report generated successfully", report) } -} \ No newline at end of file +} + +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, "Failed to get project ID", err) + return + } + userID := r.Context().Value("user-id").(int) + + var userRequest ChatBotRequest + if err := json.NewDecoder(r.Body).Decode(&userRequest); err != nil { + response.BadRequest(w, "Invalid request body", err) + return + } + + transaction, err := config.DB.Begin(r.Context()) + if err != nil { + app.ErrorLog.Println(err.Error()) + response.InternalServerError(w, "Failed to start database transaction", err) + return + } + + chat_data, err := GetOrCreateChatData(r.Context(), transaction, userID, projectID) + if err != nil { + response.InternalServerError(w, "Failed to get or create chat data", err) + return + } + + answer, err := config.AI.QueryChat(userRequest.Question) + if err != nil { + response.InternalServerError(w, err.Error(), err) + return + } + + err = SaveChatAction(r.Context(), transaction, chat_data.ID, userID, userRequest.Question, answer.ResponseText) + if err != nil { + response.InternalServerError(w, err.Error(), err) + return + } + + if err := transaction.Commit(r.Context()); err != nil { + app.ErrorLog.Println(err.Error()) + response.InternalServerError(w, "Failed to commit database transaction", err) + return + } + + response.OK(w, "Answer generated successfully", answer) + } +} diff --git a/AI/models.go b/AI/models.go index 452786f..2c906fe 100644 --- a/AI/models.go +++ b/AI/models.go @@ -1,3 +1,18 @@ package ai -type Analytics struct {} \ No newline at end of file +var SENDER_TYPE_AI string = "ai" +var SENDER_TYPE_USER string = "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"` +} \ 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..9cec5ce 100644 --- a/AI/routes.go +++ b/AI/routes.go @@ -11,4 +11,5 @@ 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))) } \ No newline at end of file diff --git a/AI/services.go b/AI/services.go index 839806c..6baa3e4 100644 --- a/AI/services.go +++ b/AI/services.go @@ -3,9 +3,12 @@ package ai import ( "DBHS/config" "DBHS/tables" + "DBHS/utils" "context" "encoding/json" + "github.com/Database-Hosting-Services/AI-Agent/RAG" + "github.com/jackc/pgx/v5" ) func getReport(projectUUID string, userID int, analytics Analytics, AI RAG.RAGmodel) (string, error) { @@ -35,3 +38,42 @@ 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 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 +} \ No newline at end of file diff --git a/go.mod b/go.mod index 0931b3a..69f06f1 100644 --- a/go.mod +++ b/go.mod @@ -6,7 +6,7 @@ go 1.24.0 // 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.5 github.com/MarceloPetrucio/go-scalar-api-reference v0.0.0-20240521013641-ce5d2efe0e06 github.com/georgysavva/scany/v2 v2.1.4 github.com/golang-jwt/jwt/v5 v5.2.1 diff --git a/go.sum b/go.sum index f536999..cff808e 100644 --- a/go.sum +++ b/go.sum @@ -12,8 +12,8 @@ cloud.google.com/go/compute/metadata v0.6.0/go.mod h1:FjyFAW1MW0C203CEOMDTu3Dk1F 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.5 h1:WzSdMIVWCzNorlP8IwnlnOJQ8dGeK1rcwBteIqq0eec= +github.com/Database-Hosting-Services/AI-Agent v1.0.5/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= diff --git a/tmp/air_errors.log b/tmp/air_errors.log index 2f95017..c215497 100644 --- a/tmp/air_errors.log +++ b/tmp/air_errors.log @@ -1 +1 @@ -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 +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 1exit status 1exit status 1 \ No newline at end of file From 9f9fc9f887b2b02cd63152a637977c52ac6cd49a Mon Sep 17 00:00:00 2001 From: rag-nar1 Date: Mon, 30 Jun 2025 09:44:01 +0300 Subject: [PATCH 015/101] feat: enhance ownership checks and add CheckOTableExist middleware for table validation --- middleware/middleware.go | 10 ++++++++-- tables/handlers.go | 15 ++++++++------- tables/routes.go | 2 +- tables/service.go | 4 +++- 4 files changed, 20 insertions(+), 11 deletions(-) diff --git a/middleware/middleware.go b/middleware/middleware.go index 9a6eac8..3e89467 100644 --- a/middleware/middleware.go +++ b/middleware/middleware.go @@ -53,11 +53,18 @@ func CheckOwnership(next http.Handler) http.Handler { response.UnAuthorized(w, "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 != "" { //check if the table belongs to the project - ok, err = utils.CheckOwnershipQueryTable(r.Context(), tableOID, projectOID, config.DB) + ok, err := utils.CheckOwnershipQueryTable(r.Context(), tableOID, projectOID, config.DB) if err != nil { response.InternalServerError(w, err.Error(), err) return @@ -68,7 +75,6 @@ func CheckOwnership(next http.Handler) http.Handler { return } } - next.ServeHTTP(w, r) }) } diff --git a/tables/handlers.go b/tables/handlers.go index 7cda309..c4287eb 100644 --- a/tables/handlers.go +++ b/tables/handlers.go @@ -162,21 +162,22 @@ 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 == "" { + projectOID := urlVariables["project_id"] + tableOID := urlVariables["table_id"] + if projectOID == "" || tableOID == "" { response.BadRequest(w, "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) return diff --git a/tables/routes.go b/tables/routes.go index 12f33d0..84d7be3 100644 --- a/tables/routes.go +++ b/tables/routes.go @@ -20,7 +20,7 @@ import ( func DefineURLs() { router := config.Router.PathPrefix("/api/projects/{project_id}/tables").Subrouter() - router.Use(middleware.JwtAuthMiddleware, middleware.CheckOwnership, SyncTables) + router.Use(middleware.JwtAuthMiddleware, middleware.CheckOwnership, SyncTables, middleware.CheckOTableExist) router.Handle("", middleware.Route(map[string]http.HandlerFunc{ http.MethodPost: CreateTableHandler(config.App), diff --git a/tables/service.go b/tables/service.go index 660407b..50c5c8f 100644 --- a/tables/service.go +++ b/tables/service.go @@ -111,7 +111,7 @@ func UpdateTable(ctx context.Context, projectOID string, tableOID string, newSch return nil } -func DeletTable(ctx context.Context, projectOID, tableOID string, servDb *pgxpool.Pool) error { +func DeleteTable(ctx context.Context, projectOID, tableOID string, servDb *pgxpool.Pool) error { userId, ok := ctx.Value("user-id").(int64) if !ok || userId == 0 { return response.ErrUnauthorized @@ -126,6 +126,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 @@ -141,6 +142,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 } From b311f1909f596c1b8d13e8b81647d777fe9097fb Mon Sep 17 00:00:00 2001 From: rag-nar1 Date: Mon, 30 Jun 2025 09:52:03 +0300 Subject: [PATCH 016/101] feat: add GetTableSchemaHandler and update table schema handling in models and utils --- tables/handlers.go | 27 ++++++++++++++++++++++++++- tables/models.go | 2 +- tables/routes.go | 8 +++++++- tables/service.go | 32 ++++++++++++++++++++++++++++++-- tables/utils.go | 2 +- utils/schema.go | 8 ++++---- 6 files changed, 69 insertions(+), 10 deletions(-) diff --git a/tables/handlers.go b/tables/handlers.go index c4287eb..76c0775 100644 --- a/tables/handlers.go +++ b/tables/handlers.go @@ -23,7 +23,7 @@ import ( // @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"] @@ -47,6 +47,31 @@ func GetAllTablesHanlder(app *config.Application) http.HandlerFunc { } } +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, "Project ID and Table ID are required", nil) + return + } + + data, err := GetTableSchema(r.Context(), projectId, tableId, config.DB) + if err != nil { + if errors.Is(err, response.ErrUnauthorized) { + response.UnAuthorized(w, "Unauthorized", nil) + return + } + app.ErrorLog.Println("Could not read table schema:", err) + response.InternalServerError(w, "Could not read table schema", err) + return + } + + response.OK(w, "Table Schema Read Successfully", data) + } +} + // CreateTableHandler godoc // @Summary Create a new table // @Description Create a new table in the specified project diff --git a/tables/models.go b/tables/models.go index 2eb29a0..9d7e212 100644 --- a/tables/models.go +++ b/tables/models.go @@ -11,7 +11,7 @@ type Table struct { 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"` + Schema *utils.Table `json:"schema" validate:"required"` } type UpdateTableSchema struct { diff --git a/tables/routes.go b/tables/routes.go index 84d7be3..ff0128c 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}? @@ -24,13 +25,18 @@ func DefineURLs() { 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), })) + + 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 50c5c8f..83c6f86 100644 --- a/tables/service.go +++ b/tables/service.go @@ -28,6 +28,34 @@ func GetAllTables(ctx context.Context, projectOID string, servDb *pgxpool.Pool) return tables, nil } +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 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 + } + + 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 { @@ -81,7 +109,7 @@ func UpdateTable(ctx context.Context, projectOID string, tableOID string, newSch return err } - DDLUpdate, err := utils.CompareTableSchemas(oldSchema, &newSchema.Schema, newSchema.Renames) + DDLUpdate, err := utils.CompareTableSchemas(oldSchema, newSchema.Schema, newSchema.Renames) if err != nil { return err } @@ -142,7 +170,7 @@ func DeleteTable(ctx context.Context, projectOID, tableOID string, servDb *pgxpo return err } defer servtx.Rollback(ctx) - + if err := DeleteTableFromServerDb(ctx, tableOID, servtx); err != nil { return err } diff --git a/tables/utils.go b/tables/utils.go index 783f415..bf0f0b9 100644 --- a/tables/utils.go +++ b/tables/utils.go @@ -40,7 +40,7 @@ func ParseTableIntoSQLCreate(table *ClientTable) (string, error) { } func CreateTableIntoHostingServer(ctx context.Context, table *Table, tx pgx.Tx) error { - DDLQuery, err := utils.GenerateCreateTableDDL(&table.Schema) + DDLQuery, err := utils.GenerateCreateTableDDL(table.Schema) if err != nil { return err } diff --git a/utils/schema.go b/utils/schema.go index 02ddec5..871c04d 100644 --- a/utils/schema.go +++ b/utils/schema.go @@ -257,7 +257,7 @@ the schema format for the tables is: ] } */ -func GetTables(ctx context.Context, db Querier) (map[string]Table, error) { +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 { @@ -298,9 +298,9 @@ func GetTables(ctx context.Context, db Querier) (map[string]Table, error) { tableIndexes[index.TableName] = append(tableIndexes[index.TableName], index) } - tablesMap := make(map[string]Table) + tablesMap := make(map[string]*Table) for tableName, columns := range tableColumns { - tablesMap[tableName] = Table{ + tablesMap[tableName] = &Table{ TableName: tableName, Columns: columns, Constraints: tableConstraints[tableName], @@ -369,7 +369,7 @@ func GetTableIndexes(ctx context.Context, tableName string, db Querier) ([]Index return indexes, nil } -func GetTable(ctx context.Context, db Querier, tableName string) (*Table, error) { +func GetTable(ctx context.Context, tableName string, db Querier) (*Table, error) { // Get the table schema tableSchema, err := GetTableSchema(ctx, tableName, db) if err != nil { From 556e743f6c5dad5f37c5afaad24161f9689df990 Mon Sep 17 00:00:00 2001 From: rag-nar1 Date: Mon, 30 Jun 2025 09:53:04 +0300 Subject: [PATCH 017/101] fix: correct GetAllTablesHanlder typo and add GetTableSchemaHandler documentation --- tables/handlers.go | 16 +++++++++++++++- 1 file changed, 15 insertions(+), 1 deletion(-) diff --git a/tables/handlers.go b/tables/handlers.go index 76c0775..29f867c 100644 --- a/tables/handlers.go +++ b/tables/handlers.go @@ -11,7 +11,7 @@ import ( ) -// GetAllTablesHanlder godoc +// GetAllTablesHandler godoc // @Summary Get all tables in a project // @Description Get a list of all tables in the specified project // @Tags tables @@ -47,6 +47,20 @@ func GetAllTablesHandler(app *config.Application) http.HandlerFunc { } } +// 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) From e9c786706d8ca906fff2246dfc9bd7f438068989 Mon Sep 17 00:00:00 2001 From: rag-nar1 Date: Mon, 30 Jun 2025 10:47:34 +0300 Subject: [PATCH 018/101] Add AgentAccept handler and update routes for agent acceptance --- AI/handlers.go | 6 ++++++ AI/routes.go | 1 + 2 files changed, 7 insertions(+) diff --git a/AI/handlers.go b/AI/handlers.go index d50043c..071c438 100644 --- a/AI/handlers.go +++ b/AI/handlers.go @@ -85,4 +85,10 @@ func Agent(app *config.Application) http.HandlerFunc { response.OK(w, "Agent query successful", AIresponse) } +} + +func AgentAccept(app *config.Application) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + + } } \ No newline at end of file diff --git a/AI/routes.go b/AI/routes.go index 4739689..4ebe01b 100644 --- a/AI/routes.go +++ b/AI/routes.go @@ -12,4 +12,5 @@ func DefineURLs() { AIProtected.Handle("/report", middleware.MethodsAllowed(http.MethodGet)(Report(config.App))) AIProtected.Handle("/agent", middleware.MethodsAllowed(http.MethodPost)(Agent(config.App))) + AIProtected.Handle("/agent/accept", middleware.MethodsAllowed(http.MethodPost)(Agent(config.App))) } \ No newline at end of file From d052f3bfb9099a6eb79e07098f57195a888a9642 Mon Sep 17 00:00:00 2001 From: rag-nar1 Date: Mon, 30 Jun 2025 14:00:49 +0300 Subject: [PATCH 019/101] Update Swagger documentation and Go module dependencies - Modified Swagger JSON and YAML files to reflect new table schema updates and error response structures. - Added new endpoint for retrieving table schema by project and table IDs. - Updated definitions for table updates and removed obsolete column definitions. - Added new utility definitions for renaming relations. - Updated Go module dependencies, including upgrading Axiom Go and OpenTelemetry packages. - Fixed indirect dependencies and ensured compatibility with the latest versions. --- AI/handlers.go | 24 +++++ config/application.go | 11 +++ docs/docs.go | 203 +++++++++++++++++++++++++++--------------- docs/swagger.json | 203 +++++++++++++++++++++++++++--------------- docs/swagger.yaml | 141 ++++++++++++++++++----------- go.mod | 17 ++-- go.sum | 63 +++++++++---- tmp/air_errors.log | 1 + 8 files changed, 439 insertions(+), 224 deletions(-) diff --git a/AI/handlers.go b/AI/handlers.go index 757f734..d82ed5f 100644 --- a/AI/handlers.go +++ b/AI/handlers.go @@ -4,6 +4,10 @@ import ( "DBHS/config" "DBHS/response" "net/http" + "time" + + "github.com/axiomhq/axiom-go/axiom" + "github.com/axiomhq/axiom-go/axiom/ingest" "github.com/gorilla/mux" ) @@ -37,9 +41,29 @@ func Report(app *config.Application) http.HandlerFunc { report, err := getReport(projectID, userID, Analytics, AI) if err != nil { response.InternalServerError(w, 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, "Report generated successfully", report) } } \ No newline at end of file diff --git a/config/application.go b/config/application.go index c063d5c..7bf8857 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() { @@ -195,6 +198,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() { diff --git a/docs/docs.go b/docs/docs.go index 2f30bba..c00100a 100644 --- a/docs/docs.go +++ b/docs/docs.go @@ -391,12 +391,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 +410,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" } } } @@ -467,19 +473,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" } } } @@ -2224,40 +2313,6 @@ const docTemplate = `{ } } }, - "tables.Column": { - "type": "object", - "properties": { - "foreignKey": { - "$ref": "#/definitions/tables.ForeignKey" - }, - "isNullable": { - "type": "boolean" - }, - "isPrimaryKey": { - "type": "boolean" - }, - "isUnique": { - "type": "boolean" - }, - "name": { - "type": "string" - }, - "type": { - "type": "string" - } - } - }, - "tables.ColumnCollection": { - "type": "object", - "properties": { - "columns": { - "type": "array", - "items": { - "$ref": "#/definitions/tables.Column" - } - } - } - }, "tables.Data": { "type": "object", "properties": { @@ -2276,17 +2331,6 @@ const docTemplate = `{ } } }, - "tables.ForeignKey": { - "type": "object", - "properties": { - "columnName": { - "type": "string" - }, - "tableName": { - "type": "string" - } - } - }, "tables.ShowColumn": { "type": "object", "properties": { @@ -2325,34 +2369,36 @@ const docTemplate = `{ } } }, - "tables.TableUpdate": { + "tables.UpdateTableSchema": { "type": "object", + "required": [ + "name", + "schema" + ], "properties": { - "delete": { - "type": "array", - "items": { - "type": "string" - } + "description": { + "type": "string" + }, + "id": { + "type": "integer" }, - "insert": { - "$ref": "#/definitions/tables.ColumnCollection" + "name": { + "type": "string" + }, + "oid": { + "type": "string" }, - "update": { + "project_id": { + "type": "integer" + }, + "renames": { "type": "array", "items": { - "$ref": "#/definitions/tables.UpdateColumn" + "$ref": "#/definitions/utils.RenameRelation" } - } - } - }, - "tables.UpdateColumn": { - "type": "object", - "properties": { - "name": { - "type": "string" }, - "update": { - "$ref": "#/definitions/tables.Column" + "schema": { + "$ref": "#/definitions/utils.Table" } } }, @@ -2408,6 +2454,17 @@ const docTemplate = `{ } } }, + "utils.RenameRelation": { + "type": "object", + "properties": { + "newName": { + "type": "string" + }, + "oldName": { + "type": "string" + } + } + }, "utils.Table": { "type": "object", "properties": { diff --git a/docs/swagger.json b/docs/swagger.json index 3c1e20b..4a89711 100644 --- a/docs/swagger.json +++ b/docs/swagger.json @@ -384,12 +384,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 +403,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" } } } @@ -460,19 +466,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" } } } @@ -2217,40 +2306,6 @@ } } }, - "tables.Column": { - "type": "object", - "properties": { - "foreignKey": { - "$ref": "#/definitions/tables.ForeignKey" - }, - "isNullable": { - "type": "boolean" - }, - "isPrimaryKey": { - "type": "boolean" - }, - "isUnique": { - "type": "boolean" - }, - "name": { - "type": "string" - }, - "type": { - "type": "string" - } - } - }, - "tables.ColumnCollection": { - "type": "object", - "properties": { - "columns": { - "type": "array", - "items": { - "$ref": "#/definitions/tables.Column" - } - } - } - }, "tables.Data": { "type": "object", "properties": { @@ -2269,17 +2324,6 @@ } } }, - "tables.ForeignKey": { - "type": "object", - "properties": { - "columnName": { - "type": "string" - }, - "tableName": { - "type": "string" - } - } - }, "tables.ShowColumn": { "type": "object", "properties": { @@ -2318,34 +2362,36 @@ } } }, - "tables.TableUpdate": { + "tables.UpdateTableSchema": { "type": "object", + "required": [ + "name", + "schema" + ], "properties": { - "delete": { - "type": "array", - "items": { - "type": "string" - } + "description": { + "type": "string" + }, + "id": { + "type": "integer" }, - "insert": { - "$ref": "#/definitions/tables.ColumnCollection" + "name": { + "type": "string" + }, + "oid": { + "type": "string" }, - "update": { + "project_id": { + "type": "integer" + }, + "renames": { "type": "array", "items": { - "$ref": "#/definitions/tables.UpdateColumn" + "$ref": "#/definitions/utils.RenameRelation" } - } - } - }, - "tables.UpdateColumn": { - "type": "object", - "properties": { - "name": { - "type": "string" }, - "update": { - "$ref": "#/definitions/tables.Column" + "schema": { + "$ref": "#/definitions/utils.Table" } } }, @@ -2401,6 +2447,17 @@ } } }, + "utils.RenameRelation": { + "type": "object", + "properties": { + "newName": { + "type": "string" + }, + "oldName": { + "type": "string" + } + } + }, "utils.Table": { "type": "object", "properties": { diff --git a/docs/swagger.yaml b/docs/swagger.yaml index e2d0224..8beb675 100644 --- a/docs/swagger.yaml +++ b/docs/swagger.yaml @@ -316,28 +316,6 @@ definitions: example: Operation successful 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: - type: string - type: object - tables.ColumnCollection: - properties: - columns: - items: - $ref: '#/definitions/tables.Column' - type: array - type: object tables.Data: properties: columns: @@ -350,13 +328,6 @@ definitions: type: object type: array type: object - tables.ForeignKey: - properties: - columnName: - type: string - tableName: - type: string - type: object tables.ShowColumn: properties: name: @@ -382,25 +353,27 @@ definitions: - name - schema type: object - tables.TableUpdate: - properties: - delete: - items: - type: string - type: array - insert: - $ref: '#/definitions/tables.ColumnCollection' - update: - items: - $ref: '#/definitions/tables.UpdateColumn' - type: array - type: object - tables.UpdateColumn: + tables.UpdateTableSchema: properties: + description: + type: string + id: + type: integer name: type: string - update: - $ref: '#/definitions/tables.Column' + oid: + 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: @@ -436,6 +409,13 @@ definitions: TableName: type: string type: object + utils.RenameRelation: + properties: + newName: + type: string + oldName: + type: string + type: object utils.Table: properties: Columns: @@ -656,15 +636,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 @@ -749,12 +733,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: @@ -765,20 +749,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 diff --git a/go.mod b/go.mod index a74cb3a..8ad938c 100644 --- a/go.mod +++ b/go.mod @@ -8,6 +8,7 @@ go 1.24.0 require ( github.com/Database-Hosting-Services/AI-Agent v1.0.0 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/google/uuid v1.6.0 @@ -32,11 +33,12 @@ require ( 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/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 @@ -45,6 +47,7 @@ require ( 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/go-querystring v1.1.0 // 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 @@ -52,25 +55,25 @@ require ( 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 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/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 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 + golang.org/x/tools v0.34.0 // indirect google.golang.org/api v0.186.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 diff --git a/go.sum b/go.sum index cbadec6..048fb21 100644 --- a/go.sum +++ b/go.sum @@ -21,11 +21,17 @@ 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/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/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/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= @@ -47,8 +53,8 @@ github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSw 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,8 +65,8 @@ 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/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.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= @@ -86,9 +92,12 @@ github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMyw 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.2/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.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= +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.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= @@ -100,6 +109,8 @@ github.com/googleapis/gax-go/v2 v2.12.5 h1:8gw9KZK8TiVKB6q3zHY3SBzLnrGp6HQjyfYBY github.com/googleapis/gax-go/v2 v2.12.5/go.mod h1:BUDKcWo+RaKq5SC9vVYL0wLADa3VcfswbOMMRmB9H3E= 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= @@ -113,6 +124,8 @@ 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.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= @@ -151,24 +164,38 @@ github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOf 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= +github.com/tidwall/gjson v1.14.4 h1:uo0p8EbA09J7RQaflQ1aBRffTR7xedD2bcIVSYxLnkM= +github.com/tidwall/gjson v1.14.4/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 h1:RWIZEg2iJ8/g6fDDYzMpobmaoGh5OLl4AXtGUGPcqCs= +github.com/tidwall/pretty v1.2.0/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.opencensus.io v0.24.0 h1:y73uSU6J157QMP2kn2r30vwW1A2W2WFwSCGnAVxeaD0= go.opencensus.io v0.24.0/go.mod h1:vNK8G9p7aAivkbmorf4v+7Hgx+Zs0yY+0fOtgBfjQKo= 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/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= 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= @@ -212,8 +239,8 @@ golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGm golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY= 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.34.0 h1:qIpSLOxeCYGg9TrcJokLBG4KFA6d795g0xkBkiESGlo= +golang.org/x/tools v0.34.0/go.mod h1:pAP9OwEaY1CAW3HOmg3hLZC5Z0CCmzjAF2UQMSqNARg= 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= diff --git a/tmp/air_errors.log b/tmp/air_errors.log index e69de29..05e5985 100644 --- a/tmp/air_errors.log +++ b/tmp/air_errors.log @@ -0,0 +1 @@ +exit status 1 \ No newline at end of file From 63d3278048d42315f986acf9c4f469bb7b8f480b Mon Sep 17 00:00:00 2001 From: rag-nar1 Date: Mon, 30 Jun 2025 14:02:19 +0300 Subject: [PATCH 020/101] feat: enable deployment mode and update table schema documentation in Swagger --- config/application.go | 2 +- docs/docs.go | 203 +++++++++++++++++++++++++++--------------- docs/swagger.json | 203 +++++++++++++++++++++++++++--------------- docs/swagger.yaml | 141 ++++++++++++++++++----------- 4 files changed, 349 insertions(+), 200 deletions(-) diff --git a/config/application.go b/config/application.go index c063d5c..39b963a 100644 --- a/config/application.go +++ b/config/application.go @@ -72,7 +72,7 @@ func loadEnv() { } const ( - deploy = false + deploy = true ) func Init(infoLog, errorLog *log.Logger) { diff --git a/docs/docs.go b/docs/docs.go index 2f30bba..c00100a 100644 --- a/docs/docs.go +++ b/docs/docs.go @@ -391,12 +391,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 +410,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" } } } @@ -467,19 +473,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" } } } @@ -2224,40 +2313,6 @@ const docTemplate = `{ } } }, - "tables.Column": { - "type": "object", - "properties": { - "foreignKey": { - "$ref": "#/definitions/tables.ForeignKey" - }, - "isNullable": { - "type": "boolean" - }, - "isPrimaryKey": { - "type": "boolean" - }, - "isUnique": { - "type": "boolean" - }, - "name": { - "type": "string" - }, - "type": { - "type": "string" - } - } - }, - "tables.ColumnCollection": { - "type": "object", - "properties": { - "columns": { - "type": "array", - "items": { - "$ref": "#/definitions/tables.Column" - } - } - } - }, "tables.Data": { "type": "object", "properties": { @@ -2276,17 +2331,6 @@ const docTemplate = `{ } } }, - "tables.ForeignKey": { - "type": "object", - "properties": { - "columnName": { - "type": "string" - }, - "tableName": { - "type": "string" - } - } - }, "tables.ShowColumn": { "type": "object", "properties": { @@ -2325,34 +2369,36 @@ const docTemplate = `{ } } }, - "tables.TableUpdate": { + "tables.UpdateTableSchema": { "type": "object", + "required": [ + "name", + "schema" + ], "properties": { - "delete": { - "type": "array", - "items": { - "type": "string" - } + "description": { + "type": "string" + }, + "id": { + "type": "integer" }, - "insert": { - "$ref": "#/definitions/tables.ColumnCollection" + "name": { + "type": "string" + }, + "oid": { + "type": "string" }, - "update": { + "project_id": { + "type": "integer" + }, + "renames": { "type": "array", "items": { - "$ref": "#/definitions/tables.UpdateColumn" + "$ref": "#/definitions/utils.RenameRelation" } - } - } - }, - "tables.UpdateColumn": { - "type": "object", - "properties": { - "name": { - "type": "string" }, - "update": { - "$ref": "#/definitions/tables.Column" + "schema": { + "$ref": "#/definitions/utils.Table" } } }, @@ -2408,6 +2454,17 @@ const docTemplate = `{ } } }, + "utils.RenameRelation": { + "type": "object", + "properties": { + "newName": { + "type": "string" + }, + "oldName": { + "type": "string" + } + } + }, "utils.Table": { "type": "object", "properties": { diff --git a/docs/swagger.json b/docs/swagger.json index 3c1e20b..4a89711 100644 --- a/docs/swagger.json +++ b/docs/swagger.json @@ -384,12 +384,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 +403,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" } } } @@ -460,19 +466,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" } } } @@ -2217,40 +2306,6 @@ } } }, - "tables.Column": { - "type": "object", - "properties": { - "foreignKey": { - "$ref": "#/definitions/tables.ForeignKey" - }, - "isNullable": { - "type": "boolean" - }, - "isPrimaryKey": { - "type": "boolean" - }, - "isUnique": { - "type": "boolean" - }, - "name": { - "type": "string" - }, - "type": { - "type": "string" - } - } - }, - "tables.ColumnCollection": { - "type": "object", - "properties": { - "columns": { - "type": "array", - "items": { - "$ref": "#/definitions/tables.Column" - } - } - } - }, "tables.Data": { "type": "object", "properties": { @@ -2269,17 +2324,6 @@ } } }, - "tables.ForeignKey": { - "type": "object", - "properties": { - "columnName": { - "type": "string" - }, - "tableName": { - "type": "string" - } - } - }, "tables.ShowColumn": { "type": "object", "properties": { @@ -2318,34 +2362,36 @@ } } }, - "tables.TableUpdate": { + "tables.UpdateTableSchema": { "type": "object", + "required": [ + "name", + "schema" + ], "properties": { - "delete": { - "type": "array", - "items": { - "type": "string" - } + "description": { + "type": "string" + }, + "id": { + "type": "integer" }, - "insert": { - "$ref": "#/definitions/tables.ColumnCollection" + "name": { + "type": "string" + }, + "oid": { + "type": "string" }, - "update": { + "project_id": { + "type": "integer" + }, + "renames": { "type": "array", "items": { - "$ref": "#/definitions/tables.UpdateColumn" + "$ref": "#/definitions/utils.RenameRelation" } - } - } - }, - "tables.UpdateColumn": { - "type": "object", - "properties": { - "name": { - "type": "string" }, - "update": { - "$ref": "#/definitions/tables.Column" + "schema": { + "$ref": "#/definitions/utils.Table" } } }, @@ -2401,6 +2447,17 @@ } } }, + "utils.RenameRelation": { + "type": "object", + "properties": { + "newName": { + "type": "string" + }, + "oldName": { + "type": "string" + } + } + }, "utils.Table": { "type": "object", "properties": { diff --git a/docs/swagger.yaml b/docs/swagger.yaml index e2d0224..8beb675 100644 --- a/docs/swagger.yaml +++ b/docs/swagger.yaml @@ -316,28 +316,6 @@ definitions: example: Operation successful 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: - type: string - type: object - tables.ColumnCollection: - properties: - columns: - items: - $ref: '#/definitions/tables.Column' - type: array - type: object tables.Data: properties: columns: @@ -350,13 +328,6 @@ definitions: type: object type: array type: object - tables.ForeignKey: - properties: - columnName: - type: string - tableName: - type: string - type: object tables.ShowColumn: properties: name: @@ -382,25 +353,27 @@ definitions: - name - schema type: object - tables.TableUpdate: - properties: - delete: - items: - type: string - type: array - insert: - $ref: '#/definitions/tables.ColumnCollection' - update: - items: - $ref: '#/definitions/tables.UpdateColumn' - type: array - type: object - tables.UpdateColumn: + tables.UpdateTableSchema: properties: + description: + type: string + id: + type: integer name: type: string - update: - $ref: '#/definitions/tables.Column' + oid: + 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: @@ -436,6 +409,13 @@ definitions: TableName: type: string type: object + utils.RenameRelation: + properties: + newName: + type: string + oldName: + type: string + type: object utils.Table: properties: Columns: @@ -656,15 +636,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 @@ -749,12 +733,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: @@ -765,20 +749,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 From 45da124c85c90be4f4eff8d51bb442364b01e06a Mon Sep 17 00:00:00 2001 From: GergesHany Date: Tue, 1 Jul 2025 18:04:51 +0300 Subject: [PATCH 021/101] Init sqlEditor package --- SqlEditor/SqlQueries.go | 1 + SqlEditor/handlers.go | 1 + SqlEditor/models.go | 1 + SqlEditor/repository.go | 1 + SqlEditor/routes.go | 1 + SqlEditor/service.go | 1 + SqlEditor/utils.go | 1 + 7 files changed, 7 insertions(+) create mode 100644 SqlEditor/SqlQueries.go create mode 100644 SqlEditor/handlers.go create mode 100644 SqlEditor/models.go create mode 100644 SqlEditor/repository.go create mode 100644 SqlEditor/routes.go create mode 100644 SqlEditor/service.go create mode 100644 SqlEditor/utils.go diff --git a/SqlEditor/SqlQueries.go b/SqlEditor/SqlQueries.go new file mode 100644 index 0000000..e323193 --- /dev/null +++ b/SqlEditor/SqlQueries.go @@ -0,0 +1 @@ +package sqleditor \ No newline at end of file diff --git a/SqlEditor/handlers.go b/SqlEditor/handlers.go new file mode 100644 index 0000000..e323193 --- /dev/null +++ b/SqlEditor/handlers.go @@ -0,0 +1 @@ +package sqleditor \ No newline at end of file diff --git a/SqlEditor/models.go b/SqlEditor/models.go new file mode 100644 index 0000000..e323193 --- /dev/null +++ b/SqlEditor/models.go @@ -0,0 +1 @@ +package sqleditor \ No newline at end of file diff --git a/SqlEditor/repository.go b/SqlEditor/repository.go new file mode 100644 index 0000000..e323193 --- /dev/null +++ b/SqlEditor/repository.go @@ -0,0 +1 @@ +package sqleditor \ No newline at end of file diff --git a/SqlEditor/routes.go b/SqlEditor/routes.go new file mode 100644 index 0000000..e323193 --- /dev/null +++ b/SqlEditor/routes.go @@ -0,0 +1 @@ +package sqleditor \ No newline at end of file diff --git a/SqlEditor/service.go b/SqlEditor/service.go new file mode 100644 index 0000000..e323193 --- /dev/null +++ b/SqlEditor/service.go @@ -0,0 +1 @@ +package sqleditor \ No newline at end of file diff --git a/SqlEditor/utils.go b/SqlEditor/utils.go new file mode 100644 index 0000000..e323193 --- /dev/null +++ b/SqlEditor/utils.go @@ -0,0 +1 @@ +package sqleditor \ No newline at end of file From 943ad7a084b23d629fb5ca6014b0c1ccad496a8a Mon Sep 17 00:00:00 2001 From: GergesHany Date: Tue, 1 Jul 2025 20:08:11 +0300 Subject: [PATCH 022/101] feat: implement SQL Editor module with query execution capabilities - Add complete SQL Editor module - Implement GET /api/projects/{project_id}/sqlEditor/run-query endpoint - Add query execution with JSON aggregation for structured results - Include execution time tracking and column name extraction The SQL Editor allows authenticated users to execute custom SQL queries against their project databases with formatted JSON results and metadata. --- .air.toml | 2 +- SqlEditor/SqlQueries.go | 2 +- SqlEditor/handlers.go | 45 ++++++++++++++++++++++++++++++++++++++++- SqlEditor/models.go | 14 ++++++++++++- SqlEditor/repository.go | 37 ++++++++++++++++++++++++++++++++- SqlEditor/routes.go | 17 +++++++++++++++- SqlEditor/service.go | 37 ++++++++++++++++++++++++++++++++- SqlEditor/utils.go | 41 ++++++++++++++++++++++++++++++++++++- main/routes.go | 4 +++- 9 files changed, 190 insertions(+), 9 deletions(-) diff --git a/.air.toml b/.air.toml index da4cb18..0f47cd0 100644 --- a/.air.toml +++ b/.air.toml @@ -13,7 +13,7 @@ include_ext = ["go"] include_dir = ["main", "accounts", "build", "caching", "config", "middleware", "projects", "response", "scripts", "utils", "test", "indexes", "AI", "analytics", - "tables"] + "tables", "SqlEditor"] exclude_dir = ["tmp", "vendor", "testdata", ".git"] kill_delay = "1s" diff --git a/SqlEditor/SqlQueries.go b/SqlEditor/SqlQueries.go index e323193..1a4bc9e 100644 --- a/SqlEditor/SqlQueries.go +++ b/SqlEditor/SqlQueries.go @@ -1 +1 @@ -package sqleditor \ No newline at end of file +package sqleditor diff --git a/SqlEditor/handlers.go b/SqlEditor/handlers.go index e323193..5cc2e5d 100644 --- a/SqlEditor/handlers.go +++ b/SqlEditor/handlers.go @@ -1 +1,44 @@ -package sqleditor \ No newline at end of file +package sqleditor + +import ( + "DBHS/config" + "DBHS/response" + "DBHS/utils" + "encoding/json" + "net/http" + + "github.com/gorilla/mux" +) + +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, "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, "Invalid request body", nil) + return + } + + if RequestBody.Query == "" { + response.BadRequest(w, "Query is required", 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, "Query executed successfully", queryResponse) + config.App.InfoLog.Println("Query executed successfully for project:", projectOid) + } +} diff --git a/SqlEditor/models.go b/SqlEditor/models.go index e323193..61b7eff 100644 --- a/SqlEditor/models.go +++ b/SqlEditor/models.go @@ -1 +1,13 @@ -package sqleditor \ No newline at end of file +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 index e323193..0106626 100644 --- a/SqlEditor/repository.go +++ b/SqlEditor/repository.go @@ -1 +1,36 @@ -package sqleditor \ No newline at end of file +package sqleditor + +import ( + api "DBHS/utils/apiError" + "context" + "encoding/json" + "errors" + "time" + + "github.com/jackc/pgx/v5/pgxpool" +) + +func FetchQueryData(ctx context.Context, conn *pgxpool.Pool, query string) (ResponseBody, api.ApiError) { + startTime := time.Now() + query = WrapQueryWithJSONAgg(query) + + var result string + err := conn.QueryRow(ctx, query).Scan(&result) + if err != nil { + return ResponseBody{}, *api.NewApiError("Internal server error", 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 index e323193..ecaa196 100644 --- a/SqlEditor/routes.go +++ b/SqlEditor/routes.go @@ -1 +1,16 @@ -package sqleditor \ No newline at end of file +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.MethodGet: RunSqlQuery(config.App), + })) +} diff --git a/SqlEditor/service.go b/SqlEditor/service.go index e323193..656e8d8 100644 --- a/SqlEditor/service.go +++ b/SqlEditor/service.go @@ -1 +1,36 @@ -package sqleditor \ No newline at end of file +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 index e323193..66875e2 100644 --- a/SqlEditor/utils.go +++ b/SqlEditor/utils.go @@ -1 +1,40 @@ -package sqleditor \ No newline at end of file +package sqleditor + +import ( + "encoding/json" + "fmt" + "strings" +) + +// WrapQueryWithJSONAgg takes a SQL query and wraps it with json_agg and row_to_json +// to return the result as a JSON array +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/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() } From 6bd5e3476218729c07ca5c9d0895c284ee32d7b3 Mon Sep 17 00:00:00 2001 From: GergesHany Date: Thu, 3 Jul 2025 14:50:46 +0300 Subject: [PATCH 023/101] Documentation Updates --- SqlEditor/handlers.go | 15 +++++++++ docs/docs.go | 78 +++++++++++++++++++++++++++++++++++++++++++ docs/swagger.json | 78 +++++++++++++++++++++++++++++++++++++++++++ docs/swagger.yaml | 51 ++++++++++++++++++++++++++++ tmp/air_errors.log | 1 + 5 files changed, 223 insertions(+) diff --git a/SqlEditor/handlers.go b/SqlEditor/handlers.go index 5cc2e5d..f7daff9 100644 --- a/SqlEditor/handlers.go +++ b/SqlEditor/handlers.go @@ -10,6 +10,21 @@ import ( "github.com/gorilla/mux" ) +// 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 +// @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 "Query executed successfully" +// @Failure 400 {object} response.ErrorResponse "Project ID is missing or invalid request body" +// @Failure 401 {object} response.ErrorResponse "Unauthorized access" +// @Failure 404 {object} response.ErrorResponse "Project not found" +// @Failure 500 {object} response.ErrorResponse "Internal server error or query execution failed" +// @Router /projects/{project_id}/sqlEditor/run-query [get] func RunSqlQuery(app *config.Application) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { urlVariables := mux.Vars(r) diff --git a/docs/docs.go b/docs/docs.go index c00100a..339a23a 100644 --- a/docs/docs.go +++ b/docs/docs.go @@ -1460,6 +1460,76 @@ const docTemplate = `{ } } }, + "/projects/{project_id}/sqlEditor/run-query": { + "get": { + "security": [ + { + "BearerAuth": [] + } + ], + "description": "Execute a dynamic SQL query against a specific project's PostgreSQL database and return structured JSON results with metadata", + "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", + "schema": { + "$ref": "#/definitions/response.SuccessResponse" + } + }, + "400": { + "description": "Project ID is missing or invalid request body", + "schema": { + "$ref": "#/definitions/response.ErrorResponse" + } + }, + "401": { + "description": "Unauthorized access", + "schema": { + "$ref": "#/definitions/response.ErrorResponse" + } + }, + "404": { + "description": "Project not found", + "schema": { + "$ref": "#/definitions/response.ErrorResponse" + } + }, + "500": { + "description": "Internal server error or query execution failed", + "schema": { + "$ref": "#/definitions/response.ErrorResponse" + } + } + } + } + }, "/user/forget-password/verify": { "post": { "description": "Verify code and reset user password", @@ -2313,6 +2383,14 @@ const docTemplate = `{ } } }, + "sqleditor.RequestBody": { + "type": "object", + "properties": { + "query": { + "type": "string" + } + } + }, "tables.Data": { "type": "object", "properties": { diff --git a/docs/swagger.json b/docs/swagger.json index 4a89711..14f280e 100644 --- a/docs/swagger.json +++ b/docs/swagger.json @@ -1453,6 +1453,76 @@ } } }, + "/projects/{project_id}/sqlEditor/run-query": { + "get": { + "security": [ + { + "BearerAuth": [] + } + ], + "description": "Execute a dynamic SQL query against a specific project's PostgreSQL database and return structured JSON results with metadata", + "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", + "schema": { + "$ref": "#/definitions/response.SuccessResponse" + } + }, + "400": { + "description": "Project ID is missing or invalid request body", + "schema": { + "$ref": "#/definitions/response.ErrorResponse" + } + }, + "401": { + "description": "Unauthorized access", + "schema": { + "$ref": "#/definitions/response.ErrorResponse" + } + }, + "404": { + "description": "Project not found", + "schema": { + "$ref": "#/definitions/response.ErrorResponse" + } + }, + "500": { + "description": "Internal server error or query execution failed", + "schema": { + "$ref": "#/definitions/response.ErrorResponse" + } + } + } + } + }, "/user/forget-password/verify": { "post": { "description": "Verify code and reset user password", @@ -2306,6 +2376,14 @@ } } }, + "sqleditor.RequestBody": { + "type": "object", + "properties": { + "query": { + "type": "string" + } + } + }, "tables.Data": { "type": "object", "properties": { diff --git a/docs/swagger.yaml b/docs/swagger.yaml index 8beb675..77edb58 100644 --- a/docs/swagger.yaml +++ b/docs/swagger.yaml @@ -316,6 +316,11 @@ definitions: example: Operation successful type: string type: object + sqleditor.RequestBody: + properties: + query: + type: string + type: object tables.Data: properties: columns: @@ -1363,6 +1368,52 @@ paths: summary: Update the name of a specific index tags: - indexes + /projects/{project_id}/sqlEditor/run-query: + get: + consumes: + - application/json + description: Execute a dynamic SQL query against a specific project's PostgreSQL + database and return structured JSON results with metadata + 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 + schema: + $ref: '#/definitions/response.SuccessResponse' + "400": + description: Project ID is missing or invalid request body + schema: + $ref: '#/definitions/response.ErrorResponse' + "401": + description: Unauthorized access + schema: + $ref: '#/definitions/response.ErrorResponse' + "404": + description: Project not found + schema: + $ref: '#/definitions/response.ErrorResponse' + "500": + description: Internal server error or query execution failed + 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/tmp/air_errors.log b/tmp/air_errors.log index e69de29..4660a1c 100644 --- a/tmp/air_errors.log +++ b/tmp/air_errors.log @@ -0,0 +1 @@ +exit status 1exit status 1 \ No newline at end of file From cfa953edfa2fc249af2997a6c5febbdc25892a6c Mon Sep 17 00:00:00 2001 From: GergesHany Date: Thu, 3 Jul 2025 15:07:52 +0300 Subject: [PATCH 024/101] feat(SqlEditor): Add comprehensive SQL query validation and Swagger documentation - Add ValidateQuery function to prevent dangerous SQL operations - Allow CRUD operations (SELECT, INSERT, UPDATE, DELETE) while blocking schema modifications - Protect system tables (PG_DATABASE, PG_CLASS, PG_NAMESPACE, PG_TABLES, PG_ATTRIBUTE, INFORMATION_SCHEMA) from updates - Block file operations, system commands, and destructive operations (CREATE, DROP, ALTER, TRUNCATE) - Add comprehensive Swagger/OpenAPI documentation for RunSqlQuery endpoint --- SqlEditor/handlers.go | 7 ++++ SqlEditor/utils.go | 87 ++++++++++++++++++++++++++++++++++++++++++- 2 files changed, 93 insertions(+), 1 deletion(-) diff --git a/SqlEditor/handlers.go b/SqlEditor/handlers.go index f7daff9..3bfe2cf 100644 --- a/SqlEditor/handlers.go +++ b/SqlEditor/handlers.go @@ -46,6 +46,13 @@ func RunSqlQuery(app *config.Application) http.HandlerFunc { return } + // Validate query for dangerous operations + isValid, err := ValidateQuery(RequestBody.Query) + if !isValid { + response.BadRequest(w, err.Error(), nil) + return + } + // Get the query response queryResponse, apiErr := GetQueryResponse(r.Context(), config.DB, projectOid, RequestBody.Query) if apiErr.Error() != nil { diff --git a/SqlEditor/utils.go b/SqlEditor/utils.go index 66875e2..0456d93 100644 --- a/SqlEditor/utils.go +++ b/SqlEditor/utils.go @@ -2,12 +2,97 @@ 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 -// to return the result as a JSON array func WrapQueryWithJSONAgg(sqlQuery string) string { // Clean the input query by removing leading/trailing whitespace and semicolon cleanQuery := strings.TrimSuffix(strings.TrimSpace(sqlQuery), ";") From 98c8958da9a194134ac30726aca376078544b2f8 Mon Sep 17 00:00:00 2001 From: rag-nar1 Date: Thu, 3 Jul 2025 17:05:00 +0300 Subject: [PATCH 025/101] fix: disable deployment mode and simplify filter check in repository --- config/application.go | 2 +- tables/repository.go | 3 ++- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/config/application.go b/config/application.go index 39b963a..c063d5c 100644 --- a/config/application.go +++ b/config/application.go @@ -72,7 +72,7 @@ func loadEnv() { } const ( - deploy = true + deploy = false ) func Init(infoLog, errorLog *log.Logger) { diff --git a/tables/repository.go b/tables/repository.go index 4ec4867..836f4ed 100644 --- a/tables/repository.go +++ b/tables/repository.go @@ -158,6 +158,7 @@ func ReadTableData(ctx context.Context, tableName string, parameters map[string] if columns == nil { return nil, err } + data := Data{ Columns: make([]ShowColumn, len(columns)), } @@ -219,7 +220,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 " From bb5255fe1f0490bbca596d464c94f31ba84717c7 Mon Sep 17 00:00:00 2001 From: OmarAlaraby Date: Thu, 3 Jul 2025 18:55:52 +0300 Subject: [PATCH 026/101] chatbot base functionality --- AI/handlers.go | 3 +++ AI/services.go | 10 +++++----- config/application.go | 2 +- go.mod | 12 ++++++------ go.sum | 16 ++++++++++++++++ tmp/air_errors.log | 2 +- 6 files changed, 32 insertions(+), 13 deletions(-) diff --git a/AI/handlers.go b/AI/handlers.go index ce93113..9fe4000 100644 --- a/AI/handlers.go +++ b/AI/handlers.go @@ -4,6 +4,8 @@ import ( "DBHS/config" "DBHS/response" "encoding/json" + + //"encoding/json" "github.com/gorilla/mux" "net/http" ) @@ -71,6 +73,7 @@ func ChatBotAsk(app *config.Application) http.HandlerFunc { chat_data, err := GetOrCreateChatData(r.Context(), transaction, userID, projectID) if err != nil { + app.ErrorLog.Println(err.Error()) response.InternalServerError(w, "Failed to get or create chat data", err) return } diff --git a/AI/services.go b/AI/services.go index 6baa3e4..71bad36 100644 --- a/AI/services.go +++ b/AI/services.go @@ -6,9 +6,10 @@ import ( "DBHS/utils" "context" "encoding/json" + "errors" + "github.com/jackc/pgx/v5" "github.com/Database-Hosting-Services/AI-Agent/RAG" - "github.com/jackc/pgx/v5" ) func getReport(projectUUID string, userID int, analytics Analytics, AI RAG.RAGmodel) (string, error) { @@ -39,7 +40,6 @@ 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 @@ -63,10 +63,10 @@ func GetProjectIDfromOID(ctx context.Context, db utils.Querier, projectOID strin 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 err == pgx.ErrNoRows { + if errors.Is(err, pgx.ErrNoRows) { chat, err = CreateNewChat(ctx, db, utils.GenerateOID(), userID, projectID) if err != nil { return ChatData{}, err @@ -76,4 +76,4 @@ func GetOrCreateChatData(ctx context.Context, db utils.Querier, userID, projectI } } return chat, nil -} \ No newline at end of file +} diff --git a/config/application.go b/config/application.go index fcf65be..76f66e8 100644 --- a/config/application.go +++ b/config/application.go @@ -71,7 +71,7 @@ func loadEnv() { } } -const deploy = true +const deploy = false func Init(infoLog, errorLog *log.Logger) { diff --git a/go.mod b/go.mod index 69f06f1..ffeaf49 100644 --- a/go.mod +++ b/go.mod @@ -6,20 +6,20 @@ go 1.24.0 // update go version to 1.24.0 require ( - github.com/Database-Hosting-Services/AI-Agent v1.0.5 + github.com/Database-Hosting-Services/AI-Agent v1.0.6 github.com/MarceloPetrucio/go-scalar-api-reference v0.0.0-20240521013641-ce5d2efe0e06 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/redis/go-redis/v9 v9.11.0 github.com/rs/cors v1.11.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 ) @@ -70,7 +70,7 @@ require ( 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 + golang.org/x/tools v0.34.0 // indirect google.golang.org/api v0.186.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 diff --git a/go.sum b/go.sum index cff808e..bb1da40 100644 --- a/go.sum +++ b/go.sum @@ -12,8 +12,14 @@ cloud.google.com/go/compute/metadata v0.6.0/go.mod h1:FjyFAW1MW0C203CEOMDTu3Dk1F 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.2 h1:lAf3mendjBP+1Q6G1bHSUNP7HsoSS+lZLF4bL8b/zkk= +github.com/Database-Hosting-Services/AI-Agent v1.0.2/go.mod h1:4JoX4wIMVtU9ydEOFS6MiDQKnuRfCxSmuesGhmCokAY= +github.com/Database-Hosting-Services/AI-Agent v1.0.3 h1:Vf4w2jC/02CnlXzwqm8w7qxM5Rmx35O0UddjAlDPGPE= +github.com/Database-Hosting-Services/AI-Agent v1.0.3/go.mod h1:4JoX4wIMVtU9ydEOFS6MiDQKnuRfCxSmuesGhmCokAY= github.com/Database-Hosting-Services/AI-Agent v1.0.5 h1:WzSdMIVWCzNorlP8IwnlnOJQ8dGeK1rcwBteIqq0eec= github.com/Database-Hosting-Services/AI-Agent v1.0.5/go.mod h1:4JoX4wIMVtU9ydEOFS6MiDQKnuRfCxSmuesGhmCokAY= +github.com/Database-Hosting-Services/AI-Agent v1.0.6 h1:b38d2Z98DX8UvBDE/Km8aEDoGtCM5xVBWCeVNY27NuM= +github.com/Database-Hosting-Services/AI-Agent v1.0.6/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= @@ -63,6 +69,8 @@ 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-jwt/jwt/v5 v5.2.2 h1:Rl4B7itRWVtYIHFrSNd7vhTiz9UpLdi6gZhZ3wEeDy8= +github.com/golang-jwt/jwt/v5 v5.2.2/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= @@ -106,6 +114,8 @@ github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 h1:iCEnooe7Ulw 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= @@ -132,6 +142,8 @@ github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZN 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/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/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ= github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc= github.com/rs/cors v1.11.1 h1:eU3gRzXLRK57F5rKMGMZURNdIG4EoAmX8k94r9wXWHA= @@ -207,6 +219,8 @@ 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/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-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= @@ -214,6 +228,8 @@ golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3 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.34.0 h1:qIpSLOxeCYGg9TrcJokLBG4KFA6d795g0xkBkiESGlo= +golang.org/x/tools v0.34.0/go.mod h1:pAP9OwEaY1CAW3HOmg3hLZC5Z0CCmzjAF2UQMSqNARg= 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= diff --git a/tmp/air_errors.log b/tmp/air_errors.log index c215497..ee75dba 100644 --- a/tmp/air_errors.log +++ b/tmp/air_errors.log @@ -1 +1 @@ -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 1exit status 1exit status 1 \ No newline at end of file +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 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1 \ No newline at end of file From 985452b9db691e79ab366cfdbed128f6048f0176 Mon Sep 17 00:00:00 2001 From: Omar Alaraby <99359641+OmarAlaraby@users.noreply.github.com> Date: Thu, 3 Jul 2025 19:03:58 +0300 Subject: [PATCH 027/101] Update AI/models.go Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- AI/models.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/AI/models.go b/AI/models.go index 2c906fe..6d81463 100644 --- a/AI/models.go +++ b/AI/models.go @@ -1,7 +1,7 @@ package ai -var SENDER_TYPE_AI string = "ai" -var SENDER_TYPE_USER string = "user" +const SENDER_TYPE_AI = "ai" +const SENDER_TYPE_USER = "user" type Analytics struct {} From 1bdb9ac206098ebb5e3c5a96c96a4278c03d1e07 Mon Sep 17 00:00:00 2001 From: OmarAlaraby Date: Thu, 3 Jul 2025 19:14:36 +0300 Subject: [PATCH 028/101] fix duplicated imports --- AI/handlers.go | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/AI/handlers.go b/AI/handlers.go index 6d3a06e..d3ff90f 100644 --- a/AI/handlers.go +++ b/AI/handlers.go @@ -4,9 +4,8 @@ import ( "DBHS/config" "DBHS/response" "encoding/json" - "io" - "net/http" "github.com/gorilla/mux" + "io" "net/http" ) @@ -46,7 +45,6 @@ func Report(app *config.Application) http.HandlerFunc { } } - func ChatBotAsk(app *config.Application) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { @@ -140,4 +138,3 @@ func Agent(app *config.Application) http.HandlerFunc { response.OK(w, "Agent query successful", AIresponse) } } - From 6032073fc3fa4a5c2bda2cf856f01dffcea97471 Mon Sep 17 00:00:00 2001 From: OmarAlaraby Date: Thu, 3 Jul 2025 20:17:56 +0300 Subject: [PATCH 029/101] document chatbot endpoint --- AI/handlers.go | 17 ++++++++++- docs/docs.go | 72 ++++++++++++++++++++++++++++++++++++++++++++++ docs/swagger.json | 72 ++++++++++++++++++++++++++++++++++++++++++++++ docs/swagger.yaml | 44 ++++++++++++++++++++++++++++ tmp/air_errors.log | 2 +- 5 files changed, 205 insertions(+), 2 deletions(-) diff --git a/AI/handlers.go b/AI/handlers.go index d3ff90f..e2d2a97 100644 --- a/AI/handlers.go +++ b/AI/handlers.go @@ -45,6 +45,18 @@ func Report(app *config.Application) http.HandlerFunc { } } +// 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.Response "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) { @@ -55,7 +67,8 @@ func ChatBotAsk(app *config.Application) http.HandlerFunc { response.InternalServerError(w, "Failed to get project ID", err) return } - userID := r.Context().Value("user-id").(int) + userID64 := r.Context().Value("user-id").(int64) + userID := int(userID64) var userRequest ChatBotRequest if err := json.NewDecoder(r.Body).Decode(&userRequest); err != nil { @@ -70,12 +83,14 @@ func ChatBotAsk(app *config.Application) http.HandlerFunc { 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, "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 { diff --git a/docs/docs.go b/docs/docs.go index c4c89dc..26fa6da 100644 --- a/docs/docs.go +++ b/docs/docs.go @@ -777,6 +777,70 @@ const docTemplate = `{ } } }, + "/projects/{project_id}/ai/chatbot/ask": { + "post": { + "security": [ + { + "BearerAuth": [] + } + ], + "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" + ], + "produces": [ + "application/json" + ], + "tags": [ + "AI" + ], + "summary": "Chat Bot Ask", + "parameters": [ + { + "type": "string", + "description": "Project ID", + "name": "project_id", + "in": "path", + "required": true + }, + { + "description": "Chat Bot Request", + "name": "ChatBotRequest", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/ai.ChatBotRequest" + } + } + ], + "responses": { + "200": { + "description": "Answer generated successfully", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/response.Response" + }, + { + "type": "object", + "properties": { + "data": { + "type": "object" + } + } + } + ] + } + }, + "500": { + "description": "Internal server error", + "schema": { + "$ref": "#/definitions/response.Response" + } + } + } + } + }, "/projects/{project_id}/ai/report": { "get": { "security": [ @@ -2018,6 +2082,14 @@ const docTemplate = `{ } } }, + "ai.ChatBotRequest": { + "type": "object", + "properties": { + "question": { + "type": "string" + } + } + }, "indexes.IndexData": { "type": "object", "properties": { diff --git a/docs/swagger.json b/docs/swagger.json index 5ea4838..ded7167 100644 --- a/docs/swagger.json +++ b/docs/swagger.json @@ -770,6 +770,70 @@ } } }, + "/projects/{project_id}/ai/chatbot/ask": { + "post": { + "security": [ + { + "BearerAuth": [] + } + ], + "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" + ], + "produces": [ + "application/json" + ], + "tags": [ + "AI" + ], + "summary": "Chat Bot Ask", + "parameters": [ + { + "type": "string", + "description": "Project ID", + "name": "project_id", + "in": "path", + "required": true + }, + { + "description": "Chat Bot Request", + "name": "ChatBotRequest", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/ai.ChatBotRequest" + } + } + ], + "responses": { + "200": { + "description": "Answer generated successfully", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/response.Response" + }, + { + "type": "object", + "properties": { + "data": { + "type": "object" + } + } + } + ] + } + }, + "500": { + "description": "Internal server error", + "schema": { + "$ref": "#/definitions/response.Response" + } + } + } + } + }, "/projects/{project_id}/ai/report": { "get": { "security": [ @@ -2011,6 +2075,14 @@ } } }, + "ai.ChatBotRequest": { + "type": "object", + "properties": { + "question": { + "type": "string" + } + } + }, "indexes.IndexData": { "type": "object", "properties": { diff --git a/docs/swagger.yaml b/docs/swagger.yaml index c3ba981..0d540d5 100644 --- a/docs/swagger.yaml +++ b/docs/swagger.yaml @@ -203,6 +203,11 @@ definitions: example: User verified successfully type: string type: object + ai.ChatBotRequest: + properties: + question: + type: string + type: object indexes.IndexData: properties: columns: @@ -842,6 +847,45 @@ paths: summary: Update a project tags: - projects + /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.Response' + security: + - BearerAuth: [] + summary: Chat Bot Ask + tags: + - AI /projects/{project_id}/ai/report: get: consumes: diff --git a/tmp/air_errors.log b/tmp/air_errors.log index ee75dba..ccbfd30 100644 --- a/tmp/air_errors.log +++ b/tmp/air_errors.log @@ -1 +1 @@ -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 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1 \ No newline at end of file +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 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 From 76a96a5e20773070679cf9364fcba28109614c5f Mon Sep 17 00:00:00 2001 From: rag-nar1 Date: Fri, 4 Jul 2025 15:10:45 +0300 Subject: [PATCH 030/101] fix: update CheckOwnershipQueryTable to use project ID instead of project OID --- middleware/middleware.go | 8 +++++++- tmp/air_errors.log | 1 + utils/database.go | 4 ++-- 3 files changed, 10 insertions(+), 3 deletions(-) diff --git a/middleware/middleware.go b/middleware/middleware.go index 3e89467..81e5c25 100644 --- a/middleware/middleware.go +++ b/middleware/middleware.go @@ -63,8 +63,14 @@ func CheckOTableExist(next http.Handler) http.Handler { 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, "Failed to get project ID", err) + return + } //check if the table belongs to the project - ok, err := utils.CheckOwnershipQueryTable(r.Context(), tableOID, projectOID, config.DB) + ok, err := utils.CheckOwnershipQueryTable(r.Context(), tableOID, projectId.(int64), config.DB) if err != nil { response.InternalServerError(w, err.Error(), err) return diff --git a/tmp/air_errors.log b/tmp/air_errors.log index e69de29..f50898d 100644 --- a/tmp/air_errors.log +++ b/tmp/air_errors.log @@ -0,0 +1 @@ +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 1 \ No newline at end of file diff --git a/utils/database.go b/utils/database.go index 76cf917..cb0184f 100644 --- a/utils/database.go +++ b/utils/database.go @@ -27,9 +27,9 @@ func CheckOwnershipQuery(ctx context.Context, projectOID string, userId int64, d return count > 0, nil } -func CheckOwnershipQueryTable(ctx context.Context, tableOID string, projectOID string, db Querier) (bool, error) { +func CheckOwnershipQueryTable(ctx context.Context, tableOID string, projectID int64, db Querier) (bool, error) { var count int - err := db.QueryRow(ctx, CheckOwnershipTableStmt, tableOID, projectOID).Scan(&count) + err := db.QueryRow(ctx, CheckOwnershipTableStmt, tableOID, projectID).Scan(&count) if err != nil { return false, fmt.Errorf("failed to check ownership: %w", err) } From 6234d7ad570f928c01023d6f48b3e0070e1bab83 Mon Sep 17 00:00:00 2001 From: rag-nar1 Date: Fri, 4 Jul 2025 16:38:39 +0300 Subject: [PATCH 031/101] feat: implement agent functionality and request handling in AI module --- AI/handlers.go | 27 +++++++++++++++++++++------ AI/models.go | 4 ++++ AI/routes.go | 1 + AI/services.go | 42 ++++++++++++++++++++++++++++++++++++++++-- docs/docs.go | 8 ++++++++ docs/swagger.json | 29 ++++++++--------------------- docs/swagger.yaml | 5 +++++ tmp/air_errors.log | 1 + 8 files changed, 88 insertions(+), 29 deletions(-) diff --git a/AI/handlers.go b/AI/handlers.go index 3fbf906..f72dff4 100644 --- a/AI/handlers.go +++ b/AI/handlers.go @@ -124,19 +124,18 @@ func Agent(app *config.Application) http.HandlerFunc { } // parse the request body - var requestBody map[string]interface{} - err = json.Unmarshal(body, &requestBody) + request := &Request{} + err = json.Unmarshal(body, request) if err != nil { response.BadRequest(w, "Failed to parse request body", err) return } // check if the request body is valid - if requestBody["prompt"] == nil { + if request.Prompt == "" { response.BadRequest(w, "Prompt is required", nil) return } - prompt := requestBody["prompt"].(string) // get project id from path vars := mux.Vars(r) projectUID := vars["project_id"] @@ -144,7 +143,7 @@ func Agent(app *config.Application) http.HandlerFunc { // get user id from context userID := r.Context().Value("user-id").(int64) - AIresponse, err := AgentQuery(projectUID, userID, prompt, config.AI) + AIresponse, err := AgentQuery(projectUID, userID, request.Prompt, config.AI) if err != nil { response.InternalServerError(w, "error while querying agent", err) return @@ -156,6 +155,22 @@ func Agent(app *config.Application) http.HandlerFunc { 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, "No schema changes found or changes expired", nil) + } else { + response.InternalServerError(w, "error while executing agent", err) + } + return + } + response.OK(w, "query executed successfully", nil) } } diff --git a/AI/models.go b/AI/models.go index 6d81463..93e0206 100644 --- a/AI/models.go +++ b/AI/models.go @@ -15,4 +15,8 @@ type ChatData struct { Oid string `json:"oid"` OwnerID int `json:"owner_id"` ProjectID int `json:"project_id"` +} + +type Request struct { + Prompt string `json:"prompt"` } \ No newline at end of file diff --git a/AI/routes.go b/AI/routes.go index 38697a8..c631a0c 100644 --- a/AI/routes.go +++ b/AI/routes.go @@ -12,6 +12,7 @@ func DefineURLs() { 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)(Agent(config.App))) } \ No newline at end of file diff --git a/AI/services.go b/AI/services.go index 854ea1e..28b825e 100644 --- a/AI/services.go +++ b/AI/services.go @@ -2,7 +2,6 @@ package ai import ( "DBHS/config" - "DBHS/tables" "DBHS/utils" "context" "encoding/json" @@ -81,7 +80,7 @@ func GetOrCreateChatData(ctx context.Context, db utils.Querier, userID, projectI func AgentQuery(projectUUID string, userID int64, prompt string, AI RAG.RAGmodel) (*RAG.AgentResponse, 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 { config.App.ErrorLog.Println("Error extracting database connection:", err) return nil, err @@ -109,3 +108,42 @@ func AgentQuery(projectUUID string, userID int64, prompt string, AI RAG.RAGmodel 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 +} diff --git a/docs/docs.go b/docs/docs.go index 0ef2883..5180319 100644 --- a/docs/docs.go +++ b/docs/docs.go @@ -2240,6 +2240,14 @@ const docTemplate = `{ } } }, + "ai.ChatBotRequest": { + "type": "object", + "properties": { + "question": { + "type": "string" + } + } + }, "analytics.DatabaseActivityWithDates": { "type": "object", "properties": { diff --git a/docs/swagger.json b/docs/swagger.json index 7a8571c..a7ca083 100644 --- a/docs/swagger.json +++ b/docs/swagger.json @@ -1549,11 +1549,9 @@ } }, "400": { - "description": "Invalid code or password or email not found please sign up first", "description": "Invalid code or password or email not found please sign up first", "schema": { "$ref": "#/definitions/accounts.ErrorResponse400EmailNotFound" - "$ref": "#/definitions/accounts.ErrorResponse400EmailNotFound" } } } @@ -1775,11 +1773,9 @@ } }, "400": { - "description": "Invalid verification code or email not found please sign up first", "description": "Invalid verification code or email not found please sign up first", "schema": { "$ref": "#/definitions/accounts.ErrorResponse400EmailNotFound" - "$ref": "#/definitions/accounts.ErrorResponse400EmailNotFound" } } } @@ -1998,15 +1994,6 @@ } } }, - "accounts.ErrorResponse400EmailNotFound": { - "type": "object", - "properties": { - "error": { - "type": "string", - "example": "email not found please sign up first" - } - } - }, "accounts.LoginResponse": { "type": "object", "properties": { @@ -2246,6 +2233,14 @@ } } }, + "ai.ChatBotRequest": { + "type": "object", + "properties": { + "question": { + "type": "string" + } + } + }, "analytics.DatabaseActivityWithDates": { "type": "object", "properties": { @@ -2291,14 +2286,6 @@ } } }, - "ai.ChatBotRequest": { - "type": "object", - "properties": { - "question": { - "type": "string" - } - } - }, "indexes.IndexData": { "type": "object", "properties": { diff --git a/docs/swagger.yaml b/docs/swagger.yaml index bf4a59c..743abbb 100644 --- a/docs/swagger.yaml +++ b/docs/swagger.yaml @@ -203,6 +203,11 @@ definitions: example: User verified successfully type: string type: object + ai.ChatBotRequest: + properties: + question: + type: string + type: object analytics.DatabaseActivityWithDates: properties: timestamp: diff --git a/tmp/air_errors.log b/tmp/air_errors.log index 6ad8b47..928f129 100644 --- a/tmp/air_errors.log +++ b/tmp/air_errors.log @@ -1 +1,2 @@ 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 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 +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 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 From fdc039730ce41de81cbd280135f2a3ae8f88c12b Mon Sep 17 00:00:00 2001 From: rag-nar1 Date: Fri, 4 Jul 2025 16:41:04 +0300 Subject: [PATCH 032/101] fix: improve logging for AI report generation and agent query execution --- AI/handlers.go | 38 ++++++++++++++++++++++++++++---------- tmp/air_errors.log | 2 +- 2 files changed, 29 insertions(+), 11 deletions(-) diff --git a/AI/handlers.go b/AI/handlers.go index a14c976..7e8112f 100644 --- a/AI/handlers.go +++ b/AI/handlers.go @@ -4,7 +4,6 @@ import ( "DBHS/config" "DBHS/response" "encoding/json" - "net/http" "time" "github.com/axiomhq/axiom-go/axiom" @@ -46,10 +45,10 @@ func Report(app *config.Application) http.HandlerFunc { 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", + "project_id": projectID, + "user_id": userID, + "error": err.Error(), + "message": "Failed to generate AI report", }, }) return @@ -58,11 +57,11 @@ func Report(app *config.Application) http.HandlerFunc { 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", + "project_id": projectID, + "user_id": userID, + "report": report, + "status": "success", + "message": "AI report generated successfully", }, }) @@ -194,8 +193,27 @@ func AgentAccept(app *config.Application) http.HandlerFunc { } else { response.InternalServerError(w, "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, "query executed successfully", nil) } } diff --git a/tmp/air_errors.log b/tmp/air_errors.log index 971ef5f..a9f01e1 100644 --- a/tmp/air_errors.log +++ b/tmp/air_errors.log @@ -4,4 +4,4 @@ exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1ex ======= exit status 1 >>>>>>> logging -exit status 1exit status 1 \ No newline at end of file +exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1 \ No newline at end of file From 20280b7fd7774557ada2a19021d4b6f34f39f682 Mon Sep 17 00:00:00 2001 From: rag-nar1 Date: Fri, 4 Jul 2025 20:16:55 +0300 Subject: [PATCH 033/101] feat: add AI agent query endpoints and logging functionality --- AI/handlers.go | 95 ++++++++++++++++++++++++++++++++++++++++++++++ AI/routes.go | 3 +- AI/services.go | 10 +++++ docs/docs.go | 8 ++++ docs/swagger.json | 8 ++++ docs/swagger.yaml | 5 +++ tmp/air_errors.log | 2 +- 7 files changed, 129 insertions(+), 2 deletions(-) diff --git a/AI/handlers.go b/AI/handlers.go index 7e8112f..0c465d8 100644 --- a/AI/handlers.go +++ b/AI/handlers.go @@ -138,6 +138,19 @@ func ChatBotAsk(app *config.Application) http.HandlerFunc { } } +// 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.Response404 "Bad request" +// @Failure 500 {object} response.Response500 "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 @@ -170,13 +183,47 @@ func Agent(app *config.Application) http.HandlerFunc { AIresponse, err := AgentQuery(projectUID, userID, request.Prompt, config.AI) if err != nil { response.InternalServerError(w, "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, "Agent query successful", AIresponse) } } +// 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.Response400 "Bad request" +// @Failure 500 {object} response.Response500 "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 @@ -217,3 +264,51 @@ func AgentAccept(app *config.Application) http.HandlerFunc { response.OK(w, "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.Response400 "Bad request" +// @Failure 500 {object} response.Response500 "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, "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, "Agent query cancelled successfully", nil) + } +} diff --git a/AI/routes.go b/AI/routes.go index c631a0c..34b5cae 100644 --- a/AI/routes.go +++ b/AI/routes.go @@ -14,5 +14,6 @@ func DefineURLs() { 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)(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 28b825e..c10e338 100644 --- a/AI/services.go +++ b/AI/services.go @@ -147,3 +147,13 @@ func AgentExec(projectUUID string, userID int64, AI RAG.RAGmodel) error { } 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 +} \ No newline at end of file diff --git a/docs/docs.go b/docs/docs.go index 5180319..95bfc42 100644 --- a/docs/docs.go +++ b/docs/docs.go @@ -2248,6 +2248,14 @@ const docTemplate = `{ } } }, + "ai.Request": { + "type": "object", + "properties": { + "prompt": { + "type": "string" + } + } + }, "analytics.DatabaseActivityWithDates": { "type": "object", "properties": { diff --git a/docs/swagger.json b/docs/swagger.json index a7ca083..2d86d99 100644 --- a/docs/swagger.json +++ b/docs/swagger.json @@ -2241,6 +2241,14 @@ } } }, + "ai.Request": { + "type": "object", + "properties": { + "prompt": { + "type": "string" + } + } + }, "analytics.DatabaseActivityWithDates": { "type": "object", "properties": { diff --git a/docs/swagger.yaml b/docs/swagger.yaml index 743abbb..37749f2 100644 --- a/docs/swagger.yaml +++ b/docs/swagger.yaml @@ -208,6 +208,11 @@ definitions: question: type: string type: object + ai.Request: + properties: + prompt: + type: string + type: object analytics.DatabaseActivityWithDates: properties: timestamp: diff --git a/tmp/air_errors.log b/tmp/air_errors.log index a9f01e1..e04ddd3 100644 --- a/tmp/air_errors.log +++ b/tmp/air_errors.log @@ -4,4 +4,4 @@ exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1ex ======= exit status 1 >>>>>>> logging -exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1 \ No newline at end of file +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 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 1exit status 1 \ No newline at end of file From 7252312ae88793770b13d0c5b50889d0f6b06bd2 Mon Sep 17 00:00:00 2001 From: rag-nar1 Date: Fri, 4 Jul 2025 20:19:27 +0300 Subject: [PATCH 034/101] fix: update deployment flag for production environment --- config/application.go | 2 +- tmp/air_errors.log | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/config/application.go b/config/application.go index 7bf8857..9e4ad36 100644 --- a/config/application.go +++ b/config/application.go @@ -75,7 +75,7 @@ func loadEnv() { } const ( - deploy = false + deploy = true // Set to true if running in production, false for development ) func Init(infoLog, errorLog *log.Logger) { diff --git a/tmp/air_errors.log b/tmp/air_errors.log index e04ddd3..0a7ff00 100644 --- a/tmp/air_errors.log +++ b/tmp/air_errors.log @@ -4,4 +4,4 @@ exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1ex ======= exit status 1 >>>>>>> logging -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 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 1exit status 1 \ No newline at end of file +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 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 1exit status 1exit status 1exit status 1exit status 1 \ No newline at end of file From 9fe9dc5fcc00c77e2a314c18db7604f7e4e67809 Mon Sep 17 00:00:00 2001 From: rag-nar1 Date: Fri, 4 Jul 2025 20:26:38 +0300 Subject: [PATCH 035/101] feat: add AI agent query endpoints and update error response handling --- AI/handlers.go | 16 ++-- AI/models.go | 7 +- docs/docs.go | 180 ++++++++++++++++++++++++++++++++++++++++++++- docs/swagger.json | 180 ++++++++++++++++++++++++++++++++++++++++++++- docs/swagger.yaml | 115 ++++++++++++++++++++++++++++- tmp/air_errors.log | 2 +- 6 files changed, 484 insertions(+), 16 deletions(-) diff --git a/AI/handlers.go b/AI/handlers.go index 0c465d8..2d72ac1 100644 --- a/AI/handlers.go +++ b/AI/handlers.go @@ -24,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 { @@ -78,7 +78,7 @@ func Report(app *config.Application) http.HandlerFunc { // @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.Response "Internal server error" +// @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 { @@ -147,8 +147,8 @@ func ChatBotAsk(app *config.Application) http.HandlerFunc { // @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.Response404 "Bad request" -// @Failure 500 {object} response.Response500 "Internal server error" +// @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 { @@ -220,8 +220,8 @@ func Agent(app *config.Application) http.HandlerFunc { // @Produce json // @Param project_id path string true "Project ID" // @Success 200 {object} response.Response "Query executed successfully" -// @Failure 400 {object} response.Response400 "Bad request" -// @Failure 500 {object} response.Response500 "Internal server error" +// @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 { @@ -272,8 +272,8 @@ func AgentAccept(app *config.Application) http.HandlerFunc { // @Produce json // @Param project_id path string true "Project ID" // @Success 200 {object} response.Response "Agent query cancelled successfully" -// @Failure 400 {object} response.Response400 "Bad request" -// @Failure 500 {object} response.Response500 "Internal server error" +// @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 { diff --git a/AI/models.go b/AI/models.go index 93e0206..ffc1e74 100644 --- a/AI/models.go +++ b/AI/models.go @@ -1,5 +1,4 @@ package ai - const SENDER_TYPE_AI = "ai" const SENDER_TYPE_USER = "user" @@ -19,4 +18,10 @@ type ChatData struct { type Request struct { Prompt string `json:"prompt"` +} + +type AgentResponse struct { + Response string `json:"response"` + SchemaChanges string `json:"schema_changes"` + SchemaDDL string `json:"schema_ddl"` } \ No newline at end of file diff --git a/docs/docs.go b/docs/docs.go index 741de2f..9079a25 100644 --- a/docs/docs.go +++ b/docs/docs.go @@ -878,6 +878,168 @@ const docTemplate = `{ } } }, + "/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" + } + }, + "500": { + "description": "Internal server error", + "schema": { + "$ref": "#/definitions/response.ErrorResponse500" + } + } + } + } + }, "/projects/{project_id}/ai/chatbot/ask": { "post": { "security": [ @@ -936,7 +1098,7 @@ const docTemplate = `{ "500": { "description": "Internal server error", "schema": { - "$ref": "#/definitions/response.Response" + "$ref": "#/definitions/response.ErrorResponse500" } } } @@ -991,7 +1153,7 @@ const docTemplate = `{ "500": { "description": "Internal server error", "schema": { - "$ref": "#/definitions/response.Response" + "$ref": "#/definitions/response.ErrorResponse500" } } } @@ -2310,6 +2472,20 @@ const docTemplate = `{ } } }, + "ai.AgentResponse": { + "type": "object", + "properties": { + "response": { + "type": "string" + }, + "schema_changes": { + "type": "string" + }, + "schema_ddl": { + "type": "string" + } + } + }, "ai.ChatBotRequest": { "type": "object", "properties": { diff --git a/docs/swagger.json b/docs/swagger.json index 6907d5d..d3e839e 100644 --- a/docs/swagger.json +++ b/docs/swagger.json @@ -871,6 +871,168 @@ } } }, + "/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" + } + }, + "500": { + "description": "Internal server error", + "schema": { + "$ref": "#/definitions/response.ErrorResponse500" + } + } + } + } + }, "/projects/{project_id}/ai/chatbot/ask": { "post": { "security": [ @@ -929,7 +1091,7 @@ "500": { "description": "Internal server error", "schema": { - "$ref": "#/definitions/response.Response" + "$ref": "#/definitions/response.ErrorResponse500" } } } @@ -984,7 +1146,7 @@ "500": { "description": "Internal server error", "schema": { - "$ref": "#/definitions/response.Response" + "$ref": "#/definitions/response.ErrorResponse500" } } } @@ -2303,6 +2465,20 @@ } } }, + "ai.AgentResponse": { + "type": "object", + "properties": { + "response": { + "type": "string" + }, + "schema_changes": { + "type": "string" + }, + "schema_ddl": { + "type": "string" + } + } + }, "ai.ChatBotRequest": { "type": "object", "properties": { diff --git a/docs/swagger.yaml b/docs/swagger.yaml index ef3bf43..7b6974d 100644 --- a/docs/swagger.yaml +++ b/docs/swagger.yaml @@ -203,6 +203,15 @@ definitions: example: User verified successfully type: string type: object + ai.AgentResponse: + properties: + response: + type: string + schema_changes: + type: string + schema_ddl: + type: string + type: object ai.ChatBotRequest: properties: question: @@ -1044,6 +1053,108 @@ 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: @@ -1077,7 +1188,7 @@ paths: "500": description: Internal server error schema: - $ref: '#/definitions/response.Response' + $ref: '#/definitions/response.ErrorResponse500' security: - BearerAuth: [] summary: Chat Bot Ask @@ -1109,7 +1220,7 @@ paths: "500": description: Internal server error schema: - $ref: '#/definitions/response.Response' + $ref: '#/definitions/response.ErrorResponse500' security: - BearerAuth: [] summary: Generate AI Report diff --git a/tmp/air_errors.log b/tmp/air_errors.log index 0a7ff00..d15f370 100644 --- a/tmp/air_errors.log +++ b/tmp/air_errors.log @@ -4,4 +4,4 @@ exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1ex ======= exit status 1 >>>>>>> logging -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 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 1exit status 1exit status 1exit status 1exit status 1 \ No newline at end of file +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 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 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 From 8f5ef495588eab7887f3206d3bbce2a488b116e0 Mon Sep 17 00:00:00 2001 From: rag-nar1 Date: Tue, 8 Jul 2025 14:37:20 +0300 Subject: [PATCH 036/101] fix: ensure data is an empty slice if no tables are found --- tables/handlers.go | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/tables/handlers.go b/tables/handlers.go index 29f867c..f993bd7 100644 --- a/tables/handlers.go +++ b/tables/handlers.go @@ -42,7 +42,9 @@ func GetAllTablesHandler(app *config.Application) http.HandlerFunc { response.InternalServerError(w, "Failed to read tables", err) return } - + if data == nil { + data = []Table{} // Ensure data is an empty slice if no tables found + } response.OK(w, "", data) } } From c9a50c9343ef4c97baffdfaf9cd498c00732807e Mon Sep 17 00:00:00 2001 From: rag-nar1 Date: Tue, 8 Jul 2025 14:45:30 +0300 Subject: [PATCH 037/101] feat: add project and table existence checks in middleware --- middleware/middleware.go | 25 +++++++++++++++++++++++++ utils/database.go | 21 +++++++++++++++++++++ 2 files changed, 46 insertions(+) diff --git a/middleware/middleware.go b/middleware/middleware.go index 81e5c25..1a3e313 100644 --- a/middleware/middleware.go +++ b/middleware/middleware.go @@ -43,6 +43,19 @@ func CheckOwnership(next http.Handler) http.Handler { 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, "Failed to check project existence", err) + return + } + + if !exists { + config.App.ErrorLog.Printf("Project %s does not exist", projectOID) + response.NotFound(w, "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, err.Error(), err) @@ -69,6 +82,18 @@ func CheckOTableExist(next http.Handler) http.Handler { response.InternalServerError(w, "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, "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, "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 { diff --git a/utils/database.go b/utils/database.go index cb0184f..c5c55bc 100644 --- a/utils/database.go +++ b/utils/database.go @@ -10,7 +10,9 @@ import ( 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 { @@ -19,6 +21,7 @@ func UpdateDataInDatabase(ctx context.Context, db Querier, query string, dest .. } 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 { @@ -27,6 +30,15 @@ func CheckOwnershipQuery(ctx context.Context, projectOID string, userId int64, d 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, CheckOwnershipTableStmt, tableOID, projectID).Scan(&count) @@ -36,6 +48,15 @@ func CheckOwnershipQueryTable(ctx context.Context, tableOID string, projectID in 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) From 0f543b8969c315f9e2c105f0572b4b20f30839e1 Mon Sep 17 00:00:00 2001 From: rag-nar1 Date: Tue, 8 Jul 2025 15:29:15 +0300 Subject: [PATCH 038/101] Refactor response handling to include request context in error and success responses - Updated response functions to accept the request context, allowing for better logging and tracking of user-related information. - Modified various handler functions across analytics, indexes, middleware, projects, schemas, tables, and response packages to pass the request context to response functions. - Removed the pagination response file as it was no longer needed. - Enhanced error handling to provide more context in logs and responses. --- AI/handlers.go | 40 ++++++++--------- SqlEditor/handlers.go | 10 ++--- accounts/handlers.go | 90 +++++++++++++++++++-------------------- analytics/handlers.go | 14 +++--- indexes/handlers.go | 28 ++++++------ middleware/middleware.go | 22 +++++----- middleware/rateLimiter.go | 2 +- middleware/token.go | 8 ++-- projects/handlers.go | 54 +++++++++++------------ response/errors.go | 24 +++++------ response/pagination.go | 23 ---------- response/response.go | 20 ++++++++- response/success.go | 12 +++--- schemas/handlers.go | 26 +++++------ tables/handlers.go | 56 ++++++++++++------------ tables/middleware.go | 8 ++-- 16 files changed, 216 insertions(+), 221 deletions(-) delete mode 100644 response/pagination.go diff --git a/AI/handlers.go b/AI/handlers.go index 2d72ac1..fc96549 100644 --- a/AI/handlers.go +++ b/AI/handlers.go @@ -41,7 +41,7 @@ func Report(app *config.Application) http.HandlerFunc { 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(), @@ -65,7 +65,7 @@ func Report(app *config.Application) http.HandlerFunc { }, }) - response.OK(w, "Report generated successfully", report) + response.OK(w, r, "Report generated successfully", report) } } @@ -88,7 +88,7 @@ func ChatBotAsk(app *config.Application) http.HandlerFunc { projectOID := vars["project_id"] projectID, err := GetProjectIDfromOID(r.Context(), config.DB, projectOID) if err != nil { - response.InternalServerError(w, "Failed to get project ID", err) + response.InternalServerError(w, r, "Failed to get project ID", err) return } userID64 := r.Context().Value("user-id").(int64) @@ -96,14 +96,14 @@ func ChatBotAsk(app *config.Application) http.HandlerFunc { var userRequest ChatBotRequest if err := json.NewDecoder(r.Body).Decode(&userRequest); err != nil { - response.BadRequest(w, "Invalid request body", err) + 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, "Failed to start database transaction", err) + response.InternalServerError(w, r, "Failed to start database transaction", err) return } @@ -111,30 +111,30 @@ func ChatBotAsk(app *config.Application) http.HandlerFunc { chat_data, err := GetOrCreateChatData(r.Context(), transaction, userID, projectID) if err != nil { app.ErrorLog.Println(err.Error()) - response.InternalServerError(w, "Failed to get or create chat data", err) + 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, err.Error(), err) + 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, err.Error(), err) + response.InternalServerError(w, r, err.Error(), err) return } if err := transaction.Commit(r.Context()); err != nil { app.ErrorLog.Println(err.Error()) - response.InternalServerError(w, "Failed to commit database transaction", err) + response.InternalServerError(w, r, "Failed to commit database transaction", err) return } - response.OK(w, "Answer generated successfully", answer) + response.OK(w, r, "Answer generated successfully", answer) } } @@ -156,7 +156,7 @@ func Agent(app *config.Application) http.HandlerFunc { // get the request body body, err := io.ReadAll(r.Body) if err != nil { - response.BadRequest(w, "Failed to read request body", err) + response.BadRequest(w, r, "Failed to read request body", err) return } @@ -164,12 +164,12 @@ func Agent(app *config.Application) http.HandlerFunc { request := &Request{} err = json.Unmarshal(body, request) if err != nil { - response.BadRequest(w, "Failed to parse request body", err) + response.BadRequest(w, r, "Failed to parse request body", err) return } // check if the request body is valid if request.Prompt == "" { - response.BadRequest(w, "Prompt is required", nil) + response.BadRequest(w, r, "Prompt is required", nil) return } @@ -182,7 +182,7 @@ func Agent(app *config.Application) http.HandlerFunc { AIresponse, err := AgentQuery(projectUID, userID, request.Prompt, config.AI) if err != nil { - response.InternalServerError(w, "error while querying agent", err) + response.InternalServerError(w, r, "error while querying agent", err) // log the error to Axiom config.AxiomLogger.IngestEvents(r.Context(), "ai-logs", []axiom.Event{ { @@ -209,7 +209,7 @@ func Agent(app *config.Application) http.HandlerFunc { }, }) - response.OK(w, "Agent query successful", AIresponse) + response.OK(w, r, "Agent query successful", AIresponse) } } @@ -236,9 +236,9 @@ func AgentAccept(app *config.Application) http.HandlerFunc { err := AgentExec(projectUID, userID, config.AI) if err != nil { if err.Error() == "changes expired or not found" { - response.BadRequest(w, "No schema changes found or changes expired", nil) + response.BadRequest(w, r, "No schema changes found or changes expired", nil) } else { - response.InternalServerError(w, "error while executing agent", err) + response.InternalServerError(w, r, "error while executing agent", err) } // log the error to Axiom config.AxiomLogger.IngestEvents(r.Context(), "ai-logs", []axiom.Event{ @@ -261,7 +261,7 @@ func AgentAccept(app *config.Application) http.HandlerFunc { "message": "Agent query executed successfully", }, }) - response.OK(w, "query executed successfully", nil) + response.OK(w, r, "query executed successfully", nil) } } @@ -286,7 +286,7 @@ func AgentCancel(app *config.Application) http.HandlerFunc { // cancel the agent query err := ClearCacheForProject(projectUID) if err != nil { - response.InternalServerError(w, "error while cancelling agent query", err) + response.InternalServerError(w, r, "error while cancelling agent query", err) // log the error to Axiom config.AxiomLogger.IngestEvents(r.Context(), "ai-logs", []axiom.Event{ { @@ -309,6 +309,6 @@ func AgentCancel(app *config.Application) http.HandlerFunc { }, }) - response.OK(w, "Agent query cancelled successfully", nil) + response.OK(w, r, "Agent query cancelled successfully", nil) } } diff --git a/SqlEditor/handlers.go b/SqlEditor/handlers.go index 3bfe2cf..deef8b9 100644 --- a/SqlEditor/handlers.go +++ b/SqlEditor/handlers.go @@ -30,26 +30,26 @@ func RunSqlQuery(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 } // Get the request body var RequestBody RequestBody if err := json.NewDecoder(r.Body).Decode(&RequestBody); err != nil { - response.BadRequest(w, "Invalid request body", nil) + response.BadRequest(w, r, "Invalid request body", nil) return } if RequestBody.Query == "" { - response.BadRequest(w, "Query is required", nil) + response.BadRequest(w, r, "Query is required", nil) return } // Validate query for dangerous operations isValid, err := ValidateQuery(RequestBody.Query) if !isValid { - response.BadRequest(w, err.Error(), nil) + response.BadRequest(w, r, err.Error(), nil) return } @@ -60,7 +60,7 @@ func RunSqlQuery(app *config.Application) http.HandlerFunc { return } - response.OK(w, "Query executed successfully", queryResponse) + response.OK(w, r, "Query executed successfully", queryResponse) config.App.InfoLog.Println("Query executed successfully for project:", projectOid) } } diff --git a/accounts/handlers.go b/accounts/handlers.go index bc0776b..82f15e1 100644 --- a/accounts/handlers.go +++ b/accounts/handlers.go @@ -31,7 +31,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 +43,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 +62,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 +89,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 +125,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) + 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 +170,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 +180,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 +209,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 +244,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 +277,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 +291,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 +323,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 +345,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 +377,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 +387,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/analytics/handlers.go b/analytics/handlers.go index 5f084fd..5d377f9 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", storage) } } @@ -62,7 +62,7 @@ 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 } @@ -72,7 +72,7 @@ func ExecutionTime(app *config.Application) http.HandlerFunc { return } - response.OK(w, "Execution time statistics retrieved successfully", stats) + response.OK(w, r, "Execution time statistics retrieved successfully", stats) } } @@ -94,7 +94,7 @@ 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 } @@ -104,6 +104,6 @@ func DatabaseUsage(app *config.Application) http.HandlerFunc { return } - response.OK(w, "Database usage statistics retrieved successfully", stats) + response.OK(w, r, "Database usage statistics retrieved successfully", stats) } } 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/middleware/middleware.go b/middleware/middleware.go index 1a3e313..8a8a501 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) @@ -46,24 +46,24 @@ func CheckOwnership(next http.Handler) http.Handler { // check if the project exists exists, err := utils.CheckProjectExist(r.Context(), projectOID, config.DB) if err != nil { - response.InternalServerError(w, "Failed to check project existence", 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, "Project not found", nil) + 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, err.Error(), err) + 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) @@ -79,30 +79,30 @@ func CheckOTableExist(next http.Handler) http.Handler { // get the project id from the database _, projectId, err := utils.GetProjectNameID(r.Context(), projectOID, config.DB) if err != nil { - response.InternalServerError(w, "Failed to get project ID", err) + 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, "Failed to check table existence", err) + 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, "Table not found", nil) + 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, err.Error(), err) + response.InternalServerError(w, r, err.Error(), err) return } if !ok { - response.NotFound(w, "Table not found", nil) + response.NotFound(w, r, "Table not found", nil) return } } 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..542812f 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", errors.New("Project creation failed")) } 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/response/errors.go b/response/errors.go index ca200e8..382504c 100644 --- a/response/errors.go +++ b/response/errors.go @@ -12,28 +12,28 @@ var ( // 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..c007497 100644 --- a/response/response.go +++ b/response/response.go @@ -1,8 +1,13 @@ package response import ( + "DBHS/config" "encoding/json" "net/http" + "time" + + "github.com/axiomhq/axiom-go/axiom" + "github.com/axiomhq/axiom-go/axiom/ingest" ) type Response struct { @@ -28,21 +33,34 @@ 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, + } if err != nil { response = &Response{ Status: status, Error: err.Error(), } + // log the error to axiom + event["error"] = err.Error() } else { response = &Response{ Status: status, Data: data, } + jsonResponse, _ := json.Marshal(response) + event["response"] = string(jsonResponse) } if message != "" { response.Message = message } + event["massage"] = response.Message + config.AxiomLogger.IngestEvents(r.Context(), "api", []axiom.Event{event}) SendResponse(w, status, headers, response) } 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/tables/handlers.go b/tables/handlers.go index f993bd7..9ae5241 100644 --- a/tables/handlers.go +++ b/tables/handlers.go @@ -28,24 +28,24 @@ func GetAllTablesHandler(app *config.Application) http.HandlerFunc { urlVariables := mux.Vars(r) projectId := urlVariables["project_id"] if projectId == "" { - response.NotFound(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, "", data) + response.OK(w, r, "", data) } } @@ -69,22 +69,22 @@ func GetTableSchemaHandler(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 } data, err := GetTableSchema(r.Context(), projectId, tableId, 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 schema:", err) - response.InternalServerError(w, "Could not read table schema", err) + response.InternalServerError(w, r, "Could not read table schema", err) return } - response.OK(w, "Table Schema Read Successfully", data) + response.OK(w, r, "Table Schema Read Successfully", data) } } @@ -108,13 +108,13 @@ func CreateTableHandler(app *config.Application) http.HandlerFunc { table := Table{} // 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) + 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 } @@ -122,22 +122,22 @@ 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, }) } @@ -166,7 +166,7 @@ func UpdateTableHandler(app *config.Application) http.HandlerFunc { 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 } @@ -175,22 +175,22 @@ func UpdateTableHandler(app *config.Application) http.HandlerFunc { projectOID := urlVariables["project_id"] tableId := urlVariables["table_id"] if projectOID == "" || 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 } // Call the service function to update the table 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) } } @@ -214,21 +214,21 @@ func DeleteTableHandler(app *config.Application) http.HandlerFunc { projectOID := urlVariables["project_id"] tableOID := urlVariables["table_id"] if projectOID == "" || tableOID == "" { - response.BadRequest(w, "Project ID and Table ID are required", nil) + response.BadRequest(w, r, "Project ID and Table ID are required", nil) return } // Call the service function to delete the table 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) } } @@ -257,13 +257,13 @@ 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 } @@ -271,14 +271,14 @@ 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, "Table Read Succesfully", data) + response.OK(w, r, "Table Read Succesfully", data) } } diff --git a/tables/middleware.go b/tables/middleware.go index f005a6b..bf2b754 100644 --- a/tables/middleware.go +++ b/tables/middleware.go @@ -15,25 +15,25 @@ func SyncTables(next http.Handler) http.Handler { requestVars := mux.Vars(r) projectOID := requestVars["project_id"] if projectOID == "" { - response.NotFound(w, "Project ID is required", nil) + 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, "Unauthorized", nil) + 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, "Failed to extract database connection", err) + 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, err.Error(), err) + response.InternalServerError(w, r, err.Error(), err) return } next.ServeHTTP(w, r) From 43d406943a017ca1313d4656d38b470ce547d3c5 Mon Sep 17 00:00:00 2001 From: rag-nar1 Date: Tue, 8 Jul 2025 15:32:18 +0300 Subject: [PATCH 039/101] fix: update ResponseHandler to include request context in error responses --- utils/utils.go | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/utils/utils.go b/utils/utils.go index e6db941..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) } } From fa802de962f76f5c39301005af6b3ba3a98b6499 Mon Sep 17 00:00:00 2001 From: rag-nar1 Date: Tue, 8 Jul 2025 15:53:24 +0300 Subject: [PATCH 040/101] fix: include request URI in response logging for better traceability --- response/response.go | 1 + 1 file changed, 1 insertion(+) diff --git a/response/response.go b/response/response.go index c007497..1f8e53a 100644 --- a/response/response.go +++ b/response/response.go @@ -41,6 +41,7 @@ func CreateResponse(w http.ResponseWriter, r *http.Request, status int, message "user-oid": r.Context().Value("user-oid"), "user-name": r.Context().Value("user-name"), "status-code": status, + "URI": r.RequestURI, } if err != nil { response = &Response{ From 37102e2443fcacecab437013a775cc79e7ebfa2c Mon Sep 17 00:00:00 2001 From: rag-nar1 Date: Tue, 8 Jul 2025 15:57:10 +0300 Subject: [PATCH 041/101] fix: update response logging to store response object directly --- response/response.go | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/response/response.go b/response/response.go index 1f8e53a..ee827a5 100644 --- a/response/response.go +++ b/response/response.go @@ -55,8 +55,7 @@ func CreateResponse(w http.ResponseWriter, r *http.Request, status int, message Status: status, Data: data, } - jsonResponse, _ := json.Marshal(response) - event["response"] = string(jsonResponse) + event["response"] = response } if message != "" { response.Message = message From 331eb7da078cbc14617ea02cdf91a52f11debc6b Mon Sep 17 00:00:00 2001 From: rag-nar1 Date: Tue, 8 Jul 2025 16:05:46 +0300 Subject: [PATCH 042/101] fix: include request header and body in event logging for better traceability --- response/response.go | 2 ++ 1 file changed, 2 insertions(+) diff --git a/response/response.go b/response/response.go index ee827a5..1d2c55c 100644 --- a/response/response.go +++ b/response/response.go @@ -42,6 +42,8 @@ func CreateResponse(w http.ResponseWriter, r *http.Request, status int, message "user-name": r.Context().Value("user-name"), "status-code": status, "URI": r.RequestURI, + "request-header": r.Header, + "request-body": r.Body, } if err != nil { response = &Response{ From 5b8a9bffd0f68dc4812f43ef7ba97f4009cf854d Mon Sep 17 00:00:00 2001 From: rag-nar1 Date: Tue, 8 Jul 2025 16:55:38 +0300 Subject: [PATCH 043/101] fix: add ownership check middleware to project routes for enhanced security --- projects/routes.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/projects/routes.go b/projects/routes.go index ac1cfda..f5df53d 100644 --- a/projects/routes.go +++ b/projects/routes.go @@ -8,7 +8,7 @@ import ( func DefineURLs() { router := config.Router.PathPrefix("/api/projects").Subrouter() - router.Use(middleware.JwtAuthMiddleware) + router.Use(middleware.JwtAuthMiddleware, middleware.CheckOwnership) router.Handle("", middleware.Route(map[string]http.HandlerFunc{ http.MethodPost: CreateProject(config.App), From ea6edffadcf4507aa739694eeaf1b98541ab6f16 Mon Sep 17 00:00:00 2001 From: rag-nar1 Date: Tue, 8 Jul 2025 20:49:21 +0300 Subject: [PATCH 044/101] fix: refactor project routes to separate ownership check middleware for clarity --- projects/routes.go | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/projects/routes.go b/projects/routes.go index f5df53d..14562f3 100644 --- a/projects/routes.go +++ b/projects/routes.go @@ -8,14 +8,16 @@ import ( func DefineURLs() { router := config.Router.PathPrefix("/api/projects").Subrouter() - router.Use(middleware.JwtAuthMiddleware, middleware.CheckOwnership) - + 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), From 583f487575d4c7168db049d119beb07c0fad21fd Mon Sep 17 00:00:00 2001 From: rag-nar1 Date: Wed, 9 Jul 2025 17:30:14 +0300 Subject: [PATCH 045/101] fix: ensure error level is logged in CreateResponse function for better error tracking --- response/response.go | 1 + 1 file changed, 1 insertion(+) diff --git a/response/response.go b/response/response.go index 1d2c55c..615c233 100644 --- a/response/response.go +++ b/response/response.go @@ -52,6 +52,7 @@ func CreateResponse(w http.ResponseWriter, r *http.Request, status int, message } // log the error to axiom event["error"] = err.Error() + event["level"] = "error" } else { response = &Response{ Status: status, From 7f021800666fb25902b465419c4c7e7c0ea62fd3 Mon Sep 17 00:00:00 2001 From: rag-nar1 Date: Wed, 9 Jul 2025 17:46:52 +0300 Subject: [PATCH 046/101] fix: refactor GetUser function to use pgxscan for improved database querying --- accounts/repository.go | 6 ++++-- accounts/service.go | 46 +++++------------------------------------- 2 files changed, 9 insertions(+), 43 deletions(-) diff --git a/accounts/repository.go b/accounts/repository.go index ccf0694..e890cfa 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,8 @@ 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) } From ddd8e8db6c459abf3457dcf6c3c49fd947f07e02 Mon Sep 17 00:00:00 2001 From: rag-nar1 Date: Wed, 9 Jul 2025 18:07:58 +0300 Subject: [PATCH 047/101] fix: update CreateResponse to log request body as structured data for better traceability --- response/response.go | 19 ++++++++++++++++++- 1 file changed, 18 insertions(+), 1 deletion(-) diff --git a/response/response.go b/response/response.go index 615c233..fe69004 100644 --- a/response/response.go +++ b/response/response.go @@ -3,6 +3,7 @@ package response import ( "DBHS/config" "encoding/json" + "io" "net/http" "time" @@ -35,6 +36,7 @@ func SendResponse(w http.ResponseWriter, status int, 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 + bodydata, _ := JsonString(r.Body) event := axiom.Event{ ingest.TimestampField: time.Now(), "user-id": r.Context().Value("user-id"), @@ -43,7 +45,7 @@ func CreateResponse(w http.ResponseWriter, r *http.Request, status int, message "status-code": status, "URI": r.RequestURI, "request-header": r.Header, - "request-body": r.Body, + "request-body": bodydata, } if err != nil { response = &Response{ @@ -67,3 +69,18 @@ func CreateResponse(w http.ResponseWriter, r *http.Request, status int, message config.AxiomLogger.IngestEvents(r.Context(), "api", []axiom.Event{event}) SendResponse(w, status, headers, response) } + +func JsonString(body io.ReadCloser) (map[string]interface{}, error) { + + bodyBytes, err := io.ReadAll(body) + if err != nil { + return nil, err + } + defer body.Close() + + var data map[string]interface{} + if err := json.Unmarshal(bodyBytes, &data); err != nil { + return nil, err + } + return data, nil +} \ No newline at end of file From 8145a666503f62e6ccd996fef2ed01ffb0bbf4ac Mon Sep 17 00:00:00 2001 From: rag-nar1 Date: Wed, 9 Jul 2025 18:18:38 +0300 Subject: [PATCH 048/101] fix: update JsonString function to use 'any' type for improved type flexibility --- response/response.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/response/response.go b/response/response.go index fe69004..0de5e43 100644 --- a/response/response.go +++ b/response/response.go @@ -70,7 +70,7 @@ func CreateResponse(w http.ResponseWriter, r *http.Request, status int, message SendResponse(w, status, headers, response) } -func JsonString(body io.ReadCloser) (map[string]interface{}, error) { +func JsonString(body io.ReadCloser) (map[string]any, error) { bodyBytes, err := io.ReadAll(body) if err != nil { @@ -78,7 +78,7 @@ func JsonString(body io.ReadCloser) (map[string]interface{}, error) { } defer body.Close() - var data map[string]interface{} + var data map[string]any if err := json.Unmarshal(bodyBytes, &data); err != nil { return nil, err } From e963144db15419a86169497059485660025807b8 Mon Sep 17 00:00:00 2001 From: rag-nar1 Date: Wed, 9 Jul 2025 21:35:53 +0300 Subject: [PATCH 049/101] fix: update database connection configuration to set default query execution mode --- config/application.go | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/config/application.go b/config/application.go index 9e4ad36..c26b335 100644 --- a/config/application.go +++ b/config/application.go @@ -120,6 +120,7 @@ func Init(infoLog, errorLog *log.Logger) { infoLog.Println("Connected to Admin PostgreSQL successfully! ✅") + // --------------------------------------- DataBase Pool Manager ----------------------------------------- // ConfigManager, err = NewUserDbConfig(adminConnStr) @@ -147,12 +148,13 @@ func Init(infoLog, errorLog *log.Logger) { // 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.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) From 86848dfaa558d27446bce2bb1d8ac45b885cb80a Mon Sep 17 00:00:00 2001 From: GergesHany Date: Wed, 9 Jul 2025 21:51:16 +0300 Subject: [PATCH 050/101] refactor(config): remove unused prepared statement cleanup code --- config/application.go | 13 ------------- 1 file changed, 13 deletions(-) diff --git a/config/application.go b/config/application.go index c26b335..a996c60 100644 --- a/config/application.go +++ b/config/application.go @@ -120,7 +120,6 @@ func Init(infoLog, errorLog *log.Logger) { infoLog.Println("Connected to Admin PostgreSQL successfully! ✅") - // --------------------------------------- DataBase Pool Manager ----------------------------------------- // ConfigManager, err = NewUserDbConfig(adminConnStr) @@ -142,18 +141,6 @@ 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 { From 7acba33dfee08e0b08ed2c6d1d6935deab80245e Mon Sep 17 00:00:00 2001 From: GergesHany Date: Wed, 9 Jul 2025 22:01:03 +0300 Subject: [PATCH 051/101] refactor(config): remove unused HTTP error helper functions --- config/helpers.go | 22 ---------------------- 1 file changed, 22 deletions(-) delete mode 100644 config/helpers.go 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) -} From 335a61d6f9ac89de34524c8c244ef9e30f4cfcd5 Mon Sep 17 00:00:00 2001 From: GergesHany Date: Wed, 9 Jul 2025 22:06:37 +0300 Subject: [PATCH 052/101] refactor(sqleditor): remove empty SqlQueries.go file --- SqlEditor/SqlQueries.go | 1 - 1 file changed, 1 deletion(-) delete mode 100644 SqlEditor/SqlQueries.go diff --git a/SqlEditor/SqlQueries.go b/SqlEditor/SqlQueries.go deleted file mode 100644 index 1a4bc9e..0000000 --- a/SqlEditor/SqlQueries.go +++ /dev/null @@ -1 +0,0 @@ -package sqleditor From 4eda04eb2dd35472d0b06972b7ce8b98270e0ffb Mon Sep 17 00:00:00 2001 From: rag-nar1 Date: Thu, 10 Jul 2025 00:59:47 +0300 Subject: [PATCH 053/101] fix(schema): update GetTableConstraints to set ColumnName for CHECK constraints --- utils/schema.go | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/utils/schema.go b/utils/schema.go index 871c04d..e8eded9 100644 --- a/utils/schema.go +++ b/utils/schema.go @@ -349,6 +349,12 @@ func GetTableConstraints(ctx context.Context, tableName string, db Querier) ([]C 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] + } + } + return constraints, nil } From 83a6c8c1c7cd45963ab54b4fe6116e7c306fdf1c Mon Sep 17 00:00:00 2001 From: rag-nar1 Date: Thu, 10 Jul 2025 13:12:59 +0300 Subject: [PATCH 054/101] fix(schema): update GetTableConstraints to handle NOT NULL constraints --- utils/schema.go | 3 +++ 1 file changed, 3 insertions(+) diff --git a/utils/schema.go b/utils/schema.go index e8eded9..fd68ed7 100644 --- a/utils/schema.go +++ b/utils/schema.go @@ -352,6 +352,9 @@ func GetTableConstraints(ctx context.Context, tableName string, db Querier) ([]C 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" + } } } From 078054e889637e74a23f9da3d0e09b490c99385a Mon Sep 17 00:00:00 2001 From: rag-nar1 Date: Thu, 10 Jul 2025 14:05:28 +0300 Subject: [PATCH 055/101] feat(validation): add non-negative number check for pagination parameters --- tables/handlers.go | 9 +++++++++ tables/repository.go | 30 +++++++++++++++--------------- tables/utils.go | 15 +++++++++++++++ 3 files changed, 39 insertions(+), 15 deletions(-) diff --git a/tables/handlers.go b/tables/handlers.go index 9ae5241..027c615 100644 --- a/tables/handlers.go +++ b/tables/handlers.go @@ -267,6 +267,15 @@ func ReadTableHandler(app *config.Application) http.HandlerFunc { return } + 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 + } + // Call the service function to read the table data, err := ReadTable(r.Context(), projectId, tableId, parameters, config.DB) if err != nil { diff --git a/tables/repository.go b/tables/repository.go index 836f4ed..87edafd 100644 --- a/tables/repository.go +++ b/tables/repository.go @@ -18,7 +18,7 @@ func GetAllTablesRepository(ctx context.Context, projectId int64, userDb utils.Q if err != nil { return nil, err } - + // extract the table schema tableSchema, err := utils.GetTables(ctx, userDb) if err != nil { @@ -43,21 +43,20 @@ func GetAllTablesRepository(ctx context.Context, projectId int64, userDb utils.Q // 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) + 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] @@ -158,7 +157,7 @@ func ReadTableData(ctx context.Context, tableName string, parameters map[string] if columns == nil { return nil, err } - + data := Data{ Columns: make([]ShowColumn, len(columns)), } @@ -213,6 +212,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 diff --git a/tables/utils.go b/tables/utils.go index bf0f0b9..ef1c867 100644 --- a/tables/utils.go +++ b/tables/utils.go @@ -4,9 +4,11 @@ import ( "DBHS/config" "DBHS/utils" "context" + "errors" "fmt" "log" "slices" + "strconv" "strings" "github.com/georgysavva/scany/v2/pgxscan" @@ -182,5 +184,18 @@ func SyncTableSchemas(ctx context.Context, projectId int64, servDb utils.Querier 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 From c5896aab6ba1e684e261296a9651da6c63428491 Mon Sep 17 00:00:00 2001 From: rag-nar1 Date: Thu, 10 Jul 2025 14:27:45 +0300 Subject: [PATCH 056/101] fix(handlers): enhance error handling in SignIn function to include scan errors --- accounts/handlers.go | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/accounts/handlers.go b/accounts/handlers.go index 82f15e1..7f7d42d 100644 --- a/accounts/handlers.go +++ b/accounts/handlers.go @@ -8,9 +8,11 @@ import ( "encoding/json" "errors" "fmt" + "net/http" + "strings" + "github.com/gorilla/mux" "github.com/redis/go-redis/v9" - "net/http" ) // GetUserData godoc @@ -136,7 +138,7 @@ func SignIn(app *config.Application) http.HandlerFunc { 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" { + 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 } From e029e7ee623847653d76c695ed3de27ecdea2f94 Mon Sep 17 00:00:00 2001 From: rag-nar1 Date: Thu, 10 Jul 2025 18:34:54 +0300 Subject: [PATCH 057/101] fix(repository): add logging for query execution in ReadTableData function --- tables/repository.go | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/tables/repository.go b/tables/repository.go index 87edafd..471fe86 100644 --- a/tables/repository.go +++ b/tables/repository.go @@ -5,6 +5,7 @@ import ( "DBHS/utils" "context" "fmt" + "log" "slices" "strconv" "strings" @@ -142,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 @@ -176,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 } From c7055d22434f3195cbde8abd8fd53fdffa0b195b Mon Sep 17 00:00:00 2001 From: rag-nar1 Date: Thu, 10 Jul 2025 18:37:09 +0300 Subject: [PATCH 058/101] fix(response): add HTTP method to event logging in CreateResponse function --- response/response.go | 1 + 1 file changed, 1 insertion(+) diff --git a/response/response.go b/response/response.go index 0de5e43..e1b01f7 100644 --- a/response/response.go +++ b/response/response.go @@ -43,6 +43,7 @@ func CreateResponse(w http.ResponseWriter, r *http.Request, status int, message "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": bodydata, From 2f9844dc5347abe00732f59523155be58b51203c Mon Sep 17 00:00:00 2001 From: rag-nar1 Date: Thu, 10 Jul 2025 20:45:52 +0300 Subject: [PATCH 059/101] feat(tables): implement InsertRowHandler and InserNewRow service function for adding new rows to tables --- .gitignore | 1 + config/application.go | 2 +- response/errors.go | 1 + tables/SqlQueries.go | 3 +++ tables/handlers.go | 42 ++++++++++++++++++++++++++++++++++++++---- tables/repository.go | 22 ++++++++++++++++++++++ tables/routes.go | 1 + tables/service.go | 34 ++++++++++++++++++++++++++++++++++ 8 files changed, 101 insertions(+), 5 deletions(-) diff --git a/.gitignore b/.gitignore index 7eb430d..ac2191d 100644 --- a/.gitignore +++ b/.gitignore @@ -10,3 +10,4 @@ curl.sh *.log pr.md response.* +/tmp \ No newline at end of file diff --git a/config/application.go b/config/application.go index a996c60..7cce282 100644 --- a/config/application.go +++ b/config/application.go @@ -75,7 +75,7 @@ func loadEnv() { } const ( - deploy = true // Set to true if running in production, false for development + deploy = false // Set to true if running in production, false for development ) func Init(infoLog, errorLog *log.Logger) { diff --git a/response/errors.go b/response/errors.go index 382504c..7678c22 100644 --- a/response/errors.go +++ b/response/errors.go @@ -7,6 +7,7 @@ import ( var ( ErrUnauthorized = errors.New("Unauthorized") + ErrBadRequest = errors.New("BadRequest") ) // you can use one of these frequently used response for more code readability diff --git a/tables/SqlQueries.go b/tables/SqlQueries.go index 1b106db..c4cbaf8 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 027c615..2bb3624 100644 --- a/tables/handlers.go +++ b/tables/handlers.go @@ -10,7 +10,6 @@ import ( "github.com/gorilla/mux" ) - // GetAllTablesHandler godoc // @Summary Get all tables in a project // @Description Get a list of all tables in the specified project @@ -31,7 +30,7 @@ func GetAllTablesHandler(app *config.Application) http.HandlerFunc { 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) { @@ -143,7 +142,6 @@ func CreateTableHandler(app *config.Application) http.HandlerFunc { } } - // UpdateTableHandler godoc // @Summary Update an existing table // @Description Update table structure by adding, modifying, or deleting columns @@ -275,7 +273,7 @@ func ReadTableHandler(app *config.Application) http.HandlerFunc { response.BadRequest(w, r, "enter a valid limit number", nil) return } - + // Call the service function to read the table data, err := ReadTable(r.Context(), projectId, tableId, parameters, config.DB) if err != nil { @@ -291,3 +289,39 @@ func ReadTableHandler(app *config.Application) http.HandlerFunc { response.OK(w, r, "Table Read Succesfully", data) } } + +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 + } + + row := make(map[string]interface{}) + if err := json.NewDecoder(r.Body).Decode(&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.Created(w, r, "row created succefully", nil) + } +} diff --git a/tables/repository.go b/tables/repository.go index 471fe86..eb43718 100644 --- a/tables/repository.go +++ b/tables/repository.go @@ -276,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 ff0128c..75f9596 100644 --- a/tables/routes.go +++ b/tables/routes.go @@ -32,6 +32,7 @@ func DefineURLs() { 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{ diff --git a/tables/service.go b/tables/service.go index 83c6f86..5f77e39 100644 --- a/tables/service.go +++ b/tables/service.go @@ -234,3 +234,37 @@ func ReadTable(ctx context.Context, projectOID, tableOID string, parameters map[ 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) +} From eae4fc91c99bfcec1c357ef0d0fa230d2c3af6ff Mon Sep 17 00:00:00 2001 From: rag-nar1 Date: Thu, 10 Jul 2025 22:10:28 +0300 Subject: [PATCH 060/101] fix(config): update deploy flag to true for production environment --- config/application.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/config/application.go b/config/application.go index 7cce282..a996c60 100644 --- a/config/application.go +++ b/config/application.go @@ -75,7 +75,7 @@ func loadEnv() { } const ( - deploy = false // Set to true if running in production, false for development + deploy = true // Set to true if running in production, false for development ) func Init(infoLog, errorLog *log.Logger) { From 46d0be9e04eec7a2844418769e538a0fed65e3b7 Mon Sep 17 00:00:00 2001 From: rag-nar1 Date: Thu, 10 Jul 2025 22:18:32 +0300 Subject: [PATCH 061/101] feat(tables): add InsertRowHandler and RowValue struct for inserting new rows --- tables/handlers.go | 27 ++++++++++++++++++++++----- tables/models.go | 5 +++++ 2 files changed, 27 insertions(+), 5 deletions(-) diff --git a/tables/handlers.go b/tables/handlers.go index 2bb3624..f3eb852 100644 --- a/tables/handlers.go +++ b/tables/handlers.go @@ -88,8 +88,8 @@ func GetTableSchemaHandler(app *config.Application) http.HandlerFunc { } // CreateTableHandler godoc -// @Summary Create a new table -// @Description Create a new table in the specified project +// @Summary insert new row +// @Description insert new row in the specified project // @Tags tables // @Accept json // @Produce json @@ -244,9 +244,10 @@ func DeleteTableHandler(app *config.Application) http.HandlerFunc { // @Param filter query string false "Filter condition (e.g. name=value)" // @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) { @@ -290,6 +291,22 @@ func ReadTableHandler(app *config.Application) http.HandlerFunc { } } +// 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 table 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 diff --git a/tables/models.go b/tables/models.go index 9d7e212..fd91c11 100644 --- a/tables/models.go +++ b/tables/models.go @@ -94,3 +94,8 @@ type Data struct { Columns []ShowColumn `json:"columns"` Rows []map[string]interface{} `json:"rows"` } + +type RowValue struct { + ColumnName string `json: "column"` + Value interface{} `json: "value"` +} From 76090d03a62702da25f01c3561703423c1c2a25a Mon Sep 17 00:00:00 2001 From: rag-nar1 Date: Thu, 10 Jul 2025 22:24:31 +0300 Subject: [PATCH 062/101] fix(handlers): correct parameter type in ReadTableHandler documentation --- tables/handlers.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tables/handlers.go b/tables/handlers.go index f3eb852..58b950e 100644 --- a/tables/handlers.go +++ b/tables/handlers.go @@ -299,7 +299,7 @@ func ReadTableHandler(app *config.Application) http.HandlerFunc { // @Produce json // @Param project_id path string true "Project ID" // @Param table_id path string true "Table ID" -// @Param table body []RowValue true "Row information" +// @Param table body RowValue true "Row information" // @Security BearerAuth // @Success 200 {object} response.SuccessResponse // @Failure 400 {object} response.ErrorResponse400 From 5ae8ffd4e865d660c2e8ec1ffbda941ce7b235e3 Mon Sep 17 00:00:00 2001 From: rag-nar1 Date: Thu, 10 Jul 2025 22:25:07 +0300 Subject: [PATCH 063/101] fix(handlers): update parameter name in ReadTableHandler documentation --- tables/handlers.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tables/handlers.go b/tables/handlers.go index 58b950e..16e6a01 100644 --- a/tables/handlers.go +++ b/tables/handlers.go @@ -299,7 +299,7 @@ func ReadTableHandler(app *config.Application) http.HandlerFunc { // @Produce json // @Param project_id path string true "Project ID" // @Param table_id path string true "Table ID" -// @Param table body RowValue true "Row information" +// @Param row body RowValue true "Row information" // @Security BearerAuth // @Success 200 {object} response.SuccessResponse // @Failure 400 {object} response.ErrorResponse400 From 1200acb7cf783e11e8bc88a5ef86df14e48aee25 Mon Sep 17 00:00:00 2001 From: rag-nar1 Date: Fri, 11 Jul 2025 00:31:53 +0300 Subject: [PATCH 064/101] fix(config): set deploy flag to false for development environment docs: update descriptions and summaries in Swagger documentation feat(tables): add endpoint for inserting new rows with RowValue definition fix(handlers): correct summary and description in CreateTableHandler documentation --- config/application.go | 2 +- docs/docs.go | 119 ++++++++++++++++++++++++++++++++++++++---- docs/swagger.json | 119 ++++++++++++++++++++++++++++++++++++++---- docs/swagger.yaml | 85 ++++++++++++++++++++++++++---- tables/handlers.go | 13 ++--- tmp/air_errors.log | 2 +- 6 files changed, 302 insertions(+), 38 deletions(-) diff --git a/config/application.go b/config/application.go index a996c60..7cce282 100644 --- a/config/application.go +++ b/config/application.go @@ -75,7 +75,7 @@ func loadEnv() { } const ( - deploy = true // Set to true if running in production, false for development + deploy = false // Set to true if running in production, false for development ) func Init(infoLog, errorLog *log.Logger) { diff --git a/docs/docs.go b/docs/docs.go index 9079a25..5d1e5ca 100644 --- a/docs/docs.go +++ b/docs/docs.go @@ -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", @@ -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" } } } @@ -341,19 +347,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" } } } @@ -433,6 +445,84 @@ const docTemplate = `{ } } }, + "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": { + "$ref": "#/definitions/tables.RowValue" + } + } + } + ], + "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" + } + } + } + }, "delete": { "security": [ { @@ -2749,6 +2839,15 @@ const docTemplate = `{ } } }, + "tables.RowValue": { + "type": "object", + "properties": { + "columnName": { + "type": "string" + }, + "value": {} + } + }, "tables.ShowColumn": { "type": "object", "properties": { diff --git a/docs/swagger.json b/docs/swagger.json index d3e839e..a750011 100644 --- a/docs/swagger.json +++ b/docs/swagger.json @@ -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", @@ -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" } } } @@ -334,19 +340,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" } } } @@ -426,6 +438,84 @@ } } }, + "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": { + "$ref": "#/definitions/tables.RowValue" + } + } + } + ], + "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" + } + } + } + }, "delete": { "security": [ { @@ -2742,6 +2832,15 @@ } } }, + "tables.RowValue": { + "type": "object", + "properties": { + "columnName": { + "type": "string" + }, + "value": {} + } + }, "tables.ShowColumn": { "type": "object", "properties": { diff --git a/docs/swagger.yaml b/docs/swagger.yaml index 7b6974d..6e7b9b2 100644 --- a/docs/swagger.yaml +++ b/docs/swagger.yaml @@ -384,6 +384,12 @@ definitions: type: object type: array type: object + tables.RowValue: + properties: + columnName: + type: string + value: {} + type: object tables.ShowColumn: properties: name: @@ -631,7 +637,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 @@ -652,20 +658,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}: @@ -760,20 +770,75 @@ 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: + $ref: '#/definitions/tables.RowValue' + 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 diff --git a/tables/handlers.go b/tables/handlers.go index 16e6a01..7f6c2c2 100644 --- a/tables/handlers.go +++ b/tables/handlers.go @@ -88,8 +88,8 @@ func GetTableSchemaHandler(app *config.Application) http.HandlerFunc { } // CreateTableHandler godoc -// @Summary insert new row -// @Description insert new row in the specified project +// @Summary Create new table +// @Description Create new table in the specified project // @Tags tables // @Accept json // @Produce json @@ -97,9 +97,10 @@ func GetTableSchemaHandler(app *config.Application) http.HandlerFunc { // @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) { @@ -299,7 +300,7 @@ func ReadTableHandler(app *config.Application) http.HandlerFunc { // @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" +// @Param row body []RowValue true "Row information" // @Security BearerAuth // @Success 200 {object} response.SuccessResponse // @Failure 400 {object} response.ErrorResponse400 diff --git a/tmp/air_errors.log b/tmp/air_errors.log index d15f370..8d13f34 100644 --- a/tmp/air_errors.log +++ b/tmp/air_errors.log @@ -4,4 +4,4 @@ exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1ex ======= exit status 1 >>>>>>> logging -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 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 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 +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 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 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 From 50f625ac5fe08f4550e2144b3485bf1fad8a9d25 Mon Sep 17 00:00:00 2001 From: rag-nar1 Date: Fri, 11 Jul 2025 00:32:37 +0300 Subject: [PATCH 065/101] fix(config): update deploy flag to true for production environment --- config/application.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/config/application.go b/config/application.go index 7cce282..a996c60 100644 --- a/config/application.go +++ b/config/application.go @@ -75,7 +75,7 @@ func loadEnv() { } const ( - deploy = false // Set to true if running in production, false for development + deploy = true // Set to true if running in production, false for development ) func Init(infoLog, errorLog *log.Logger) { From 73706a9be9de4919bced8414a17a6b83b162cd39 Mon Sep 17 00:00:00 2001 From: rag-nar1 Date: Fri, 11 Jul 2025 00:43:13 +0300 Subject: [PATCH 066/101] fix(response): update JsonString function to return string instead of map --- response/response.go | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/response/response.go b/response/response.go index e1b01f7..c249f8b 100644 --- a/response/response.go +++ b/response/response.go @@ -71,17 +71,17 @@ func CreateResponse(w http.ResponseWriter, r *http.Request, status int, message SendResponse(w, status, headers, response) } -func JsonString(body io.ReadCloser) (map[string]any, error) { +func JsonString(body io.ReadCloser) (string, error) { bodyBytes, err := io.ReadAll(body) if err != nil { - return nil, err + return "nil", err } defer body.Close() - var data map[string]any - if err := json.Unmarshal(bodyBytes, &data); err != nil { - return nil, err + var data []byte + if data, err = json.Marshal(bodyBytes); err != nil { + return "", err } - return data, nil + return string(data), nil } \ No newline at end of file From e0aa85453e41ec713d9f34b0d5b4f8e90d5d0b30 Mon Sep 17 00:00:00 2001 From: rag-nar1 Date: Fri, 11 Jul 2025 00:46:49 +0300 Subject: [PATCH 067/101] fix(response): simplify JsonString function to return raw string from body --- response/response.go | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/response/response.go b/response/response.go index c249f8b..875e302 100644 --- a/response/response.go +++ b/response/response.go @@ -79,9 +79,5 @@ func JsonString(body io.ReadCloser) (string, error) { } defer body.Close() - var data []byte - if data, err = json.Marshal(bodyBytes); err != nil { - return "", err - } - return string(data), nil + return string(bodyBytes), nil } \ No newline at end of file From bc1bea299e19caf29572a46f5591d42535cd42c6 Mon Sep 17 00:00:00 2001 From: rag-nar1 Date: Fri, 11 Jul 2025 01:58:29 +0300 Subject: [PATCH 068/101] fix(dependencies): update AI-Agent to version 1.0.7 --- go.mod | 2 +- go.sum | 2 ++ 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/go.mod b/go.mod index 0d039d9..12734c4 100644 --- a/go.mod +++ b/go.mod @@ -6,7 +6,7 @@ go 1.24.0 // update go version to 1.24.0 require ( - github.com/Database-Hosting-Services/AI-Agent v1.0.6 + github.com/Database-Hosting-Services/AI-Agent v1.0.7 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 diff --git a/go.sum b/go.sum index 5633ad9..ccacf13 100644 --- a/go.sum +++ b/go.sum @@ -14,6 +14,8 @@ cloud.google.com/go/longrunning v0.5.7/go.mod h1:8GClkudohy1Fxm3owmBGid8W0pSgodE github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= github.com/Database-Hosting-Services/AI-Agent v1.0.6 h1:b38d2Z98DX8UvBDE/Km8aEDoGtCM5xVBWCeVNY27NuM= github.com/Database-Hosting-Services/AI-Agent v1.0.6/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/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= From 3a4d6464ff0c4f912f30666390dfb5e5dc918581 Mon Sep 17 00:00:00 2001 From: rag-nar1 Date: Fri, 11 Jul 2025 02:05:05 +0300 Subject: [PATCH 069/101] refactor(models): remove redundant fields from AgentResponse and adjust imports --- AI/models.go | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/AI/models.go b/AI/models.go index ffc1e74..653d26f 100644 --- a/AI/models.go +++ b/AI/models.go @@ -1,4 +1,9 @@ package ai + +import ( + "github.com/Database-Hosting-Services/AI-Agent/RAG" +) + const SENDER_TYPE_AI = "ai" const SENDER_TYPE_USER = "user" @@ -21,7 +26,5 @@ type Request struct { } type AgentResponse struct { - Response string `json:"response"` - SchemaChanges string `json:"schema_changes"` - SchemaDDL string `json:"schema_ddl"` + RAG.AgentResponse } \ No newline at end of file From 47eb87901daac96d17f80fe77d32d799045248e5 Mon Sep 17 00:00:00 2001 From: rag-nar1 Date: Fri, 11 Jul 2025 02:08:07 +0300 Subject: [PATCH 070/101] fix: ai agent json response format --- AI/models.go | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/AI/models.go b/AI/models.go index 653d26f..dfc3ed0 100644 --- a/AI/models.go +++ b/AI/models.go @@ -1,7 +1,7 @@ package ai import ( - "github.com/Database-Hosting-Services/AI-Agent/RAG" + "DBHS/utils" ) const SENDER_TYPE_AI = "ai" @@ -26,5 +26,7 @@ type Request struct { } type AgentResponse struct { - RAG.AgentResponse + Response string `json:"response"` + SchemaChanges []utils.Table `json:"schema_changes"` + SchemaDDL string `json:"schema_ddl"` } \ No newline at end of file From f689377ff45eb6c922becdf5a28d16db6c8df70e Mon Sep 17 00:00:00 2001 From: GergesHany Date: Fri, 11 Jul 2025 05:23:14 +0300 Subject: [PATCH 071/101] Update The /api/projects/{project_id}/sqlEditor/run-query endpoint method --- SqlEditor/routes.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/SqlEditor/routes.go b/SqlEditor/routes.go index ecaa196..5e86147 100644 --- a/SqlEditor/routes.go +++ b/SqlEditor/routes.go @@ -11,6 +11,6 @@ func DefineURLs() { router.Use(middleware.JwtAuthMiddleware) router.Handle("/run-query", middleware.Route(map[string]http.HandlerFunc{ - http.MethodGet: RunSqlQuery(config.App), + http.MethodPost: RunSqlQuery(config.App), })) } From 01bc90c0893c83597174254e1ea63026d548d968 Mon Sep 17 00:00:00 2001 From: GergesHany Date: Fri, 11 Jul 2025 05:23:33 +0300 Subject: [PATCH 072/101] Documentation Updates --- SqlEditor/handlers.go | 2 +- docs/docs.go | 7 +++++-- docs/swagger.json | 7 +++++-- docs/swagger.yaml | 6 ++++-- 4 files changed, 15 insertions(+), 7 deletions(-) diff --git a/SqlEditor/handlers.go b/SqlEditor/handlers.go index deef8b9..5cdb0fd 100644 --- a/SqlEditor/handlers.go +++ b/SqlEditor/handlers.go @@ -24,7 +24,7 @@ import ( // @Failure 401 {object} response.ErrorResponse "Unauthorized access" // @Failure 404 {object} response.ErrorResponse "Project not found" // @Failure 500 {object} response.ErrorResponse "Internal server error or query execution failed" -// @Router /projects/{project_id}/sqlEditor/run-query [get] +// @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) diff --git a/docs/docs.go b/docs/docs.go index 5d1e5ca..b0dffb1 100644 --- a/docs/docs.go +++ b/docs/docs.go @@ -1777,7 +1777,7 @@ const docTemplate = `{ } }, "/projects/{project_id}/sqlEditor/run-query": { - "get": { + "post": { "security": [ { "BearerAuth": [] @@ -2569,7 +2569,10 @@ const docTemplate = `{ "type": "string" }, "schema_changes": { - "type": "string" + "type": "array", + "items": { + "$ref": "#/definitions/utils.Table" + } }, "schema_ddl": { "type": "string" diff --git a/docs/swagger.json b/docs/swagger.json index a750011..41e0572 100644 --- a/docs/swagger.json +++ b/docs/swagger.json @@ -1770,7 +1770,7 @@ } }, "/projects/{project_id}/sqlEditor/run-query": { - "get": { + "post": { "security": [ { "BearerAuth": [] @@ -2562,7 +2562,10 @@ "type": "string" }, "schema_changes": { - "type": "string" + "type": "array", + "items": { + "$ref": "#/definitions/utils.Table" + } }, "schema_ddl": { "type": "string" diff --git a/docs/swagger.yaml b/docs/swagger.yaml index 6e7b9b2..68423d5 100644 --- a/docs/swagger.yaml +++ b/docs/swagger.yaml @@ -208,7 +208,9 @@ definitions: response: type: string schema_changes: - type: string + items: + $ref: '#/definitions/utils.Table' + type: array schema_ddl: type: string type: object @@ -1626,7 +1628,7 @@ paths: tags: - indexes /projects/{project_id}/sqlEditor/run-query: - get: + post: consumes: - application/json description: Execute a dynamic SQL query against a specific project's PostgreSQL From 9c585bf2cddb94538d0aae977540d2e520caea8b Mon Sep 17 00:00:00 2001 From: GergesHany Date: Fri, 11 Jul 2025 05:29:35 +0300 Subject: [PATCH 073/101] refactor(schemas): remove unused getDatabaseByName function --- schemas/repository.go | 10 ---------- 1 file changed, 10 deletions(-) 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) From 95d73e687cff808ed3bff831cf9c41ac3f3c7992 Mon Sep 17 00:00:00 2001 From: GergesHany Date: Fri, 11 Jul 2025 05:32:03 +0300 Subject: [PATCH 074/101] refactor(schemas): remove empty services.go file --- schemas/services.go | 1 - 1 file changed, 1 deletion(-) delete mode 100644 schemas/services.go 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 From 292eb864aaeeb95620fce00a47be2d7de31bbe73 Mon Sep 17 00:00:00 2001 From: GergesHany Date: Fri, 11 Jul 2025 05:37:25 +0300 Subject: [PATCH 075/101] Remove CloseAllPools comment because this method has changed --- config/application.go | 1 - 1 file changed, 1 deletion(-) diff --git a/config/application.go b/config/application.go index a996c60..9975919 100644 --- a/config/application.go +++ b/config/application.go @@ -207,5 +207,4 @@ func CloseDB() { AdminDB.Close() App.InfoLog.Println("Admin database connection closed. 🔌") } - // config.CloseAllPools(); } From c2c8db398a2b110c69e9efa516246ebec6a3acc5 Mon Sep 17 00:00:00 2001 From: GergesHany Date: Fri, 11 Jul 2025 16:57:25 +0300 Subject: [PATCH 076/101] feat(SqlEditor); Add proper error handling for syntax and permission issues --- SqlEditor/repository.go | 39 ++++++++- go.mod | 39 +++++---- go.sum | 174 +++++++++++++++++----------------------- tmp/air_errors.log | 2 +- 4 files changed, 133 insertions(+), 121 deletions(-) diff --git a/SqlEditor/repository.go b/SqlEditor/repository.go index 0106626..45e0fce 100644 --- a/SqlEditor/repository.go +++ b/SqlEditor/repository.go @@ -5,19 +5,50 @@ import ( "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) { - startTime := time.Now() - query = WrapQueryWithJSONAgg(query) + // 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 - err := conn.QueryRow(ctx, query).Scan(&result) + startTime := time.Now() + err := conn.QueryRow(ctx, wrappedQuery).Scan(&result) if err != nil { - return ResponseBody{}, *api.NewApiError("Internal server error", 500, errors.New("failed to execute query: "+err.Error())) + 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())) + } + return ResponseBody{}, *api.NewApiError("Query execution failed", 500, errors.New("failed to execute query: "+err.Error())) } // Extract column names from the JSON result diff --git a/go.mod b/go.mod index 12734c4..05374d9 100644 --- a/go.mod +++ b/go.mod @@ -1,6 +1,8 @@ module DBHS -go 1.24.0 +go 1.24.4 + +toolchain go1.24.5 // go 1.23 // update go version to 1.24.0 @@ -25,17 +27,18 @@ require ( ) 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.3 // indirect @@ -44,13 +47,11 @@ require ( 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/go-querystring v1.1.0 // 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/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 @@ -60,25 +61,35 @@ require ( 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 - 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/pingcap/tidb/parser v0.0.0-20231013125129-93a834a6bf8d // 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/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.34.0 // indirect - google.golang.org/api v0.186.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 ccacf13..088debe 100644 --- a/go.sum +++ b/go.sum @@ -1,19 +1,16 @@ -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.6 h1:b38d2Z98DX8UvBDE/Km8aEDoGtCM5xVBWCeVNY27NuM= -github.com/Database-Hosting-Services/AI-Agent v1.0.6/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/KyleBanks/depth v1.2.1 h1:5h8fQADFrWtarTdtDudMmGsC7GPbOAu6RVB3ffsVFHc= @@ -25,6 +22,7 @@ github.com/apapsch/go-jsonmerge/v2 v2.0.0 h1:axGnT1gRIfimI7gJifB699GoE/oq+F2MU7D 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= @@ -34,22 +32,18 @@ github.com/cenkalti/backoff/v4 v4.3.0 h1:MyRJ/UdXutAwSAT+s3wNd7MfTIcy71VQueUuFK3 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/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= 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= @@ -71,44 +65,23 @@ 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/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/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.2/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.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= 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.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/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= @@ -128,8 +101,11 @@ github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFF 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= @@ -140,46 +116,55 @@ 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/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= -github.com/tidwall/gjson v1.14.4 h1:uo0p8EbA09J7RQaflQ1aBRffTR7xedD2bcIVSYxLnkM= -github.com/tidwall/gjson v1.14.4/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk= +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 h1:RWIZEg2iJ8/g6fDDYzMpobmaoGh5OLl4AXtGUGPcqCs= 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.opencensus.io v0.24.0 h1:y73uSU6J157QMP2kn2r30vwW1A2W2WFwSCGnAVxeaD0= -go.opencensus.io v0.24.0/go.mod h1:vNK8G9p7aAivkbmorf4v+7Hgx+Zs0yY+0fOtgBfjQKo= 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/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= @@ -198,90 +183,75 @@ go.opentelemetry.io/otel/trace v1.37.0 h1:HLdcFNbRQBE2imdSEgm/kwqmQj1Or1l/7bW6mx 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/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.12.0 h1:ScB/8o8olJvc+CQPWrK3fPZNfh7qgwCrY0zJmoEQLSE= golang.org/x/time v0.12.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/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.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/tmp/air_errors.log b/tmp/air_errors.log index 8d13f34..a56fdea 100644 --- a/tmp/air_errors.log +++ b/tmp/air_errors.log @@ -4,4 +4,4 @@ exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1ex ======= exit status 1 >>>>>>> logging -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 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 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 +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 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 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 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 From 235475060d05af387f25340e05c7998e054b18ea Mon Sep 17 00:00:00 2001 From: GergesHany Date: Fri, 11 Jul 2025 17:00:28 +0300 Subject: [PATCH 077/101] Documentation Updates --- SqlEditor/handlers.go | 21 +++++++++++++----- docs/docs.go | 51 ++++++++++++++++++++++++++++++++++++++----- docs/swagger.json | 51 ++++++++++++++++++++++++++++++++++++++----- docs/swagger.yaml | 41 +++++++++++++++++++++++++++++----- 4 files changed, 141 insertions(+), 23 deletions(-) diff --git a/SqlEditor/handlers.go b/SqlEditor/handlers.go index 5cdb0fd..bad16fb 100644 --- a/SqlEditor/handlers.go +++ b/SqlEditor/handlers.go @@ -10,20 +10,31 @@ import ( "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 +// @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 "Query executed successfully" -// @Failure 400 {object} response.ErrorResponse "Project ID is missing or invalid request body" +// @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" -// @Failure 500 {object} response.ErrorResponse "Internal server error or query execution failed" +// @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) { diff --git a/docs/docs.go b/docs/docs.go index b0dffb1..288074b 100644 --- a/docs/docs.go +++ b/docs/docs.go @@ -1783,7 +1783,7 @@ const docTemplate = `{ "BearerAuth": [] } ], - "description": "Execute a dynamic SQL query against a specific project's PostgreSQL database and return structured JSON results with metadata", + "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" ], @@ -1814,13 +1814,25 @@ const docTemplate = `{ ], "responses": { "200": { - "description": "Query executed successfully", + "description": "Query executed successfully with results, column names, and execution time", "schema": { - "$ref": "#/definitions/response.SuccessResponse" + "allOf": [ + { + "$ref": "#/definitions/response.SuccessResponse" + }, + { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/sqleditor.ResponseBodySwagger" + } + } + } + ] } }, "400": { - "description": "Project ID is missing or invalid request body", + "description": "Project ID missing, invalid request body, empty query, or dangerous SQL operations detected", "schema": { "$ref": "#/definitions/response.ErrorResponse" } @@ -1832,13 +1844,13 @@ const docTemplate = `{ } }, "404": { - "description": "Project not found", + "description": "Project not found or referenced table/column does not exist", "schema": { "$ref": "#/definitions/response.ErrorResponse" } }, "500": { - "description": "Internal server error or query execution failed", + "description": "Internal server error, query execution failed, or error parsing results", "schema": { "$ref": "#/definitions/response.ErrorResponse" } @@ -2824,6 +2836,33 @@ const docTemplate = `{ } } }, + "sqleditor.ResponseBodySwagger": { + "description": "SQL query execution response with results and metadata", + "type": "object", + "properties": { + "column_names": { + "description": "Names of columns in the result set", + "type": "array", + "items": { + "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\"}]" + } + } + }, "tables.Data": { "type": "object", "properties": { diff --git a/docs/swagger.json b/docs/swagger.json index 41e0572..8c51da4 100644 --- a/docs/swagger.json +++ b/docs/swagger.json @@ -1776,7 +1776,7 @@ "BearerAuth": [] } ], - "description": "Execute a dynamic SQL query against a specific project's PostgreSQL database and return structured JSON results with metadata", + "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" ], @@ -1807,13 +1807,25 @@ ], "responses": { "200": { - "description": "Query executed successfully", + "description": "Query executed successfully with results, column names, and execution time", "schema": { - "$ref": "#/definitions/response.SuccessResponse" + "allOf": [ + { + "$ref": "#/definitions/response.SuccessResponse" + }, + { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/sqleditor.ResponseBodySwagger" + } + } + } + ] } }, "400": { - "description": "Project ID is missing or invalid request body", + "description": "Project ID missing, invalid request body, empty query, or dangerous SQL operations detected", "schema": { "$ref": "#/definitions/response.ErrorResponse" } @@ -1825,13 +1837,13 @@ } }, "404": { - "description": "Project not found", + "description": "Project not found or referenced table/column does not exist", "schema": { "$ref": "#/definitions/response.ErrorResponse" } }, "500": { - "description": "Internal server error or query execution failed", + "description": "Internal server error, query execution failed, or error parsing results", "schema": { "$ref": "#/definitions/response.ErrorResponse" } @@ -2817,6 +2829,33 @@ } } }, + "sqleditor.ResponseBodySwagger": { + "description": "SQL query execution response with results and metadata", + "type": "object", + "properties": { + "column_names": { + "description": "Names of columns in the result set", + "type": "array", + "items": { + "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\"}]" + } + } + }, "tables.Data": { "type": "object", "properties": { diff --git a/docs/swagger.yaml b/docs/swagger.yaml index 68423d5..624fe98 100644 --- a/docs/swagger.yaml +++ b/docs/swagger.yaml @@ -374,6 +374,26 @@ definitions: query: type: string type: object + sqleditor.ResponseBodySwagger: + description: SQL query execution response with results and metadata + properties: + column_names: + description: Names of columns in the result set + example: + - '["id"' + - '"name"]' + items: + 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: columns: @@ -1632,7 +1652,8 @@ paths: consumes: - application/json description: Execute a dynamic SQL query against a specific project's PostgreSQL - database and return structured JSON results with metadata + database and return structured JSON results with metadata including column + names and execution time parameters: - description: Project ID (OID) in: path @@ -1649,11 +1670,18 @@ paths: - application/json responses: "200": - description: Query executed successfully + description: Query executed successfully with results, column names, and + execution time schema: - $ref: '#/definitions/response.SuccessResponse' + allOf: + - $ref: '#/definitions/response.SuccessResponse' + - properties: + data: + $ref: '#/definitions/sqleditor.ResponseBodySwagger' + type: object "400": - description: Project ID is missing or invalid request body + description: Project ID missing, invalid request body, empty query, or dangerous + SQL operations detected schema: $ref: '#/definitions/response.ErrorResponse' "401": @@ -1661,11 +1689,12 @@ paths: schema: $ref: '#/definitions/response.ErrorResponse' "404": - description: Project not found + description: Project not found or referenced table/column does not exist schema: $ref: '#/definitions/response.ErrorResponse' "500": - description: Internal server error or query execution failed + description: Internal server error, query execution failed, or error parsing + results schema: $ref: '#/definitions/response.ErrorResponse' security: From c32a54865c954245a28b2e4fef51264525f91ada Mon Sep 17 00:00:00 2001 From: rag-nar1 Date: Fri, 11 Jul 2025 17:54:28 +0300 Subject: [PATCH 078/101] update the response logging to include the request body --- go.mod | 2 +- go.sum | 4 ++++ response/response.go | 13 +++++++------ 3 files changed, 12 insertions(+), 7 deletions(-) diff --git a/go.mod b/go.mod index 05374d9..cd62ea3 100644 --- a/go.mod +++ b/go.mod @@ -17,6 +17,7 @@ require ( github.com/gorilla/mux v1.8.1 github.com/jackc/pgx/v5 v5.7.5 github.com/joho/godotenv v1.5.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 @@ -64,7 +65,6 @@ require ( 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/pingcap/tidb/parser v0.0.0-20231013125129-93a834a6bf8d // 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 diff --git a/go.sum b/go.sum index 088debe..aa56394 100644 --- a/go.sum +++ b/go.sum @@ -61,6 +61,8 @@ 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/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= @@ -189,6 +191,8 @@ 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= diff --git a/response/response.go b/response/response.go index 875e302..82c3db1 100644 --- a/response/response.go +++ b/response/response.go @@ -2,6 +2,7 @@ package response import ( "DBHS/config" + "bytes" "encoding/json" "io" "net/http" @@ -72,12 +73,12 @@ func CreateResponse(w http.ResponseWriter, r *http.Request, status int, message } func JsonString(body io.ReadCloser) (string, error) { - - bodyBytes, err := io.ReadAll(body) - if err != nil { - return "nil", err - } + buf := new(bytes.Buffer) + _, err := io.Copy(buf, body) + if err != nil { + return "", nil + } defer body.Close() - return string(bodyBytes), nil + return buf.String(), nil } \ No newline at end of file From 4ba6544211c7c6495cd2e8e258d0b9b55dcbcbf8 Mon Sep 17 00:00:00 2001 From: rag-nar1 Date: Fri, 11 Jul 2025 18:12:56 +0300 Subject: [PATCH 079/101] update the response logging to include the request body --- response/response.go | 22 +++++++++++++++------- 1 file changed, 15 insertions(+), 7 deletions(-) diff --git a/response/response.go b/response/response.go index 82c3db1..6704053 100644 --- a/response/response.go +++ b/response/response.go @@ -2,8 +2,8 @@ package response import ( "DBHS/config" - "bytes" "encoding/json" + "fmt" "io" "net/http" "time" @@ -73,12 +73,20 @@ func CreateResponse(w http.ResponseWriter, r *http.Request, status int, message } func JsonString(body io.ReadCloser) (string, error) { - buf := new(bytes.Buffer) - _, err := io.Copy(buf, body) + 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 "", nil + return "", err } - defer body.Close() - - return buf.String(), nil + + return string(data), nil } \ No newline at end of file From e6e2c8e1aad38e16095e7cbfad7225076bacb0ce Mon Sep 17 00:00:00 2001 From: rag-nar1 Date: Fri, 11 Jul 2025 18:46:17 +0300 Subject: [PATCH 080/101] change refernces --- tables/handlers.go | 2 +- tables/models.go | 5 +---- 2 files changed, 2 insertions(+), 5 deletions(-) diff --git a/tables/handlers.go b/tables/handlers.go index 7f6c2c2..2ea980e 100644 --- a/tables/handlers.go +++ b/tables/handlers.go @@ -300,7 +300,7 @@ func ReadTableHandler(app *config.Application) http.HandlerFunc { // @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" +// @Param row body RowValue true "Row information" // @Security BearerAuth // @Success 200 {object} response.SuccessResponse // @Failure 400 {object} response.ErrorResponse400 diff --git a/tables/models.go b/tables/models.go index fd91c11..7e7240f 100644 --- a/tables/models.go +++ b/tables/models.go @@ -95,7 +95,4 @@ type Data struct { Rows []map[string]interface{} `json:"rows"` } -type RowValue struct { - ColumnName string `json: "column"` - Value interface{} `json: "value"` -} +type RowValue []map[string]interface{} From f03fe1d2f7ef101a650ccac9c3e885d67b7a5c2e Mon Sep 17 00:00:00 2001 From: rag-nar1 Date: Sat, 12 Jul 2025 01:13:40 +0300 Subject: [PATCH 081/101] filters --- docs/docs.go | 22 ++++------------------ docs/swagger.json | 22 ++++------------------ docs/swagger.yaml | 20 +++++++------------- tables/handlers.go | 7 ++++--- tables/repository.go | 2 +- tmp/air_errors.log | 2 +- 6 files changed, 21 insertions(+), 54 deletions(-) diff --git a/docs/docs.go b/docs/docs.go index 288074b..d601216 100644 --- a/docs/docs.go +++ b/docs/docs.go @@ -308,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" } @@ -485,7 +479,8 @@ const docTemplate = `{ "schema": { "type": "array", "items": { - "$ref": "#/definitions/tables.RowValue" + "type": "object", + "additionalProperties": true } } } @@ -2881,15 +2876,6 @@ const docTemplate = `{ } } }, - "tables.RowValue": { - "type": "object", - "properties": { - "columnName": { - "type": "string" - }, - "value": {} - } - }, "tables.ShowColumn": { "type": "object", "properties": { diff --git a/docs/swagger.json b/docs/swagger.json index 8c51da4..e468377 100644 --- a/docs/swagger.json +++ b/docs/swagger.json @@ -301,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" } @@ -478,7 +472,8 @@ "schema": { "type": "array", "items": { - "$ref": "#/definitions/tables.RowValue" + "type": "object", + "additionalProperties": true } } } @@ -2874,15 +2869,6 @@ } } }, - "tables.RowValue": { - "type": "object", - "properties": { - "columnName": { - "type": "string" - }, - "value": {} - } - }, "tables.ShowColumn": { "type": "object", "properties": { diff --git a/docs/swagger.yaml b/docs/swagger.yaml index 624fe98..f762472 100644 --- a/docs/swagger.yaml +++ b/docs/swagger.yaml @@ -406,12 +406,6 @@ definitions: type: object type: array type: object - tables.RowValue: - properties: - columnName: - type: string - value: {} - type: object tables.ShowColumn: properties: name: @@ -765,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 @@ -831,7 +824,8 @@ paths: required: true schema: items: - $ref: '#/definitions/tables.RowValue' + additionalProperties: true + type: object type: array produces: - application/json diff --git a/tables/handlers.go b/tables/handlers.go index 2ea980e..81e7fa7 100644 --- a/tables/handlers.go +++ b/tables/handlers.go @@ -5,6 +5,7 @@ import ( "DBHS/response" "encoding/json" "errors" + "log" "net/http" "github.com/gorilla/mux" @@ -240,9 +241,8 @@ 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.ErrorResponse400 @@ -266,6 +266,7 @@ func ReadTableHandler(app *config.Application) http.HandlerFunc { 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) diff --git a/tables/repository.go b/tables/repository.go index eb43718..45d4318 100644 --- a/tables/repository.go +++ b/tables/repository.go @@ -241,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 { diff --git a/tmp/air_errors.log b/tmp/air_errors.log index a56fdea..d829710 100644 --- a/tmp/air_errors.log +++ b/tmp/air_errors.log @@ -4,4 +4,4 @@ exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1ex ======= exit status 1 >>>>>>> logging -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 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 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 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 +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 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 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 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 From 682843f20b0011b04d39a119e1898776937c86f9 Mon Sep 17 00:00:00 2001 From: GergesHany Date: Sat, 12 Jul 2025 14:23:34 +0300 Subject: [PATCH 082/101] Handle There is no data case --- SqlEditor/repository.go | 3 +++ 1 file changed, 3 insertions(+) diff --git a/SqlEditor/repository.go b/SqlEditor/repository.go index 45e0fce..94368d1 100644 --- a/SqlEditor/repository.go +++ b/SqlEditor/repository.go @@ -48,6 +48,9 @@ func FetchQueryData(ctx context.Context, conn *pgxpool.Pool, query string) (Resp 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())) } From b9adb237583691c3a79ce3b4e7b50872642db109 Mon Sep 17 00:00:00 2001 From: rag-nar1 Date: Sat, 12 Jul 2025 14:45:27 +0300 Subject: [PATCH 083/101] update agent --- go.mod | 2 +- go.sum | 2 ++ 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/go.mod b/go.mod index cd62ea3..f024bd8 100644 --- a/go.mod +++ b/go.mod @@ -8,7 +8,7 @@ toolchain go1.24.5 // update go version to 1.24.0 require ( - github.com/Database-Hosting-Services/AI-Agent v1.0.7 + 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 diff --git a/go.sum b/go.sum index aa56394..309251d 100644 --- a/go.sum +++ b/go.sum @@ -13,6 +13,8 @@ cloud.google.com/go/longrunning v0.5.7/go.mod h1:8GClkudohy1Fxm3owmBGid8W0pSgodE github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= 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= From 901622b12fcf9c6f50bc312a0e9810cb34b2eca7 Mon Sep 17 00:00:00 2001 From: rag-nar1 Date: Sat, 12 Jul 2025 14:56:35 +0300 Subject: [PATCH 084/101] table names fixed --- tables/repository.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tables/repository.go b/tables/repository.go index 45d4318..1730ee5 100644 --- a/tables/repository.go +++ b/tables/repository.go @@ -193,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 From 8cfe83724cd4a13820d55bcac3c1ba63c3d148c3 Mon Sep 17 00:00:00 2001 From: rag-nar1 Date: Sat, 12 Jul 2025 15:16:10 +0300 Subject: [PATCH 085/101] extracting rows from asn empty table returns an empty list --- tables/service.go | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/tables/service.go b/tables/service.go index 5f77e39..84f41a3 100644 --- a/tables/service.go +++ b/tables/service.go @@ -232,6 +232,10 @@ 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 } From faf725f5696fc6e85d2f8c9f8f2ea43137688148 Mon Sep 17 00:00:00 2001 From: rag-nar1 Date: Sat, 12 Jul 2025 15:29:20 +0300 Subject: [PATCH 086/101] added body to the logs for the create table endpoint --- response/response.go | 3 +-- tables/handlers.go | 15 ++++++++++++++- 2 files changed, 15 insertions(+), 3 deletions(-) diff --git a/response/response.go b/response/response.go index 6704053..272b1df 100644 --- a/response/response.go +++ b/response/response.go @@ -37,7 +37,6 @@ func SendResponse(w http.ResponseWriter, status int, 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 - bodydata, _ := JsonString(r.Body) event := axiom.Event{ ingest.TimestampField: time.Now(), "user-id": r.Context().Value("user-id"), @@ -47,7 +46,7 @@ func CreateResponse(w http.ResponseWriter, r *http.Request, status int, message "method": r.Method, "URI": r.RequestURI, "request-header": r.Header, - "request-body": bodydata, + "request-body": r.Context().Value("body"), } if err != nil { response = &Response{ diff --git a/tables/handlers.go b/tables/handlers.go index 81e7fa7..032aa06 100644 --- a/tables/handlers.go +++ b/tables/handlers.go @@ -3,8 +3,10 @@ package tables import ( "DBHS/config" "DBHS/response" + "DBHS/utils" "encoding/json" "errors" + "io" "log" "net/http" @@ -107,8 +109,19 @@ func CreateTableHandler(app *config.Application) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { // Handler logic for creating a table 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 { + if err := json.Unmarshal(bodyData, &table); err != nil { response.BadRequest(w, r, "Invalid request body", err) return } From b29dc951ff95af33a3ff6ebd88225fb942ce83fc Mon Sep 17 00:00:00 2001 From: rag-nar1 Date: Sat, 12 Jul 2025 16:00:44 +0300 Subject: [PATCH 087/101] costs are up --- analytics/service.go | 3 +++ 1 file changed, 3 insertions(+) diff --git a/analytics/service.go b/analytics/service.go index 41256e0..22ffa18 100644 --- a/analytics/service.go +++ b/analytics/service.go @@ -194,6 +194,9 @@ 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 *= 1000000 + record.ReadWriteCost *= 1000000 + record.TotalCost = record.CPUCost + record.ReadWriteCost usageRecords = append(usageRecords, record) } From 1f09947802fb00221b47120db9bcff1c1c020a76 Mon Sep 17 00:00:00 2001 From: rag-nar1 Date: Sat, 12 Jul 2025 16:04:33 +0300 Subject: [PATCH 088/101] negatice cost ha --- analytics/service.go | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/analytics/service.go b/analytics/service.go index 22ffa18..353ae3d 100644 --- a/analytics/service.go +++ b/analytics/service.go @@ -194,8 +194,15 @@ 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 *= 1000000 - record.ReadWriteCost *= 1000000 + 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) } From 0591341119e87f016ffcd6298a61bae95b95861a Mon Sep 17 00:00:00 2001 From: rag-nar1 Date: Sat, 12 Jul 2025 16:21:10 +0300 Subject: [PATCH 089/101] costs --- analytics/service.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/analytics/service.go b/analytics/service.go index 353ae3d..090f288 100644 --- a/analytics/service.go +++ b/analytics/service.go @@ -194,8 +194,8 @@ 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 + record.CPUCost *= 1000000000 + record.ReadWriteCost *= 1000000000 if record.CPUCost < 0 { record.CPUCost *= -1 } From 74487b0851a663e558ab558d3b6712c7a1a3548e Mon Sep 17 00:00:00 2001 From: rag-nar1 Date: Sat, 12 Jul 2025 16:25:25 +0300 Subject: [PATCH 090/101] cost back --- analytics/service.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/analytics/service.go b/analytics/service.go index 090f288..353ae3d 100644 --- a/analytics/service.go +++ b/analytics/service.go @@ -194,8 +194,8 @@ 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 *= 1000000000 - record.ReadWriteCost *= 1000000000 + record.CPUCost *= 10000000 + record.ReadWriteCost *= 10000000 if record.CPUCost < 0 { record.CPUCost *= -1 } From 9af3dd70d49af8e3a6e72c7a8832759616d5b630 Mon Sep 17 00:00:00 2001 From: rag-nar1 Date: Sat, 12 Jul 2025 16:52:29 +0300 Subject: [PATCH 091/101] tanle name resolved --- tables/repository.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tables/repository.go b/tables/repository.go index 1730ee5..783b3dc 100644 --- a/tables/repository.go +++ b/tables/repository.go @@ -283,7 +283,7 @@ func InserRow(ctx context.Context, tableNmae string, data map[string]interface{} values := make([]interface{}, 0, len(data)) i := 1 for k, v := range data { - columns = append(columns, k) + columns = append(columns, "\""+k+"\"") placeholders = append(placeholders, fmt.Sprintf("$%d", i)) // Use ? if not PostgreSQL values = append(values, v) i++ From 7f7e73c766dd84b5b254dc2021061d1a8d65e588 Mon Sep 17 00:00:00 2001 From: rag-nar1 Date: Sat, 12 Jul 2025 16:56:36 +0300 Subject: [PATCH 092/101] added body to the logs for the insert row in table endpoint --- tables/handlers.go | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/tables/handlers.go b/tables/handlers.go index 032aa06..fa6f794 100644 --- a/tables/handlers.go +++ b/tables/handlers.go @@ -332,9 +332,18 @@ func InsertRowHandler(app *config.Application) http.HandlerFunc { 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.NewDecoder(r.Body).Decode(&row); err != nil { + if err := json.Unmarshal(bodyData, &row); err != nil { response.BadRequest(w, r, "bad request body", nil) return } From 7b09ae5e99211501ddd935f901ca6ae37b1931f7 Mon Sep 17 00:00:00 2001 From: rag-nar1 Date: Sat, 12 Jul 2025 17:09:17 +0300 Subject: [PATCH 093/101] solve inserting new row --- tables/SqlQueries.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tables/SqlQueries.go b/tables/SqlQueries.go index c4cbaf8..603f920 100644 --- a/tables/SqlQueries.go +++ b/tables/SqlQueries.go @@ -51,6 +51,6 @@ const ( ORDER BY c.column_name;` InsertNewRowStmt = ` - INSERT INTO %s(%s) VALUES(%s) + INSERT INTO "%s"(%s) VALUES(%s) ` ) From a6b5a97313ba651b70e3496dcc1c9a355a90df57 Mon Sep 17 00:00:00 2001 From: rag-nar1 Date: Sat, 12 Jul 2025 23:49:30 +0300 Subject: [PATCH 094/101] Refactor CORS middleware to allow all origins and common HTTP methods; clean up code formatting. --- middleware/middleware.go | 38 ++++++++++++++++++++++---------------- 1 file changed, 22 insertions(+), 16 deletions(-) diff --git a/middleware/middleware.go b/middleware/middleware.go index 8a8a501..7f6d65c 100644 --- a/middleware/middleware.go +++ b/middleware/middleware.go @@ -87,7 +87,7 @@ func CheckOTableExist(next http.Handler) http.Handler { 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) @@ -111,20 +111,26 @@ func CheckOTableExist(next http.Handler) http.Handler { } 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") + + // 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") - if r.Method == "OPTIONS" { - w.WriteHeader(http.StatusNoContent) - return - } + // Handle preflight OPTIONS requests + if r.Method == "OPTIONS" { + w.WriteHeader(http.StatusNoContent) + return + } - next.ServeHTTP(w, r) - }) -} \ No newline at end of file + next.ServeHTTP(w, r) + }) +} From f1cb6c4906df9d6752e15a6f17e17cdd5db5de8f Mon Sep 17 00:00:00 2001 From: rag-nar1 Date: Sun, 13 Jul 2025 00:45:26 +0300 Subject: [PATCH 095/101] Add function to remove JSON code segments from responses in AgentQuery --- AI/services.go | 25 +++++++++++++++++++++++-- 1 file changed, 23 insertions(+), 2 deletions(-) diff --git a/AI/services.go b/AI/services.go index c10e338..f4f05b3 100644 --- a/AI/services.go +++ b/AI/services.go @@ -6,9 +6,11 @@ import ( "context" "encoding/json" "errors" - "github.com/jackc/pgx/v5" + "regexp" "time" + "github.com/jackc/pgx/v5" + "github.com/Database-Hosting-Services/AI-Agent/RAG" ) @@ -99,6 +101,9 @@ func AgentQuery(projectUUID string, userID int64, prompt string, AI RAG.RAGmodel 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 { @@ -156,4 +161,20 @@ func ClearCacheForProject(projectUUID string) error { return err } return nil -} \ No newline at end of file +} + +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 +} From 3d152ad12a74c338548e08c4301f0d3c442b8749 Mon Sep 17 00:00:00 2001 From: rag-nar1 Date: Sun, 13 Jul 2025 01:25:13 +0300 Subject: [PATCH 096/101] error --- .gitignore | 3 ++- projects/handlers.go | 2 +- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/.gitignore b/.gitignore index ac2191d..49987f0 100644 --- a/.gitignore +++ b/.gitignore @@ -10,4 +10,5 @@ curl.sh *.log pr.md response.* -/tmp \ No newline at end of file +/tmp +insert_students.sh \ No newline at end of file diff --git a/projects/handlers.go b/projects/handlers.go index 542812f..f6cb11a 100644 --- a/projects/handlers.go +++ b/projects/handlers.go @@ -40,7 +40,7 @@ func CreateProject(app *config.Application) http.HandlerFunc { } else if err.Error() == "database name must start with a letter or underscore and contain only letters, numbers, underscores, or $" { 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, r, "Internal Server Error", errors.New("Project creation failed")) + response.InternalServerError(w, r, "Internal Server Error", err) } return } From ede8bb5064f2088ba6c166846e0eada0d639198c Mon Sep 17 00:00:00 2001 From: rag-nar1 Date: Sun, 13 Jul 2025 02:39:13 +0300 Subject: [PATCH 097/101] Update SQL queries in SqlQueries.go to use COALESCE for better handling of null values in execution time and query counts. --- analytics/SqlQueries.go | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) 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;` ) From af1a6061b7c4e1b068017cefcc0754fa757a49b2 Mon Sep 17 00:00:00 2001 From: rag-nar1 Date: Sun, 13 Jul 2025 03:07:08 +0300 Subject: [PATCH 098/101] Add dummy data to analytics handlers --- analytics/handlers.go | 10 +-- analytics/models.go | 174 ++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 179 insertions(+), 5 deletions(-) diff --git a/analytics/handlers.go b/analytics/handlers.go index 5d377f9..ddeded6 100644 --- a/analytics/handlers.go +++ b/analytics/handlers.go @@ -40,7 +40,7 @@ func CurrentStorage(app *config.Application) http.HandlerFunc { response.NotFound(w, r, "No storage information found for the project", nil) return } - response.OK(w, r, "Storage history retrieved successfully", storage) + response.OK(w, r, "Storage history retrieved successfully", StorageResponse) } } @@ -66,13 +66,13 @@ func ExecutionTime(app *config.Application) http.HandlerFunc { 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, r, "Execution time statistics retrieved successfully", stats) + response.OK(w, r, "Execution time statistics retrieved successfully", DatabaseActivityResponse) } } @@ -98,12 +98,12 @@ func DatabaseUsage(app *config.Application) http.HandlerFunc { 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, r, "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..65d9109 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 + StorageResponse = []StorageWithDates{ + { + Timestamp: "2025-01-01T00:00:00Z", + ManagementStorage: "125 MB", + ActualData: "2.1 GB", + }, + { + Timestamp: "2025-01-02T00:00:00Z", + ManagementStorage: "128 MB", + ActualData: "2.3 GB", + }, + { + Timestamp: "2025-01-03T00:00:00Z", + ManagementStorage: "132 MB", + ActualData: "2.6 GB", + }, + { + Timestamp: "2025-01-04T00:00:00Z", + ManagementStorage: "135 MB", + ActualData: "2.8 GB", + }, + { + Timestamp: "2025-01-05T00:00:00Z", + ManagementStorage: "141 MB", + ActualData: "3.1 GB", + }, + { + Timestamp: "2025-01-06T00:00:00Z", + ManagementStorage: "144 MB", + ActualData: "3.4 GB", + }, + { + Timestamp: "2025-01-07T00:00:00Z", + ManagementStorage: "148 MB", + ActualData: "3.7 GB", + }, + { + Timestamp: "2025-01-08T00:00:00Z", + ManagementStorage: "152 MB", + ActualData: "4.0 GB", + }, + { + Timestamp: "2025-01-09T00:00:00Z", + ManagementStorage: "156 MB", + ActualData: "4.2 GB", + }, + { + Timestamp: "2025-01-10T00:00:00Z", + ManagementStorage: "160 MB", + ActualData: "4.5 GB", + }, + } + + // Database Activity dummy data - showing realistic patterns with peaks and valleys + DatabaseActivityResponse = []DatabaseActivityWithDates{ + { + Timestamp: "2025-01-01T00:00:00Z", + TotalTimeMs: 1245.67, + TotalQueries: 1523, + }, + { + Timestamp: "2025-01-02T00:00:00Z", + TotalTimeMs: 1789.34, + TotalQueries: 2156, + }, + { + Timestamp: "2025-01-03T00:00:00Z", + TotalTimeMs: 2134.89, + TotalQueries: 2834, + }, + { + Timestamp: "2025-01-04T00:00:00Z", + TotalTimeMs: 1876.23, + TotalQueries: 2445, + }, + { + Timestamp: "2025-01-05T00:00:00Z", + TotalTimeMs: 2567.45, + TotalQueries: 3234, + }, + { + Timestamp: "2025-01-06T00:00:00Z", + TotalTimeMs: 2234.78, + TotalQueries: 2967, + }, + { + Timestamp: "2025-01-07T00:00:00Z", + TotalTimeMs: 3045.12, + TotalQueries: 3789, + }, + { + Timestamp: "2025-01-08T00:00:00Z", + TotalTimeMs: 2789.56, + TotalQueries: 3456, + }, + { + Timestamp: "2025-01-09T00:00:00Z", + TotalTimeMs: 2456.89, + TotalQueries: 3123, + }, + { + Timestamp: "2025-01-10T00:00:00Z", + TotalTimeMs: 2998.34, + TotalQueries: 3678, + }, + } + + // Database Usage Stats dummy data - showing cost variations + DatabaseUsageStatsResponse = []DatabaseUsageCostWithDates{ + { + ReadWriteCost: 85.45, + CPUCost: 67.23, + TotalCost: 152.68, + Timestamp: "2025-01-01T00:00:00Z", + }, + { + ReadWriteCost: 142.78, + CPUCost: 89.34, + TotalCost: 232.12, + Timestamp: "2025-01-02T00:00:00Z", + }, + { + ReadWriteCost: 198.56, + CPUCost: 112.67, + TotalCost: 311.23, + Timestamp: "2025-01-03T00:00:00Z", + }, + { + ReadWriteCost: 176.89, + CPUCost: 95.45, + TotalCost: 272.34, + Timestamp: "2025-01-04T00:00:00Z", + }, + { + ReadWriteCost: 234.67, + CPUCost: 134.23, + TotalCost: 368.90, + Timestamp: "2025-01-05T00:00:00Z", + }, + { + ReadWriteCost: 201.45, + CPUCost: 118.78, + TotalCost: 320.23, + Timestamp: "2025-01-06T00:00:00Z", + }, + { + ReadWriteCost: 287.34, + CPUCost: 156.89, + TotalCost: 444.23, + Timestamp: "2025-01-07T00:00:00Z", + }, + { + ReadWriteCost: 256.78, + CPUCost: 143.56, + TotalCost: 400.34, + Timestamp: "2025-01-08T00:00:00Z", + }, + { + ReadWriteCost: 213.45, + CPUCost: 125.67, + TotalCost: 339.12, + Timestamp: "2025-01-09T00:00:00Z", + }, + { + ReadWriteCost: 298.67, + CPUCost: 167.89, + TotalCost: 466.56, + Timestamp: "2025-01-10T00:00:00Z", + }, + } +) From 35284e395790463440acbf4cc47b9a998d97434b Mon Sep 17 00:00:00 2001 From: rag-nar1 Date: Sun, 13 Jul 2025 03:15:46 +0300 Subject: [PATCH 099/101] Update storage dummy data in analytics models to use kilobytes for management storage and actual data values, enhancing clarity and consistency. --- analytics/models.go | 42 +++++++++++++++++++++--------------------- 1 file changed, 21 insertions(+), 21 deletions(-) diff --git a/analytics/models.go b/analytics/models.go index 65d9109..268ebd2 100644 --- a/analytics/models.go +++ b/analytics/models.go @@ -45,57 +45,57 @@ type DatabaseUsageCostWithDates struct { } var ( - // Storage dummy data - showing growth over time + // Storage dummy data - showing growth over time (all in KB) StorageResponse = []StorageWithDates{ { Timestamp: "2025-01-01T00:00:00Z", - ManagementStorage: "125 MB", - ActualData: "2.1 GB", + ManagementStorage: "128000 kB", + ActualData: "2202010 kB", }, { Timestamp: "2025-01-02T00:00:00Z", - ManagementStorage: "128 MB", - ActualData: "2.3 GB", + ManagementStorage: "131072 kB", + ActualData: "2411724 kB", }, { Timestamp: "2025-01-03T00:00:00Z", - ManagementStorage: "132 MB", - ActualData: "2.6 GB", + ManagementStorage: "135168 kB", + ActualData: "2730394 kB", }, { Timestamp: "2025-01-04T00:00:00Z", - ManagementStorage: "135 MB", - ActualData: "2.8 GB", + ManagementStorage: "138240 kB", + ActualData: "2939002 kB", }, { Timestamp: "2025-01-05T00:00:00Z", - ManagementStorage: "141 MB", - ActualData: "3.1 GB", + ManagementStorage: "144384 kB", + ActualData: "3257671 kB", }, { Timestamp: "2025-01-06T00:00:00Z", - ManagementStorage: "144 MB", - ActualData: "3.4 GB", + ManagementStorage: "147456 kB", + ActualData: "3576340 kB", }, { Timestamp: "2025-01-07T00:00:00Z", - ManagementStorage: "148 MB", - ActualData: "3.7 GB", + ManagementStorage: "151552 kB", + ActualData: "3895009 kB", }, { Timestamp: "2025-01-08T00:00:00Z", - ManagementStorage: "152 MB", - ActualData: "4.0 GB", + ManagementStorage: "155648 kB", + ActualData: "4194304 kB", }, { Timestamp: "2025-01-09T00:00:00Z", - ManagementStorage: "156 MB", - ActualData: "4.2 GB", + ManagementStorage: "159744 kB", + ActualData: "4403021 kB", }, { Timestamp: "2025-01-10T00:00:00Z", - ManagementStorage: "160 MB", - ActualData: "4.5 GB", + ManagementStorage: "163840 kB", + ActualData: "4718592 kB", }, } From 87457f962ac0857f58057311ac6f2fba78c13894 Mon Sep 17 00:00:00 2001 From: rag-nar1 Date: Sun, 13 Jul 2025 03:24:03 +0300 Subject: [PATCH 100/101] Refactor dummy data in analytics models to use smaller values for storage, database activity, and usage stats, improving clarity and consistency in representation. --- analytics/models.go | 146 ++++++++++++++++++++++---------------------- 1 file changed, 73 insertions(+), 73 deletions(-) diff --git a/analytics/models.go b/analytics/models.go index 268ebd2..7c021b2 100644 --- a/analytics/models.go +++ b/analytics/models.go @@ -45,174 +45,174 @@ type DatabaseUsageCostWithDates struct { } var ( - // Storage dummy data - showing growth over time (all in KB) + // Storage dummy data - showing growth over time (all in KB, under 100) StorageResponse = []StorageWithDates{ { Timestamp: "2025-01-01T00:00:00Z", - ManagementStorage: "128000 kB", - ActualData: "2202010 kB", + ManagementStorage: "12 kB", + ActualData: "45 kB", }, { Timestamp: "2025-01-02T00:00:00Z", - ManagementStorage: "131072 kB", - ActualData: "2411724 kB", + ManagementStorage: "14 kB", + ActualData: "52 kB", }, { Timestamp: "2025-01-03T00:00:00Z", - ManagementStorage: "135168 kB", - ActualData: "2730394 kB", + ManagementStorage: "16 kB", + ActualData: "58 kB", }, { Timestamp: "2025-01-04T00:00:00Z", - ManagementStorage: "138240 kB", - ActualData: "2939002 kB", + ManagementStorage: "18 kB", + ActualData: "64 kB", }, { Timestamp: "2025-01-05T00:00:00Z", - ManagementStorage: "144384 kB", - ActualData: "3257671 kB", + ManagementStorage: "21 kB", + ActualData: "71 kB", }, { Timestamp: "2025-01-06T00:00:00Z", - ManagementStorage: "147456 kB", - ActualData: "3576340 kB", + ManagementStorage: "24 kB", + ActualData: "77 kB", }, { Timestamp: "2025-01-07T00:00:00Z", - ManagementStorage: "151552 kB", - ActualData: "3895009 kB", + ManagementStorage: "27 kB", + ActualData: "84 kB", }, { Timestamp: "2025-01-08T00:00:00Z", - ManagementStorage: "155648 kB", - ActualData: "4194304 kB", + ManagementStorage: "30 kB", + ActualData: "89 kB", }, { Timestamp: "2025-01-09T00:00:00Z", - ManagementStorage: "159744 kB", - ActualData: "4403021 kB", + ManagementStorage: "33 kB", + ActualData: "95 kB", }, { Timestamp: "2025-01-10T00:00:00Z", - ManagementStorage: "163840 kB", - ActualData: "4718592 kB", + ManagementStorage: "36 kB", + ActualData: "99 kB", }, } - // Database Activity dummy data - showing realistic patterns with peaks and valleys + // Database Activity dummy data - showing realistic patterns with peaks and valleys (under 100) DatabaseActivityResponse = []DatabaseActivityWithDates{ { Timestamp: "2025-01-01T00:00:00Z", - TotalTimeMs: 1245.67, - TotalQueries: 1523, + TotalTimeMs: 12.45, + TotalQueries: 15, }, { Timestamp: "2025-01-02T00:00:00Z", - TotalTimeMs: 1789.34, - TotalQueries: 2156, + TotalTimeMs: 17.89, + TotalQueries: 21, }, { Timestamp: "2025-01-03T00:00:00Z", - TotalTimeMs: 2134.89, - TotalQueries: 2834, + TotalTimeMs: 21.34, + TotalQueries: 28, }, { Timestamp: "2025-01-04T00:00:00Z", - TotalTimeMs: 1876.23, - TotalQueries: 2445, + TotalTimeMs: 18.76, + TotalQueries: 24, }, { Timestamp: "2025-01-05T00:00:00Z", - TotalTimeMs: 2567.45, - TotalQueries: 3234, + TotalTimeMs: 25.67, + TotalQueries: 32, }, { Timestamp: "2025-01-06T00:00:00Z", - TotalTimeMs: 2234.78, - TotalQueries: 2967, + TotalTimeMs: 22.34, + TotalQueries: 29, }, { Timestamp: "2025-01-07T00:00:00Z", - TotalTimeMs: 3045.12, - TotalQueries: 3789, + TotalTimeMs: 30.45, + TotalQueries: 37, }, { Timestamp: "2025-01-08T00:00:00Z", - TotalTimeMs: 2789.56, - TotalQueries: 3456, + TotalTimeMs: 27.89, + TotalQueries: 34, }, { Timestamp: "2025-01-09T00:00:00Z", - TotalTimeMs: 2456.89, - TotalQueries: 3123, + TotalTimeMs: 24.56, + TotalQueries: 31, }, { Timestamp: "2025-01-10T00:00:00Z", - TotalTimeMs: 2998.34, - TotalQueries: 3678, + TotalTimeMs: 29.98, + TotalQueries: 36, }, } - // Database Usage Stats dummy data - showing cost variations + // Database Usage Stats dummy data - showing cost variations (under 100) DatabaseUsageStatsResponse = []DatabaseUsageCostWithDates{ { - ReadWriteCost: 85.45, - CPUCost: 67.23, - TotalCost: 152.68, + ReadWriteCost: 25.45, + CPUCost: 17.23, + TotalCost: 42.68, Timestamp: "2025-01-01T00:00:00Z", }, { - ReadWriteCost: 142.78, - CPUCost: 89.34, - TotalCost: 232.12, + ReadWriteCost: 32.78, + CPUCost: 19.34, + TotalCost: 52.12, Timestamp: "2025-01-02T00:00:00Z", }, { - ReadWriteCost: 198.56, - CPUCost: 112.67, - TotalCost: 311.23, + ReadWriteCost: 38.56, + CPUCost: 22.67, + TotalCost: 61.23, Timestamp: "2025-01-03T00:00:00Z", }, { - ReadWriteCost: 176.89, - CPUCost: 95.45, - TotalCost: 272.34, + ReadWriteCost: 36.89, + CPUCost: 25.45, + TotalCost: 62.34, Timestamp: "2025-01-04T00:00:00Z", }, { - ReadWriteCost: 234.67, - CPUCost: 134.23, - TotalCost: 368.90, + ReadWriteCost: 44.67, + CPUCost: 34.23, + TotalCost: 78.90, Timestamp: "2025-01-05T00:00:00Z", }, { - ReadWriteCost: 201.45, - CPUCost: 118.78, - TotalCost: 320.23, + ReadWriteCost: 41.45, + CPUCost: 28.78, + TotalCost: 70.23, Timestamp: "2025-01-06T00:00:00Z", }, { - ReadWriteCost: 287.34, - CPUCost: 156.89, - TotalCost: 444.23, + ReadWriteCost: 47.34, + CPUCost: 36.89, + TotalCost: 84.23, Timestamp: "2025-01-07T00:00:00Z", }, { - ReadWriteCost: 256.78, - CPUCost: 143.56, - TotalCost: 400.34, + ReadWriteCost: 46.78, + CPUCost: 33.56, + TotalCost: 80.34, Timestamp: "2025-01-08T00:00:00Z", }, { - ReadWriteCost: 213.45, - CPUCost: 125.67, - TotalCost: 339.12, + ReadWriteCost: 43.45, + CPUCost: 35.67, + TotalCost: 79.12, Timestamp: "2025-01-09T00:00:00Z", }, { - ReadWriteCost: 298.67, - CPUCost: 167.89, - TotalCost: 466.56, + ReadWriteCost: 48.67, + CPUCost: 37.89, + TotalCost: 86.56, Timestamp: "2025-01-10T00:00:00Z", }, } From f88582ada59f567315e9d3b44a5be61020b2c4e6 Mon Sep 17 00:00:00 2001 From: Mohamed Fathy Mohamed Hassan Date: Sun, 13 Jul 2025 08:15:03 +0300 Subject: [PATCH 101/101] Update handlers.go --- accounts/handlers.go | 1 - 1 file changed, 1 deletion(-) diff --git a/accounts/handlers.go b/accounts/handlers.go index 41481f0..e7b9e28 100644 --- a/accounts/handlers.go +++ b/accounts/handlers.go @@ -12,7 +12,6 @@ import ( "strings" "github.com/gorilla/mux" "github.com/redis/go-redis/v9" - "net/http" ) // GetUserData godoc