From 6b1f30f0f7012237def20c79ffb12184c66dac4c Mon Sep 17 00:00:00 2001 From: itsscb Date: Tue, 17 Oct 2023 23:16:31 +0200 Subject: [PATCH] ft/adds table queries TODO: Add support for multi table requests - currently the existing fields are being merged. --- Makefile | 4 +- .../migration/000003_add_query_table.down.sql | 1 + .../migration/000003_add_query_table.up.sql | 17 +++ bff/db/mock/store.go | 35 ++++++ bff/db/query/query.sql | 3 + bff/db/sqlc/models.go | 6 + bff/db/sqlc/querier.go | 1 + bff/db/sqlc/query.go | 57 +++++++++ bff/db/sqlc/query.sql.go | 22 ++++ bff/db/sqlc/store.go | 1 + bff/gw/query.go | 109 ++++++++++++++++++ bff/main.go | 1 + 12 files changed, 255 insertions(+), 2 deletions(-) create mode 100644 bff/db/migration/000003_add_query_table.down.sql create mode 100644 bff/db/migration/000003_add_query_table.up.sql create mode 100644 bff/db/query/query.sql create mode 100644 bff/db/sqlc/query.go create mode 100644 bff/db/sqlc/query.sql.go create mode 100644 bff/gw/query.go diff --git a/Makefile b/Makefile index 16af16f..6efea5a 100644 --- a/Makefile +++ b/Makefile @@ -52,10 +52,10 @@ migratedown: docker run --name migratedown --privileged=true --rm -v $(PWD)/bff/db/migration:/migrations --network host migrate/migrate -path=/migrations/ -database $(DB_URL) down createdb: - docker exec -it postgres createdb --username=root --owner=root df + docker exec -it df-bff_postgres_1 createdb --username=root --owner=root df dropdb: - docker exec -it postgres dropdb df + docker exec -it df-bff_postgres_1 dropdb df sqlc: cd bff && \ diff --git a/bff/db/migration/000003_add_query_table.down.sql b/bff/db/migration/000003_add_query_table.down.sql new file mode 100644 index 0000000..d956a93 --- /dev/null +++ b/bff/db/migration/000003_add_query_table.down.sql @@ -0,0 +1 @@ +DROP TABLE IF EXISTS "queries"; diff --git a/bff/db/migration/000003_add_query_table.up.sql b/bff/db/migration/000003_add_query_table.up.sql new file mode 100644 index 0000000..3aae2b9 --- /dev/null +++ b/bff/db/migration/000003_add_query_table.up.sql @@ -0,0 +1,17 @@ +CREATE TABLE "queries" ( + "id" bigserial UNIQUE PRIMARY KEY NOT NULL, + "name" varchar NOT NULL, + "query" varchar NOT NULL +); + +INSERT INTO "queries" (name, query) +VALUES ( + 'all_accounts', + 'SELECT * FROM accounts' +); + +INSERT INTO "queries" (name, query) +VALUES ( + 'all_persons', + 'SELECT * FROM persons' +); \ No newline at end of file diff --git a/bff/db/mock/store.go b/bff/db/mock/store.go index bd8e1b8..4f71ecc 100644 --- a/bff/db/mock/store.go +++ b/bff/db/mock/store.go @@ -614,6 +614,21 @@ func (mr *MockStoreMockRecorder) GetProvider(arg0, arg1 any) *gomock.Call { return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetProvider", reflect.TypeOf((*MockStore)(nil).GetProvider), arg0, arg1) } +// GetQueryByName mocks base method. +func (m *MockStore) GetQueryByName(arg0 context.Context, arg1 string) (db.Query, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "GetQueryByName", arg0, arg1) + ret0, _ := ret[0].(db.Query) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// GetQueryByName indicates an expected call of GetQueryByName. +func (mr *MockStoreMockRecorder) GetQueryByName(arg0, arg1 any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetQueryByName", reflect.TypeOf((*MockStore)(nil).GetQueryByName), arg0, arg1) +} + // GetReturn mocks base method. func (m *MockStore) GetReturn(arg0 context.Context, arg1 uint64) (db.Return, error) { m.ctrl.T.Helper() @@ -854,6 +869,26 @@ func (mr *MockStoreMockRecorder) ListSessions(arg0, arg1 any) *gomock.Call { return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ListSessions", reflect.TypeOf((*MockStore)(nil).ListSessions), arg0, arg1) } +// Query mocks base method. +func (m *MockStore) Query(arg0 context.Context, arg1 string, arg2 ...any) ([]map[string]any, error) { + m.ctrl.T.Helper() + varargs := []any{arg0, arg1} + for _, a := range arg2 { + varargs = append(varargs, a) + } + ret := m.ctrl.Call(m, "Query", varargs...) + ret0, _ := ret[0].([]map[string]any) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// Query indicates an expected call of Query. +func (mr *MockStoreMockRecorder) Query(arg0, arg1 any, arg2 ...any) *gomock.Call { + mr.mock.ctrl.T.Helper() + varargs := append([]any{arg0, arg1}, arg2...) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Query", reflect.TypeOf((*MockStore)(nil).Query), varargs...) +} + // UpdateAccount mocks base method. func (m *MockStore) UpdateAccount(arg0 context.Context, arg1 db.UpdateAccountParams) (db.Account, error) { m.ctrl.T.Helper() diff --git a/bff/db/query/query.sql b/bff/db/query/query.sql new file mode 100644 index 0000000..a77fa65 --- /dev/null +++ b/bff/db/query/query.sql @@ -0,0 +1,3 @@ +-- name: GetQueryByName :one +SELECT * FROM queries +WHERE "name" = sqlc.arg(name); \ No newline at end of file diff --git a/bff/db/sqlc/models.go b/bff/db/sqlc/models.go index 20920aa..9c4f855 100644 --- a/bff/db/sqlc/models.go +++ b/bff/db/sqlc/models.go @@ -108,6 +108,12 @@ type Provider struct { Changed time.Time `json:"changed"` } +type Query struct { + ID uint64 `json:"id"` + Name string `json:"name"` + Query string `json:"query"` +} + type Return struct { ID uint64 `json:"id"` PersonID uint64 `json:"person_id"` diff --git a/bff/db/sqlc/querier.go b/bff/db/sqlc/querier.go index 0c8fb83..97ca257 100644 --- a/bff/db/sqlc/querier.go +++ b/bff/db/sqlc/querier.go @@ -59,6 +59,7 @@ type Querier interface { GetPayment(ctx context.Context, id uint64) (Payment, error) GetPerson(ctx context.Context, id uint64) (Person, error) GetProvider(ctx context.Context, id uint64) (Provider, error) + GetQueryByName(ctx context.Context, name string) (Query, error) GetReturn(ctx context.Context, id uint64) (Return, error) GetReturnIDsByPersonID(ctx context.Context, personID uint64) ([]uint64, error) GetReturns(ctx context.Context, id uint64) ([]Return, error) diff --git a/bff/db/sqlc/query.go b/bff/db/sqlc/query.go new file mode 100644 index 0000000..3a63bfc --- /dev/null +++ b/bff/db/sqlc/query.go @@ -0,0 +1,57 @@ +package db + +import ( + "context" + "database/sql" + "log/slog" +) + +var ( + restrictedFields = map[string]bool{ + "passwordhash": true, + } +) + +func (store *SQLStore) Query(ctx context.Context, statement string, args ...interface{}) (result []map[string]interface{}, err error) { + var rows *sql.Rows + // var err error + + if len(args) > 0 { + rows, err = store.db.QueryContext(ctx, statement, args) + } else { + rows, err = store.db.QueryContext(ctx, statement) + } + if err != nil { + slog.Error("db_query", slog.String("statement", statement), slog.String("error", err.Error())) + return nil, err + } + + cols, err := rows.Columns() + if err != nil { + slog.Error("db_query: getting columns", slog.String("error", err.Error())) + return nil, err + } + + for rows.Next() { + values := make([]interface{}, len(cols)) + res := make(map[string]interface{}) + for i := range values { + values[i] = new(interface{}) + } + err = rows.Scan(values...) + if err != nil { + slog.Error("db_query: scanning rows", slog.String("error", err.Error())) + return nil, err + } + for i, col := range cols { + if restrictedFields[col] { + continue + } + res[col] = *(values[i].(*interface{})) + } + + result = append(result, res) + } + + return result, rows.Err() +} diff --git a/bff/db/sqlc/query.sql.go b/bff/db/sqlc/query.sql.go new file mode 100644 index 0000000..89c46f5 --- /dev/null +++ b/bff/db/sqlc/query.sql.go @@ -0,0 +1,22 @@ +// Code generated by sqlc. DO NOT EDIT. +// versions: +// sqlc v1.22.0 +// source: query.sql + +package db + +import ( + "context" +) + +const getQueryByName = `-- name: GetQueryByName :one +SELECT id, name, query FROM queries +WHERE "name" = $1 +` + +func (q *Queries) GetQueryByName(ctx context.Context, name string) (Query, error) { + row := q.db.QueryRowContext(ctx, getQueryByName, name) + var i Query + err := row.Scan(&i.ID, &i.Name, &i.Query) + return i, err +} diff --git a/bff/db/sqlc/store.go b/bff/db/sqlc/store.go index d2cb8a9..0fb048b 100644 --- a/bff/db/sqlc/store.go +++ b/bff/db/sqlc/store.go @@ -17,6 +17,7 @@ type Store interface { DeletePersonTx(ctx context.Context, id uint64) error CreateDocumentTx(ctx context.Context, arg CreateDocumentTxParams) (doc Document, code int, err error) DeleteDocumentTx(ctx context.Context, id uint64) (code codes.Code, err error) + Query(ctx context.Context, statement string, args ...interface{}) (result []map[string]interface{}, err error) } // Store provides all functions to execute db queries and transactions diff --git a/bff/gw/query.go b/bff/gw/query.go new file mode 100644 index 0000000..7f782bb --- /dev/null +++ b/bff/gw/query.go @@ -0,0 +1,109 @@ +package gw + +import ( + "encoding/json" + "errors" + "fmt" + "io" + "net/http" + "strings" + + "github.com/gin-gonic/gin" +) + +func (server *Server) Query(ctx *gin.Context) { + // authHeader := ctx.GetHeader("authorization") + + // authFields := strings.Fields(authHeader) + + // if len(authFields) != 2 { + // ctx.JSON(http.StatusUnauthorized, errorResponse(errors.New("invalid or missing authorization header"))) + // return + // } + + // token := authFields[1] + + // authPayload, err := server.tokenMaker.VerifyToken(token) + // if err != nil { + // ctx.JSON(http.StatusUnauthorized, errorResponse(errors.New("invalid authorization header"))) + // return + // } + + // account, err := server.store.GetAccount(ctx, authPayload.AccountID) + // if err != nil { + // ctx.JSON(http.StatusNotFound, errorResponse(errors.New("account not found"))) + // return + // } + + var jsonData map[string]interface{} + data, err := io.ReadAll(ctx.Request.Body) + if err != nil { + ctx.JSON(http.StatusBadRequest, errorResponse(errors.New("failed to parse request body"))) + return + } + + fmt.Println(string(data)) + + if err = json.Unmarshal(data, &jsonData); err != nil { + ctx.JSON(http.StatusBadRequest, errorResponse(errors.New("failed to unmarshal request body json"))) + return + } + + name, ok := jsonData["name"] + if !ok { + ctx.JSON(http.StatusBadRequest, errorResponse(errors.New("required query name is missing"))) + return + } + + q, err := server.store.GetQueryByName(ctx, fmt.Sprintf("%s", name)) + if err != nil { + ctx.JSON(http.StatusBadRequest, errorResponse(errors.New("failed to get query"))) + return + } + + res, err := server.store.Query(ctx, q.Query) + if err != nil { + ctx.JSON(http.StatusBadRequest, errorResponse(fmt.Errorf("failed to run query: %v", err))) + return + } + + ctx.JSON(http.StatusOK, res) + return + + // var req *uploadDocumentRequest + // err = ctx.ShouldBind(&req) + // if err != nil { + // ctx.JSON(http.StatusBadRequest, errorResponse(errors.New("failed to parse request"))) + // return + // } + + // r := db.CreateDocumentTxParams{ + // AccountID: account.ID, + // PersonID: req.PersonID, + // MailID: req.MailID, + // File: req.File, + // Creator: account.Email, + // } + + // doc, code, err := server.store.CreateDocumentTx(ctx, r) + // if err != nil { + // if code == http.StatusInternalServerError { + // slog.Error("create_document", slog.Int64("invoked_by", int64(authPayload.AccountID)), slog.String("document_name", req.File.Filename), slog.String("error", err.Error())) + // } + // ctx.JSON(code, errorResponse(err)) + // return + // } + + // ctx.JSON(http.StatusOK, doc) +} + +func clearStatement(statement string) string { + statement = strings.ReplaceAll(statement, ";", "") + statement = strings.ReplaceAll(statement, "'", "") + statement = strings.ReplaceAll(statement, "\"", "") + statement = strings.ReplaceAll(statement, "`", "") + statement = strings.ReplaceAll(statement, "DROP", "") + statement = strings.ReplaceAll(statement, "DELETE", "") + + return statement +} diff --git a/bff/main.go b/bff/main.go index bb672a2..6078e07 100644 --- a/bff/main.go +++ b/bff/main.go @@ -144,6 +144,7 @@ func runGatewayServer(config util.Config, store db.Store, swaggerFS http.FileSys mux := gin.New() mux.Group("v1/*{grpc_gateway}").Any("", gin.WrapH(grpcMux)) mux.POST("documents/upload", server.UploadDocument) + mux.POST("query", server.Query) mux.StaticFS("/swagger/", swaggerFS) listener, err := net.Listen("tcp", config.HTTPServerAddress)