Skip to content

Commit

Permalink
update: add folder routes
Browse files Browse the repository at this point in the history
  • Loading branch information
abhijit-hota committed May 19, 2022
1 parent e147cb0 commit a419766
Show file tree
Hide file tree
Showing 6 changed files with 250 additions and 48 deletions.
14 changes: 12 additions & 2 deletions api/api.go
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
package main

import (
"net/http"

"github.com/abhijit-hota/rengoku/server/db"
"github.com/abhijit-hota/rengoku/server/handlers"
"github.com/abhijit-hota/rengoku/server/utils"
"net/http"

"github.com/gin-gonic/gin"
"github.com/joho/godotenv"
Expand Down Expand Up @@ -32,7 +33,7 @@ func main() {
db.InitializeDB()

router := gin.Default()
router.Use(CORSMiddleware())
// router.Use(CORSMiddleware())
router.Static("css", "views/css")
router.Static("js", "views/js")
router.LoadHTMLGlob("views/html/*.html")
Expand Down Expand Up @@ -62,6 +63,15 @@ func main() {
tagRouter.GET("/tree", handlers.GetLinkTree)
}

folderRouter := apiRouter.Group("/folders")
{
folderRouter.POST("", handlers.CreateFolder)
folderRouter.GET("", handlers.GetRootFolders)
folderRouter.PATCH("/:id", handlers.UpdateFolderName)
folderRouter.DELETE("/:id", handlers.DeleteFolder)
folderRouter.GET("/tree", handlers.GetLinkTree)
}

configRouter := apiRouter.Group("/config")
{
configRouter.GET("", handlers.GetConfig)
Expand Down
11 changes: 10 additions & 1 deletion api/db/bookmark.model.go
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
package db

import (
"github.com/abhijit-hota/rengoku/server/utils"
"net/url"
"strings"

"github.com/abhijit-hota/rengoku/server/utils"
)

type Meta struct {
Expand Down Expand Up @@ -38,3 +39,11 @@ type Tag struct {
Created int64 `json:"created,omitempty" form:"created"`
LastUpdated int64 `json:"last_updated,omitempty" form:"last_updated"`
}

type Folder struct {
ID int64 `json:"id"`
Name string `json:"name" form:"name" binding:"required"`
Path string `json:"path" form:"path"`
Created int64 `json:"created,omitempty" form:"created"`
LastUpdated int64 `json:"last_updated,omitempty" form:"last_updated"`
}
16 changes: 15 additions & 1 deletion api/db/db.go
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
package db

import (
"github.com/abhijit-hota/rengoku/server/utils"
"database/sql"
"os"

"github.com/abhijit-hota/rengoku/server/utils"
_ "github.com/mattn/go-sqlite3"
)

Expand Down Expand Up @@ -40,6 +40,20 @@ CREATE TABLE IF NOT EXISTS links_tags (
tag_id INTEGER NOT NULL REFERENCES tags(id),
link_id INTEGER NOT NULL REFERENCES links(id),
UNIQUE(tag_id, link_id) ON CONFLICT IGNORE
);
CREATE TABLE IF NOT EXISTS folders (
id INTEGER NOT NULL PRIMARY KEY,
name TEXT NOT NULL,
path TEXT,
created INTEGER,
last_updated INTEGER,
UNIQUE(name, path)
);
CREATE INDEX IF NOT EXISTS folder_path ON folders(path);
CREATE TABLE IF NOT EXISTS links_folders (
folder_id TEXT NOT NULL REFERENCES folders(id),
link_id INTEGER NOT NULL REFERENCES links(id),
UNIQUE(folder_id, link_id) ON CONFLICT IGNORE
);`

_, err = db.Exec(t)
Expand Down
204 changes: 204 additions & 0 deletions api/handlers/folders.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,204 @@
package handlers

import (
"net/http"
"regexp"
"strconv"
"strings"
"time"

DB "github.com/abhijit-hota/rengoku/server/db"
"github.com/abhijit-hota/rengoku/server/utils"
"github.com/gin-gonic/gin"
)

type Node struct {
Children Tree `json:"children,omitempty"`
Name string `json:"name"`
}
type Tree map[string]*Node

func GetLinkTree(ctx *gin.Context) {
db := DB.GetDB()

rows, err := db.Query(`SELECT id, name, path FROM folders`)
utils.Must(err)
defer rows.Close()

linktree := make(Tree)

for rows.Next() {
var linkID int
var name string
var path string

err = rows.Scan(&linkID, &name, &path)
utils.Must(err)

pathArr := strings.Split(path+strconv.Itoa(linkID), "/")
cursor := linktree

depth := len(pathArr) - 1
for index, tag := range pathArr {
if cursor[tag] == nil {
cursor[tag] = &Node{make(Tree), ""}
}
if index == depth {
cursor[tag].Name = name
}
cursor = cursor[tag].Children
}
}
err = rows.Err()
utils.Must(err)
ctx.JSON(http.StatusOK, linktree)
}

func GetRootFolders(ctx *gin.Context) {
db := DB.GetDB()

var query struct {
Str string `form:"q"`
}

if err := ctx.BindQuery(&query); err != nil {
return
}

dbQuery := "SELECT * FROM folders"
if query.Str != "" {
dbQuery += " WHERE name LIKE '%" + query.Str + "%'"
}
preparedStmt, err := db.Prepare(dbQuery)
utils.Must(err)

rows, err := preparedStmt.Query()
defer rows.Close()
utils.Must(err)

folders := make([]DB.Folder, 0)
for rows.Next() {
var folder DB.Folder

rows.Scan(
&folder.ID,
&folder.Name,
&folder.Path,
&folder.Created,
&folder.LastUpdated,
)
folders = append(folders, folder)
}
if rows.Err() != nil {
ctx.JSON(http.StatusInternalServerError, gin.H{"error": "INTERNAL_ERROR"})
}
ctx.JSON(http.StatusOK, folders)
}

var re = regexp.MustCompile(`(.*/|)(\d{1,})/$`)

func CreateFolder(ctx *gin.Context) {
db := DB.GetDB()

var req struct {
NameRequest
Path string `json:"path,omitempty" form:"path"`
}
if err := ctx.Bind(&req); err != nil {
return
}

now := time.Now().Unix()

split := re.FindStringSubmatch(req.Path)
if len(split) > 0 {
parentPath, immediateParent := split[1], split[2]

query := "SELECT COUNT(*) FROM folders WHERE id = ? AND path = ?"
result := db.QueryRow(query, immediateParent, parentPath)
var numId int
result.Scan(&numId)

if numId != 1 {
ctx.JSON(http.StatusBadRequest, gin.H{"message": "INVALID_FOLDER_PATH"})
return
}
} else {
req.Path = ""
}

stmt := "INSERT INTO folders (name, path, created, last_updated) VALUES (?, ?, ?, ?)"
res, err := db.Exec(stmt, req.Name, req.Path, now, now)
if err != nil && strings.HasPrefix(err.Error(), "UNIQUE constraint failed") || utils.MustGet(res.RowsAffected()) == 0 {
ctx.JSON(http.StatusBadRequest, gin.H{"code": "NAME_ALREADY_PRESENT"})
return
}
utils.Must(err)
latestId := utils.MustGet(res.LastInsertId())
folder := DB.Folder{
ID: latestId,
Created: now,
LastUpdated: now,
Name: req.Name,
Path: req.Path,
}
ctx.JSON(http.StatusOK, folder)
}

func UpdateFolderName(ctx *gin.Context) {
db := DB.GetDB()

var uri IdUri

if err := ctx.BindUri(&uri); err != nil {
return
}

var req NameRequest
if err := ctx.Bind(&req); err != nil {
return
}

tx, err := db.Begin()
utils.Must(err)

statement := "UPDATE folders SET name = ?, last_updated = ? WHERE id = ? AND name != ?"
now := time.Now().Unix()

_, err = tx.Exec(statement, req.Name, now, uri.ID, req.Name)
if err != nil && strings.HasPrefix(err.Error(), "UNIQUE constraint failed") {
ctx.JSON(http.StatusBadRequest, gin.H{"code": "NAME_ALREADY_PRESENT"})
return
}
utils.Must(err)

var folder DB.Folder
updatedTag := tx.QueryRow("SELECT * FROM folders WHERE id = ?", uri.ID)
updatedTag.Scan(&folder.ID, &folder.Name, &folder.Path, &folder.Created, &folder.LastUpdated)

tx.Commit()
ctx.JSON(http.StatusOK, folder)
}

func DeleteFolder(ctx *gin.Context) {
db := DB.GetDB()

var uri IdUri
if err := ctx.BindUri(&uri); err != nil {
return
}

tx, _ := db.Begin()

statement := "DELETE FROM folders WHERE id = ?"
info, err := tx.Exec(statement, uri.ID)
utils.Must(err)
numDeleted, _ := info.RowsAffected()

statement = "DELETE FROM links_folders WHERE folder_id = ?"
_, err = tx.Exec(statement, uri.ID)
utils.Must(err)

tx.Commit()
ctx.JSON(http.StatusOK, gin.H{"deleted": numDeleted == 1})
}
49 changes: 5 additions & 44 deletions api/handlers/tags.go
Original file line number Diff line number Diff line change
@@ -1,57 +1,17 @@
package handlers

import (
DB "github.com/abhijit-hota/rengoku/server/db"
"github.com/abhijit-hota/rengoku/server/utils"
"fmt"
"net/http"
"strings"
"time"

DB "github.com/abhijit-hota/rengoku/server/db"
"github.com/abhijit-hota/rengoku/server/utils"

"github.com/gin-gonic/gin"
)

type Node struct {
Children Tree `json:"children"`
Links []int `json:"links"`
}
type Tree map[string]*Node

func GetLinkTree(ctx *gin.Context) {
db := DB.GetDB()

rows, err := db.Query(`SELECT links_tags.link_id, tags.path FROM links_tags JOIN tags ON tags.id = links_tags.tag_id;`)
utils.Must(err)
defer rows.Close()

linktree := make(Tree)

for rows.Next() {
var linkID int
var path string

err = rows.Scan(&linkID, &path)
utils.Must(err)

pathArr := strings.Split(path, "/")
depth := len(pathArr) - 1
cursor := linktree

for index, tag := range pathArr {
if cursor[tag] == nil {
cursor[tag] = &Node{make(Tree), make([]int, 0)}
}
if index == depth {
cursor[tag].Links = append(cursor[tag].Links, linkID)
}
cursor = cursor[tag].Children
}
}
err = rows.Err()
utils.Must(err)
ctx.JSON(http.StatusOK, linktree)
}

type IdUri struct {
ID int64 `uri:"id" binding:"required"`
}
Expand Down Expand Up @@ -115,7 +75,7 @@ func CreateTag(ctx *gin.Context) {

stmt := "INSERT INTO tags (name, created, last_updated) VALUES (?, ?, ?)"
res, err := db.Exec(stmt, req.Name, now, now)
if err != nil && strings.HasPrefix(err.Error(), "UNIQUE constraint failed") {
if err != nil && strings.HasPrefix(err.Error(), "UNIQUE constraint failed") || utils.MustGet(res.RowsAffected()) == 0 {
ctx.JSON(http.StatusBadRequest, gin.H{"code": "NAME_ALREADY_PRESENT"})
return
}
Expand Down Expand Up @@ -183,5 +143,6 @@ func DeleteTag(ctx *gin.Context) {
_, err = tx.Exec(statement, uri.ID)
utils.Must(err)

tx.Commit()
ctx.JSON(http.StatusOK, gin.H{"deleted": numDeleted == 1})
}
4 changes: 4 additions & 0 deletions api/utils/error_handling.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,3 +5,7 @@ func Must(err error) {
panic(err)
}
}
func MustGet[T any](v T, err error) T {
Must(err)
return v
}

0 comments on commit a419766

Please sign in to comment.