From 06b3589ea6db9803fa83832d7cefec1730d0de6c Mon Sep 17 00:00:00 2001 From: Richie Varghese Date: Sat, 12 Feb 2022 20:02:21 +0530 Subject: [PATCH] completed basic functionality for buntdb --- README.md | 8 +- server/db.go | 299 +++++++++++++---------- server/db_test.go | 7 +- server/model.go | 6 + server/server.go | 2 +- ui/src/components/DBSelectStepper.tsx | 1 - ui/src/components/DatagridComponents.tsx | 107 ++++++-- ui/src/controllers/DatagridList.tsx | 77 +++++- 8 files changed, 337 insertions(+), 170 deletions(-) diff --git a/README.md b/README.md index bbbb828..d80bff5 100644 --- a/README.md +++ b/README.md @@ -22,10 +22,10 @@ Simple DB CRUD operations service. Supports some golang Key-Value pair file base - [x] ~~Upload existing DB~~ - [x] ~~View Key-Value pairs~~ -- [ ] Add new Key-Value pair -- [ ] Remove Key-Value pair -- [ ] Update Key-Value pair -- [ ] Download updated file +- [x] Add new Key-Value pair +- [x] Remove Key-Value pair +- [x] Update Key-Value pair +- [x] Download updated file - [ ] View Buckets in boltDB - [ ] Add / remove bucket - [ ] Move/Copy Key-Value pair under a bucket to another bucket diff --git a/server/db.go b/server/db.go index f1c43a2..0d23f89 100644 --- a/server/db.go +++ b/server/db.go @@ -2,10 +2,11 @@ package server import ( "encoding/json" + "errors" "fmt" + "io/fs" "io/ioutil" "log" - "net/url" "os" "sync" @@ -148,101 +149,20 @@ func loadFile(ctx *fasthttp.RequestCtx) { // load the db from user session userSession, valid := session.Load(dbKey) if !valid { - - // get the file name under folder - dbTypes, err := ioutil.ReadDir("temp" + string(os.PathSeparator)) - if err != nil { - log.Println(err) - ctx.Error("Error reading db folder: "+err.Error(), fasthttp.StatusInternalServerError) - return - } - log.Println("dbTypes:", dbTypes) - - // iterate over files - for _, dbType := range dbTypes { - - // get the file name under folder - dbKeys, err := ioutil.ReadDir("temp" + string(os.PathSeparator) + dbType.Name() + string(os.PathSeparator)) - if err != nil { - log.Println(err) - ctx.Error("Error reading db folder: "+err.Error(), fasthttp.StatusInternalServerError) - return - } - log.Println("dbKeys:", dbKeys) - - // iterate over files - for _, dbkey := range dbKeys { - log.Println("dbkey:", dbkey.Name(), " | dbKey:", dbkey) - - if dbkey.Name() == dbKey { - - // get the file name under folder - files, err := ioutil.ReadDir("temp" + string(os.PathSeparator) + dbType.Name() + string(os.PathSeparator) + dbKey) - if err != nil { - log.Println(err) - ctx.Error("Error reading db folder: "+err.Error(), fasthttp.StatusInternalServerError) - return - } - - // get the file name - file := files[0].Name() - userSession = Session{dbKey, file, dbType.Name(), nil} - log.Println("userSession: ", userSession) - session.Store(dbKey, userSession) - } - } - } - - // if user session is still nil, return error - if userSession == nil { - log.Println("invalid dbKey") - ctx.Error("invalid dbKey", fasthttp.StatusBadRequest) - return - } - } - sessionInfo := userSession.(Session) - log.Println("sessionInfo : ", sessionInfo) - - // switch on db type - switch sessionInfo.DBType { - - case database.BOLT_DB: - - // create the new boltdb file in the temp dir - log.Println("creating new boltdb file:", "temp"+string(os.PathSeparator)+sessionInfo.DBType+string(os.PathSeparator)+dbKey+string(os.PathSeparator)+sessionInfo.FileName) - db, err := database.NewDB("temp"+string(os.PathSeparator)+sessionInfo.DBType+string(os.PathSeparator)+dbKey+string(os.PathSeparator)+sessionInfo.FileName, sessionInfo.DBType) - if err != nil { + var err error + // try restoring user session + if userSession, err = restoreSession(dbKey); err != nil { log.Println(err) ctx.Error(err.Error(), fasthttp.StatusInternalServerError) - os.RemoveAll("temp" + string(os.PathSeparator) + dbKey) return } - defer db.CloseDB() - - // store the db access in the session - session.Store(dbKey, Session{dbKey, sessionInfo.FileName, sessionInfo.DBType, db}) - - case database.BUNT_DB: - - // create the new buntdb file in the temp dir - log.Println("creating new buntdb file:", "temp"+string(os.PathSeparator)+sessionInfo.DBType+string(os.PathSeparator)+dbKey+string(os.PathSeparator)+sessionInfo.FileName) - db, err := database.NewDB("temp"+string(os.PathSeparator)+sessionInfo.DBType+string(os.PathSeparator)+dbKey+string(os.PathSeparator)+sessionInfo.FileName, sessionInfo.DBType) - if err != nil { - log.Println(err) - ctx.Error(err.Error(), fasthttp.StatusInternalServerError) - os.RemoveAll("temp" + string(os.PathSeparator) + dbKey) - return - } - - // store the db access in the session - session.Store(dbKey, Session{dbKey, sessionInfo.FileName, sessionInfo.DBType, db}) } // return success message to UI json.NewEncoder(ctx).Encode(apiResponse{ DBKey: dbKey, - FileName: sessionInfo.FileName, - DBType: sessionInfo.DBType, + FileName: userSession.(Session).FileName, + DBType: userSession.(Session).DBType, Message: "Successfully created boltdb file: " + dbKey, Error: nil, }) @@ -260,9 +180,13 @@ func listKeyValue(ctx *fasthttp.RequestCtx) { // load the db from user session userSession, valid := session.Load(dbKey) if !valid { - log.Println("invalid dbKey") - ctx.Error("invalid dbKey", fasthttp.StatusBadRequest) - return + var err error + // try restoring user session + if userSession, err = restoreSession(dbKey); err != nil { + log.Println(err) + ctx.Error(err.Error(), fasthttp.StatusInternalServerError) + return + } } sessionInfo := userSession.(Session) @@ -385,10 +309,15 @@ func removeFile(ctx *fasthttp.RequestCtx) { // load the db from user session userSession, valid := session.Load(dbKey) if !valid { - log.Println("invalid dbKey") - ctx.Error("invalid dbKey", fasthttp.StatusBadRequest) - return + var err error + // try restoring user session + if userSession, err = restoreSession(dbKey); err != nil { + log.Println(err) + ctx.Error(err.Error(), fasthttp.StatusInternalServerError) + return + } } + log.Println("userSession:", userSession) userSession.(Session).DB.CloseDB() dbType := userSession.(Session).DBType session.Delete(dbKey) @@ -410,13 +339,9 @@ func removeFile(ctx *fasthttp.RequestCtx) { }) } +// insertKeyValue is the handler for the POST /api/v1/db/dbKey/file endpoint. func insertKeyValue(ctx *fasthttp.RequestCtx) { - type NewEntry struct { - Key string `json:"key"` - Value string `json:"value"` - } - // get the dbKey from params dbKey := string(ctx.QueryArgs().Peek("dbkey")) log.Println("dbKey:", dbKey) @@ -424,9 +349,13 @@ func insertKeyValue(ctx *fasthttp.RequestCtx) { // load the db from user session userSession, valid := session.Load(dbKey) if !valid { - log.Println("invalid dbKey") - ctx.Error("invalid dbKey", fasthttp.StatusBadRequest) - return + var err error + // try restoring user session + if userSession, err = restoreSession(dbKey); err != nil { + log.Println(err) + ctx.Error(err.Error(), fasthttp.StatusInternalServerError) + return + } } sessionInfo := userSession.(Session) @@ -468,9 +397,13 @@ func insertBucket(ctx *fasthttp.RequestCtx) { // load the db from user session userSession, valid := session.Load(dbKey) if !valid { - log.Println("invalid dbKey") - ctx.Error("invalid dbKey", fasthttp.StatusBadRequest) - return + var err error + // try restoring user session + if userSession, err = restoreSession(dbKey); err != nil { + log.Println(err) + ctx.Error(err.Error(), fasthttp.StatusInternalServerError) + return + } } sessionInfo := userSession.(Session) @@ -505,9 +438,13 @@ func deleteBucket(ctx *fasthttp.RequestCtx) { // load the db from user session userSession, valid := session.Load(dbKey) if !valid { - log.Println("invalid dbKey") - ctx.Error("invalid dbKey", fasthttp.StatusBadRequest) - return + var err error + // try restoring user session + if userSession, err = restoreSession(dbKey); err != nil { + log.Println(err) + ctx.Error(err.Error(), fasthttp.StatusInternalServerError) + return + } } sessionInfo := userSession.(Session) @@ -535,24 +472,37 @@ func deleteBucket(ctx *fasthttp.RequestCtx) { // Removes a key from the boltdb file. func deleteKeyValue(ctx *fasthttp.RequestCtx) { + type deleteKeys struct { + Keys []string `json:"keys"` + } + // get the dbKey from params - dbKey := string(ctx.UserValue("dbKey").(string)) + dbKey := string(ctx.QueryArgs().Peek("dbkey")) log.Println("dbKey:", dbKey) - // get the key from params - key := string(ctx.UserValue("key").(string)) - key, _ = url.QueryUnescape(key) - log.Println("key:", key) - // load the db from user session userSession, valid := session.Load(dbKey) if !valid { - log.Println("invalid dbKey") - ctx.Error("invalid dbKey", fasthttp.StatusBadRequest) - return + var err error + // try restoring user session + if userSession, err = restoreSession(dbKey); err != nil { + log.Println(err) + ctx.Error(err.Error(), fasthttp.StatusInternalServerError) + return + } } sessionInfo := userSession.(Session) + // get the value from payload + var keys deleteKeys + err := json.Unmarshal(ctx.PostBody(), &keys) + if err != nil { + log.Println(err) + ctx.Error(err.Error(), fasthttp.StatusInternalServerError) + return + } + log.Println("keys:", keys) + switch sessionInfo.DBType { case database.BOLT_DB: @@ -563,12 +513,24 @@ func deleteKeyValue(ctx *fasthttp.RequestCtx) { } + // for each selected keys delete from DB + for _, key := range keys.Keys { + + // delete key from DB + err = sessionInfo.DB.Delete(key) + if err != nil { + log.Println(err) + ctx.Error(err.Error(), fasthttp.StatusInternalServerError) + return + } + } + // return success message to UI json.NewEncoder(ctx).Encode(apiResponse{ DBKey: dbKey, FileName: sessionInfo.FileName, DBType: sessionInfo.DBType, - Message: "Successfully deleted key: " + key, + Message: "Successfully deleted keys from DB", Error: nil, }) } @@ -578,7 +540,7 @@ func deleteKeyValue(ctx *fasthttp.RequestCtx) { func updateKeyValue(ctx *fasthttp.RequestCtx) { // get the dbKey from params - dbKey := string(ctx.UserValue("dbKey").(string)) + dbKey := string(ctx.QueryArgs().Peek("dbkey")) log.Println("dbKey:", dbKey) // get the key from params @@ -588,21 +550,25 @@ func updateKeyValue(ctx *fasthttp.RequestCtx) { // load the db from user session userSession, valid := session.Load(dbKey) if !valid { - log.Println("invalid dbKey") - ctx.Error("invalid dbKey", fasthttp.StatusBadRequest) - return + var err error + // try restoring user session + if userSession, err = restoreSession(dbKey); err != nil { + log.Println(err) + ctx.Error(err.Error(), fasthttp.StatusInternalServerError) + return + } } sessionInfo := userSession.(Session) // get the value from payload - var value string - err := json.Unmarshal(ctx.PostBody(), &value) + var data NewEntry + err := json.Unmarshal(ctx.PostBody(), &data) if err != nil { log.Println(err) ctx.Error(err.Error(), fasthttp.StatusInternalServerError) return } - log.Println("value:", value) + log.Println("data:", data) switch sessionInfo.DBType { @@ -614,6 +580,14 @@ func updateKeyValue(ctx *fasthttp.RequestCtx) { } + // add new entry to DB + err = sessionInfo.DB.Add(key, data.Value) + if err != nil { + log.Println(err) + ctx.Error(err.Error(), fasthttp.StatusInternalServerError) + return + } + // return success message to UI json.NewEncoder(ctx).Encode(apiResponse{ DBKey: dbKey, @@ -635,9 +609,13 @@ func downloadFile(ctx *fasthttp.RequestCtx) { // load the db from user session userSession, valid := session.Load(dbKey) if !valid { - log.Println("invalid dbKey") - ctx.Error("invalid dbKey", fasthttp.StatusBadRequest) - return + var err error + // try restoring user session + if userSession, err = restoreSession(dbKey); err != nil { + log.Println(err) + ctx.Error(err.Error(), fasthttp.StatusInternalServerError) + return + } } sessionInfo := userSession.(Session) sessionInfo.DB.CloseDB() @@ -645,3 +623,70 @@ func downloadFile(ctx *fasthttp.RequestCtx) { // return the file to the UI ctx.SendFile("temp" + string(os.PathSeparator) + sessionInfo.DBType + string(os.PathSeparator) + dbKey + string(os.PathSeparator) + sessionInfo.FileName) } + +// restoreSession restores the user session from the boltdb / buntdb file if it exists on client +func restoreSession(dbKey string) (userSession Session, err error) { + + // get the file name under folder + dbTypes, err := ioutil.ReadDir("temp" + string(os.PathSeparator)) + if err != nil { + log.Println(err) + err = errors.New("Error reading db folder: " + err.Error()) + return + } + log.Println("dbTypes:", dbTypes) + + // iterate over files + for _, dbType := range dbTypes { + + var dbKeys []fs.FileInfo + // get the file name under folder + dbKeys, err = ioutil.ReadDir("temp" + string(os.PathSeparator) + dbType.Name() + string(os.PathSeparator)) + if err != nil { + log.Println(err) + err = errors.New("Error reading db folder: " + err.Error()) + return + } + log.Println("dbKeys:", dbKeys) + + // iterate over files + for _, dbkey := range dbKeys { + log.Println("dbkey:", dbkey.Name(), " | dbKey:", dbkey) + + if dbkey.Name() == dbKey { + + var files []fs.FileInfo + // get the file name under folder + files, err = ioutil.ReadDir("temp" + string(os.PathSeparator) + dbType.Name() + string(os.PathSeparator) + dbKey) + if err != nil { + log.Println(err) + err = errors.New("Error reading db folder: " + err.Error()) + return + } + + // get the file name + file := files[0].Name() + + userSession.DB, err = database.NewDB("temp"+string(os.PathSeparator)+dbType.Name()+string(os.PathSeparator)+dbKey+string(os.PathSeparator)+file, dbType.Name()) + if err != nil { + log.Println(err) + err = errors.New(err.Error()) + os.RemoveAll("temp" + string(os.PathSeparator) + dbKey) + return + } + + userSession = Session{dbKey, file, dbType.Name(), userSession.DB} + log.Println("userSession: ", userSession) + session.Store(dbKey, userSession) + } + } + } + + // if user session is still nil, return error + if (userSession == Session{}) || userSession.DB == nil { + log.Println("invalid dbKey") + err = errors.New("invalid dbKey") + return + } + return +} diff --git a/server/db_test.go b/server/db_test.go index bce6364..42bbc3d 100644 --- a/server/db_test.go +++ b/server/db_test.go @@ -296,7 +296,7 @@ func Test_listKeyValue(t *testing.T) { testDB.Add("test-key", "test-value") defer testDB.CloseDB() - session.Store("test", Session{tt.args.dbKey, tt.args.file, tt.args.file, testDB}) + session.Store("test", Session{"test", tt.args.file, tt.args.file, testDB}) req, err := http.NewRequest("POST", "http://api/v1/db", nil) if err != nil { @@ -306,10 +306,7 @@ func Test_listKeyValue(t *testing.T) { // set query params q := req.URL.Query() - q.Add("dbKey", tt.args.dbKey) - q.Add("dbtype", tt.args.dbtype) - q.Add("bucket", tt.args.bucket) - q.Add("file", tt.args.file) + q.Add("dbkey", tt.args.dbKey) req.URL.RawQuery = q.Encode() // create a new client diff --git a/server/model.go b/server/model.go index 853579f..b16ac22 100644 --- a/server/model.go +++ b/server/model.go @@ -61,3 +61,9 @@ type Session struct { DBType string DB database.DB } + +// NewEntry - for creating new entry in the database +type NewEntry struct { + Key string `json:"key"` + Value string `json:"value"` +} diff --git a/server/server.go b/server/server.go index 495bf71..455a8bb 100644 --- a/server/server.go +++ b/server/server.go @@ -61,7 +61,7 @@ func Serve(port string, debug bool) { dbroutes.PUT("/{key}", updateKeyValue) // delete the boltdb file - dbroutes.DELETE("/{key}", deleteKeyValue) + dbroutes.DELETE("/", deleteKeyValue) } // /api/v1/bucket routes diff --git a/ui/src/components/DBSelectStepper.tsx b/ui/src/components/DBSelectStepper.tsx index 8967812..bb826ba 100644 --- a/ui/src/components/DBSelectStepper.tsx +++ b/ui/src/components/DBSelectStepper.tsx @@ -105,7 +105,6 @@ export default function VerticalLinearStepper( checked={checked} onChange={handleChange} inputProps={{ "aria-label": "controlled" }} - disabled /> } label={checked ? "Create new database" : "Upload existing database"} diff --git a/ui/src/components/DatagridComponents.tsx b/ui/src/components/DatagridComponents.tsx index bfd0178..6b36d96 100644 --- a/ui/src/components/DatagridComponents.tsx +++ b/ui/src/components/DatagridComponents.tsx @@ -1,5 +1,15 @@ import styled from "@emotion/styled"; -import { Box, Pagination } from "@mui/material"; +import { Delete } from "@material-ui/icons"; +import { + Box, + Button, + Fab, + Pagination, + Tooltip, + Typography, +} from "@mui/material"; +import { useSnackbar } from "notistack"; +import http from "../services/axios-common"; import { GridToolbarContainer, GridToolbarFilterButton, @@ -105,36 +115,91 @@ export const CustomNoRowsOverlay = () => { type CustomFooterStatusComponentProps = { status: string; setUpdated: React.Dispatch>; + dbkey: string; + showDelete: boolean; + keys: any[]; }; export const CustomFooterStatusComponent = ({ status, setUpdated, + dbkey, + showDelete, + keys, }: CustomFooterStatusComponentProps) => { const apiRef = useGridApiContext(); const page = useGridSelector(apiRef, gridPageSelector); const pageCount = useGridSelector(apiRef, gridPageCountSelector); + const { enqueueSnackbar, closeSnackbar } = useSnackbar(); + + // handles delete for keys in db/bucket + const handleDelete = (keys: any[]) => { + http + .delete("/api/v1/db/?dbkey=" + dbkey, { data: { keys: keys } }) + .then((resp) => { + enqueueSnackbar(resp.data.message, { + key: "Deleted", + variant: "success", + onClick: () => { + closeSnackbar("Deleted"); + }, + }); + setUpdated(true); + }) + .catch((err) => { + enqueueSnackbar(err.response.data.message, { + key: "error", + variant: "error", + onClick: () => { + closeSnackbar("error"); + }, + }); + }); + }; return ( - - - apiRef.current.setPage(value - 1)} - /> - + <> + + + + + {showDelete && ( + + handleDelete(keys)} + > + + + Delete + + + + )} + + apiRef.current.setPage(value - 1)} + /> + + *NOTE: Double click an entry to update the value + ); }; diff --git a/ui/src/controllers/DatagridList.tsx b/ui/src/controllers/DatagridList.tsx index 28c18cd..2bf9729 100644 --- a/ui/src/controllers/DatagridList.tsx +++ b/ui/src/controllers/DatagridList.tsx @@ -1,5 +1,5 @@ import { useState, useEffect } from "react"; -import { DataGrid } from "@mui/x-data-grid"; +import { DataGrid, GridSelectionModel } from "@mui/x-data-grid"; import { useSnackbar } from "notistack"; import http from "../services/axios-common"; import { @@ -29,19 +29,14 @@ export default function FixedSizeGrid(props: Props) { const [dataGrid, setDataGrid] = useState(data); const { enqueueSnackbar, closeSnackbar } = useSnackbar(); const [updated, setUpdated] = useState(false); + const [selectionModel, setSelectionModel] = useState([]); + const [keysToDelete, setKeysToDelete] = useState([]); + const [showDelete, setShowDelete] = useState(false); useEffect(() => { http .get("/api/v1/db/?dbkey=" + props.dbkey) .then((resp) => { - const now = new Date().getTime().toString(); - enqueueSnackbar("Updating tables", { - key: now, - variant: "info", - onClick: () => { - closeSnackbar(now); - }, - }); console.log("setting data", resp.data); setDataGrid(resp.data.data); setUpdated(false); @@ -56,13 +51,60 @@ export default function FixedSizeGrid(props: Props) { }, }); }); + setSelectionModel([]); // eslint-disable-next-line react-hooks/exhaustive-deps }, [updated]); + const handleUpdate = (e: any) => { + const newValue = e.value; + const key: any = dataGrid.rows[+e.id - 1]; + console.log("newValue", newValue, "key", key.key); + http + .put("/api/v1/db/" + key.key + "?dbkey=" + props.dbkey, { + value: newValue, + key: key.key, + }) + .then((resp) => { + enqueueSnackbar(resp.data.message, { + key: "updated", + variant: "success", + onClick: () => { + closeSnackbar("updated"); + }, + }); + setUpdated(true); + }) + .catch((err) => { + enqueueSnackbar(err.response.data.message, { + key: "error", + variant: "error", + onClick: () => { + closeSnackbar("error"); + }, + }); + }); + }; + + const handleSelection = (newSelectionModel: GridSelectionModel) => { + setSelectionModel(newSelectionModel); + // replace id with key + const keys = newSelectionModel.map((item) => { + const gridItem: any = dataGrid.rows[+item - 1]; + return gridItem.key; + }); + setKeysToDelete(keys); + console.log("keys", keys); + + if (keys.length > 0) { + setShowDelete(true); + } else { + setShowDelete(false); + } + }; + return ( - {/* @ts-ignore */} + handleSelection(newSelectionModel) + } + selectionModel={selectionModel} + onCellEditCommit={(e) => handleUpdate(e)} {...dataGrid} />