diff --git a/src/db/dblist.go b/src/db/dblist.go index 7f9ff28..13821ae 100644 --- a/src/db/dblist.go +++ b/src/db/dblist.go @@ -153,7 +153,6 @@ func (o *DbList) Delete(id string) { func (o *DbList) PutPreparedStatement(id string, stmt *sql.Stmt) (string, bool) { val, ok := o.items.Load(id) if !ok { - app.Log.Errorf("SQL connection with guid='%s' not found", id) return "", false } @@ -170,15 +169,14 @@ func (o *DbList) PutPreparedStatement(id string, stmt *sql.Stmt) (string, bool) } // Gets SQL prepared statement -func (o *DbList) GetPreparedStatement(conn_id, stmt_id string) (*sql.Stmt, bool) { - val, ok := o.items.Load(conn_id) +func (o *DbList) GetPreparedStatement(connId, stmtId string) (*sql.Stmt, bool) { + val, ok := o.items.Load(connId) if !ok { - app.Log.Errorf("SQL connection with guid='%s' not found", conn_id) return nil, false } res := val.(*DbConn) - for i := 0; i < len(res.Stmt); i++ { - if res.Stmt[i].Id == stmt_id { + for i := range res.Stmt { + if res.Stmt[i].Id == stmtId { return res.Stmt[i].Stmt, true } } @@ -186,21 +184,20 @@ func (o *DbList) GetPreparedStatement(conn_id, stmt_id string) (*sql.Stmt, bool) } // Closes and deletes SQL prepared statement -func (o *DbList) ClosePreparedStatement(conn_id, stmt_id string) bool { - val, ok := o.items.Load(conn_id) +func (o *DbList) ClosePreparedStatement(connId, stmtId string) bool { + val, ok := o.items.Load(connId) if !ok { - app.Log.Errorf("SQL connection with guid='%s' not found", conn_id) return false } res := val.(*DbConn) for i := range res.Stmt { - if res.Stmt[i].Id == stmt_id { + if res.Stmt[i].Id == stmtId { res.Stmt[i].Stmt.Close() res.Stmt = slices.Delete(res.Stmt, i, i+1) break } } - o.items.Store(conn_id, res) + o.items.Store(connId, res) return true } diff --git a/src/docs/api/openapi_en.yml b/src/docs/api/openapi_en.yml index 455abb3..97775a6 100644 --- a/src/docs/api/openapi_en.yml +++ b/src/docs/api/openapi_en.yml @@ -6,7 +6,9 @@ servers: - url: http://localhost/api/v1 paths: + /connection: + post: summary: Establish SQL connection description: First, check if an SQL connection has already been established. If not, create a new connection and add it to the application pool. @@ -53,6 +55,7 @@ paths: description: Bad request /query: + get: summary: SELECT queries description: Use this method for any SQL query that is expected to return a result as a table, such as a SELECT statement. The resulting table is wrapped into a flexible JSON object, with columns dynamically determined based on the query. @@ -68,7 +71,7 @@ paths: name: SQL-Statement schema: type: string - description: SQL statemet (url-encoded). + description: SQL statement (url-encoded). required: true example: 'SELECT * FROM SALES WHERE Title LIKE "Manager %"' @@ -102,7 +105,7 @@ paths: name: SQL-Statement schema: type: string - description: SQL statemet (url-encoded). + description: SQL statement (url-encoded). required: true example: 'DELETE FROM SALES WHERE id = 783' responses: @@ -112,6 +115,69 @@ paths: description: Bad request '403': description: Forbidden + + /prepared: + + post: + summary: Create prepared statement + description: 'Create prepared statement' + parameters: + - in: header + name: Connection-Id + schema: + type: string + description: SQL connection id as GUID in a plain text, must be obtained by /connection POST method. + required: true + example: '52f0b434-4eae-4cc6-803c-2d2f604fe16c' + - in: header + name: Prepared-Statement + schema: + type: string + description: SQL prepared statement with parameters, both for select or execute (url-encoded). + required: true + example: 'SELECT * SALES WHERE id = ? and name = ?' + responses: + '200': + description: OK + content: + text/plain: + schema: + type: string + description: return SQL prepared statement id as GUID in a plain text. + example: 'f3f0b434-e4ae-c4c6-c803-d22f504fe16c' + '400': + description: Bad request + '403': + description: Forbidden + '500': + description: Internal server error + + delete: + summary: Close prepared statement + description: Close and delete prepared statement. + parameters: + - in: header + name: Connection-Id + schema: + type: string + description: SQL connection id as GUID in a plain text. + required: true + example: '52f0b434-4eae-4cc6-803c-2d2f604fe16c' + - in: header + name: Statement-Id + schema: + type: string + description: Prepared statement id as GUID in a plain text. + required: true + example: 'f3f0b434-e4ae-c4c6-c803-d22f504fe16c' + + responses: + '200': + description: OK + '400': + description: Bad request + '403': + description: Forbidden components: schemas: diff --git a/src/docs/api/openapi_ru.yml b/src/docs/api/openapi_ru.yml index 6283ee6..66253ba 100644 --- a/src/docs/api/openapi_ru.yml +++ b/src/docs/api/openapi_ru.yml @@ -6,7 +6,9 @@ servers: - url: http://localhost/api/v1 paths: + /connection: + post: summary: Получить SQL-соединение description: Сначала проверяет, установлено ли уже SQL-соединение. Если нет, устанавливает новое и добавляет его в пул доступных соединений. @@ -53,6 +55,7 @@ paths: description: Неверный запрос /query: + get: summary: Запросы SELECT description: Используйте этот метод для любого SQL-запроса, от который ожидается результат в виде таблицы, например, с оператором SELECT. Полученная таблица оборачивается в JSON-объект, с колонками, определяемыми динамически на основе запроса. @@ -112,6 +115,69 @@ paths: description: Неверный запрос '403': description: Запрещено + + /prepared: + + post: + summary: Создать предварительно подготовленный оператор + description: Используйте этот метод для создания предварительно подготовленного оператора + parameters: + - in: header + name: Connection-Id + schema: + type: string + description: Идентификатор SQL-соединения в форме GUID, должен быть получен ранее методом POST /connection. + required: true + example: '52f0b434-4eae-4cc6-803c-2d2f604fe16c' + - in: header + name: Prepared-Statement + description: Предварительно подготовленный оператор SQL с параметрами, для выборки или изменения данных (url-кодированный). + schema: + type: string + required: true + example: 'SELECT * SALES WHERE id = ? and name = ?' + responses: + '200': + description: OK + content: + text/plain: + schema: + type: string + description: Возвращает идентификатор подготовленного оператора SQL в форме GUID.. + example: 'f3f0b434-e4ae-c4c6-c803-d22f504fe16c' + '400': + description: Неверный запрос + '403': + description: Запрещено + '500': + description: Внутренняя ошибка сервера + + delete: + summary: Закрыть предварительно подготовленный оператор + description: Закрыть и удалить предварительно подготовленный оператор SQL. + parameters: + - in: header + name: Connection-Id + schema: + type: string + description: Идентификатор SQL-соединения в форме GUID, должен быть получен ранее методом POST /connection. + required: true + example: '52f0b434-4eae-4cc6-803c-2d2f604fe16c' + - in: header + name: Statement-Id + schema: + type: string + description: Идентификатор предварительно подготовленного оператора SQL в форме GUID, должен быть получен ранее методом POST /prepared. + required: true + example: 'f3f0b434-e4ae-c4c6-c803-d22f504fe16c' + + responses: + '200': + description: OK + '400': + description: Неверный запрос + '403': + description: Запрещено components: schemas: diff --git a/src/handlers/connection.go b/src/handlers/connection.go index 205eb0d..b2f9d32 100644 --- a/src/handlers/connection.go +++ b/src/handlers/connection.go @@ -16,11 +16,10 @@ func CreateConnection(w http.ResponseWriter, r *http.Request) { if connGuid, ok := db.Handler.GetByParams(&dbConnInfo); !ok { errorResponce(w, "Failed to get SQL connection", http.StatusInternalServerError) - } else { - if _, err := w.Write([]byte(connGuid)); err != nil { - errorResponce(w, err.Error(), http.StatusInternalServerError) - } + } else if _, err := w.Write([]byte(connGuid)); err != nil { + errorResponce(w, err.Error(), http.StatusInternalServerError) } + } func CloseConnection(w http.ResponseWriter, r *http.Request) { diff --git a/src/handlers/prepared.go b/src/handlers/prepared.go index b4ae839..d82aa5b 100644 --- a/src/handlers/prepared.go +++ b/src/handlers/prepared.go @@ -1,58 +1,79 @@ package handlers import ( - "encoding/json" "net/http" + "net/url" + "sql-proxy/src/app" "sql-proxy/src/db" + + "github.com/sirupsen/logrus" ) func PrepareStatement(w http.ResponseWriter, r *http.Request) { - var requestBody map[string]any - if err := json.NewDecoder(r.Body).Decode(&requestBody); err != nil { - errorResponce(w, "Error decoding JSON", http.StatusBadRequest) + connId := r.Header.Get("Connection-Id") + preparedStatement, err := url.QueryUnescape(r.Header.Get("Prepared-Statement")) + + if err != nil || connId == "" || preparedStatement == "" { + errorResponce(w, "Bad request", http.StatusBadRequest) return } - defer r.Body.Close() + app.Log.WithFields(logrus.Fields{ + "prepared_statement": preparedStatement, + "connection_id": connId, + }).Debug("Prepare statement received:") - conn, ok := db.Handler.GetById(requestBody["connection_id"].(string), true) + conn, ok := db.Handler.GetById(connId, true) if !ok { - errorResponce(w, "Invalid connection id", http.StatusBadRequest) + errorResponce(w, "Invalid connection id", http.StatusForbidden) return } - stmt, err := conn.Prepare(requestBody["sql"].(string)) + stmt, err := conn.Prepare(preparedStatement) if err != nil { errorResponce(w, err.Error(), http.StatusBadRequest) } - stmt_id, ok := db.Handler.PutPreparedStatement(requestBody["connection_id"].(string), stmt) + stmtId, ok := db.Handler.PutPreparedStatement(connId, stmt) if !ok { errorResponce(w, err.Error(), http.StatusInternalServerError) } - if _, err = w.Write([]byte(stmt_id)); err != nil { + if _, err = w.Write([]byte(stmtId)); err != nil { errorResponce(w, err.Error(), http.StatusInternalServerError) } + } func PreparedSelect(w http.ResponseWriter, r *http.Request) { - var requestBody map[string]any - - if err := json.NewDecoder(r.Body).Decode(&requestBody); err != nil { - errorResponce(w, "Error decoding JSON", http.StatusBadRequest) - return - } - defer r.Body.Close() // to do + } func PreparedExecute(w http.ResponseWriter, r *http.Request) { + // to do } func ClosePreparedStatement(w http.ResponseWriter, r *http.Request) { - // to do + + connId := r.Header.Get("Connection-Id") + stmtId := r.Header.Get("Statement-Id") + + if connId == "" || stmtId == "" { + errorResponce(w, "Bad request", http.StatusBadRequest) + return + } + + app.Log.WithFields(logrus.Fields{ + "connection_id": connId, + "prepared_statement": stmtId, + }).Debug("Delete prepared statememt received:") + + if ok := db.Handler.ClosePreparedStatement(connId, stmtId); !ok { + errorResponce(w, "Forbidden", http.StatusForbidden) + } + } diff --git a/src/handlers/query.go b/src/handlers/query.go index 21dc066..43abe48 100644 --- a/src/handlers/query.go +++ b/src/handlers/query.go @@ -13,21 +13,21 @@ import ( func SelectQuery(w http.ResponseWriter, r *http.Request) { - conn_id := r.Header.Get("Connection-Id") + connId := r.Header.Get("Connection-Id") query, err := url.QueryUnescape(r.Header.Get("SQL-Statement")) - if err != nil || conn_id == "" || query == "" { + if err != nil || connId == "" || query == "" { errorResponce(w, "Bad request", http.StatusBadRequest) return } app.Log.WithFields(logrus.Fields{ "sql": query, - "connection_id": conn_id, + "connection_id": connId, }).Debug("SQL query received:") // Search existings connection in the pool - dbConn, ok := db.Handler.GetById(conn_id, true) + dbConn, ok := db.Handler.GetById(connId, true) if !ok { errorResponce(w, "Failed to get SQL connection", http.StatusForbidden) return @@ -59,20 +59,20 @@ func SelectQuery(w http.ResponseWriter, r *http.Request) { func ExecuteQuery(w http.ResponseWriter, r *http.Request) { - conn_id := r.Header.Get("Connection-Id") + connId := r.Header.Get("Connection-Id") query, err := url.QueryUnescape(r.Header.Get("SQL-Statement")) - if err != nil || conn_id == "" || query == "" { + if err != nil || connId == "" || query == "" { errorResponce(w, "Bad request", http.StatusBadRequest) return } app.Log.WithFields(logrus.Fields{ "sql": query, - "connection_id": conn_id, + "connection_id": connId, }).Debug("SQL query received:") - dbConn, ok := db.Handler.GetById(conn_id, true) + dbConn, ok := db.Handler.GetById(connId, true) if !ok { errorResponce(w, "Invalid connection id", http.StatusForbidden) return @@ -80,7 +80,7 @@ func ExecuteQuery(w http.ResponseWriter, r *http.Request) { app.Log.WithFields(logrus.Fields{ "sql": query, - "connection_id": conn_id, + "connection_id": connId, }).Debug("SQL execute query received:") _, err = dbConn.Exec(query)