Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
34 changes: 31 additions & 3 deletions Internal/adapters/repository/dynamicDBRepo.go
Original file line number Diff line number Diff line change
@@ -1,5 +1,10 @@
package repository

import (
"github.com/Arjuna-Ragil/Localbase/Internal/core/domain"
"gorm.io/gorm"
)

type DynamicDBRepo struct {
DB *DBContainer
}
Expand All @@ -8,10 +13,33 @@ func NewDynamicDB(db *DBContainer) *DynamicDBRepo {
return &DynamicDBRepo{DB: db}
}

func (dr *DynamicDBRepo) CreateDynamicDB(query string) error {
_, err := dr.DB.Sqlx.Exec(query)
if err != nil {
func (dr *DynamicDBRepo) RunDynamicQuery(tx *gorm.DB, query string) error {
if err := tx.Exec(query).Error; err != nil {
return err
}
return nil
}

// Dynamic database note

func (dr *DynamicDBRepo) CreateDynTable(tx *gorm.DB, dynTable *domain.DynTableDef) (*domain.DynTableDef, error) {
if err := tx.Create(&dynTable).Error; err != nil {
return nil, err
}
return dynTable, nil
}

func (dr *DynamicDBRepo) CreateDynCol(tx *gorm.DB, dynCol *domain.DynColDef) error {
if err := tx.Create(&dynCol).Error; err != nil {
return err
}
return nil
}

func (dr *DynamicDBRepo) GetTables(projectID uint) ([]domain.DynTableDef, error) {
var tables []domain.DynTableDef
if err := dr.DB.Gorm.Preload("Columns").Where("project_id = ?", projectID).Find(&tables).Error; err != nil {
return nil, err
}
return tables, nil
}
2 changes: 2 additions & 0 deletions Internal/adapters/repository/postgres.go
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,8 @@ func (db *DBContainer) Migrate() error {
&domain.Invitation{},
&domain.Project{},
&domain.ProjectUser{},
&domain.DynTableDef{},
&domain.DynColDef{},
)
if err != nil {
log.Fatalf("Failed to migrate users: %v", err)
Expand Down
2 changes: 2 additions & 0 deletions Internal/api/Routes/routev1.go
Original file line number Diff line number Diff line change
Expand Up @@ -53,12 +53,14 @@ func SetupRouterV1(r *gin.Engine, deps Deps) {
{
project.GET("/projects", deps.Project.GetAllProjectHandler)
project.GET("/:projectid", deps.Project.GetProjectHandler)
project.GET("/:projectid/tables", deps.DynamicDB.GetDynTablesHandler)

manage := project.Group("/manage")
{
manage.POST("/invite/:projectid", deps.Project.InviteProjectHandler)
manage.DELETE("/remove/:projuserid", deps.Project.RemoveProjectUserHandler)
manage.POST("/create/table/:projectid", deps.DynamicDB.CreateDynamicDBHandler)
manage.POST("/create/col/:projectid/:tableid", deps.DynamicDB.AddDynamicColHandler)
}
}
}
Expand Down
77 changes: 74 additions & 3 deletions Internal/api/handlers/dynamicDBHandler.go
Original file line number Diff line number Diff line change
Expand Up @@ -25,24 +25,95 @@ func (dh *DynamicDBHandler) CreateDynamicDBHandler(c *gin.Context) {
})
return
}
var input services.CreateTableReq
if err := c.ShouldBindJSON(&input); err != nil {
var input services.DDLTableReq
if err = c.ShouldBindJSON(&input); err != nil {
c.JSON(400, gin.H{
"message": "Invalid json body",
"error": err.Error(),
})
return
}
if err := dh.Serv.CreateDynamicTable(uint(projectID), input); nil != err {
DynTable, err := dh.Serv.CreateDynamicTable(uint(projectID), input)
if nil != err {
c.JSON(500, gin.H{
"message": "Failed to create dynamic table",
"error": err.Error(),
})
return
}
if err = dh.Serv.CreateDynamicCols(DynTable.ID, uint(projectID), input); nil != err {
c.JSON(500, gin.H{
"message": "Failed to create dynamic cols",
"error": err.Error(),
})
return
}
c.JSON(200, gin.H{
"message": "Successfully created dynamic table",
"data": nil,
})
}

func (dh *DynamicDBHandler) AddDynamicColHandler(c *gin.Context) {
projectIDStr := c.Param("projectid")
projectID, err := strconv.Atoi(projectIDStr)
if err != nil {
c.JSON(400, gin.H{
"message": "Project ID not valid",
"error": err.Error(),
})
return
}
tableIDStr := c.Param("tableid")
tableID, err := strconv.Atoi(tableIDStr)
if err != nil {
c.JSON(400, gin.H{
"message": "Table ID not valid",
"error": err.Error(),
})
return
}
var input services.DDLTableReq
if err = c.ShouldBindJSON(&input); err != nil {
c.JSON(400, gin.H{
"message": "Invalid json body",
"error": err.Error(),
})
return
}
if err = dh.Serv.CreateDynamicCols(uint(tableID), uint(projectID), input); nil != err {
c.JSON(500, gin.H{
"message": "Failed to create dynamic cols",
"error": err.Error(),
})
return
}
c.JSON(200, gin.H{
"message": "Successfully added dynamic col",
"data": nil,
})
}

func (dh *DynamicDBHandler) GetDynTablesHandler(c *gin.Context) {
projectIDStr := c.Param("projectid")
projectID, err := strconv.Atoi(projectIDStr)
if err != nil {
c.JSON(400, gin.H{
"message": "Project ID not valid",
"error": err.Error(),
})
return
}
tables, err := dh.Serv.GetDynamicTables(uint(projectID))
if err != nil {
c.JSON(500, gin.H{
"message": "Failed to get dynamic table",
"error": err.Error(),
})
return
}
c.JSON(200, gin.H{
"message": "Successfully get dynamic table",
"data": tables,
})
}
15 changes: 14 additions & 1 deletion Internal/core/domain/projectdb.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,4 +21,17 @@ type ProjectUser struct {
CreatedAt time.Time `gorm:"default:CURRENT_TIMESTAMP" json:"created_at"`
}

//Changes last savepoint
type DynTableDef struct {
ID uint `gorm:"primaryKey;autoIncrement" json:"id"`
ProjectID uint `gorm:"not null" json:"project_id"`
Name string `gorm:"size:255;not null" json:"name"`
Alias string `gorm:"size:255;not null" json:"alias"`
Columns []DynColDef `gorm:"foreignKey:DynTableDefID" json:"columns"`
}

type DynColDef struct {
ID uint `gorm:"primaryKey;autoIncrement" json:"id"`
DynTableDefID uint `gorm:"not null" json:"dyn_table_def_id"`
Name string `gorm:"size:255;not null" json:"name"`
DataType string `gorm:"size:255;not null" json:"data_type"`
}
91 changes: 80 additions & 11 deletions Internal/core/services/dynamicDBService.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,22 +6,24 @@ import (
"strings"

"github.com/Arjuna-Ragil/Localbase/Internal/adapters/repository"
"github.com/Arjuna-Ragil/Localbase/Internal/core/domain"
)

type DynamicDBService struct {
Repo *repository.DynamicDBRepo
DB *repository.DBContainer
}

func NewDynamicDBService(repo *repository.DynamicDBRepo) *DynamicDBService {
return &DynamicDBService{Repo: repo}
func NewDynamicDBService(repo *repository.DynamicDBRepo, db *repository.DBContainer) *DynamicDBService {
return &DynamicDBService{Repo: repo, DB: db}
}

type ColumnReq struct {
Name string `json:"name"`
Type string `json:"type"`
}

type CreateTableReq struct {
type DDLTableReq struct {
TableName string `json:"table_name"`
Columns []ColumnReq `json:"columns"`
}
Expand All @@ -34,26 +36,93 @@ func sanitizeInput(input string) string {
return result
}

func (ds *DynamicDBService) CreateDynamicTable(projectID uint, req CreateTableReq) error {
func (ds *DynamicDBService) CreateDynamicTable(projectID uint, req DDLTableReq) (*domain.DynTableDef, error) {
tx := ds.DB.Gorm.Begin()

defer func() {
if r := recover(); r != nil {
tx.Rollback()
}
}()

safeTableName := fmt.Sprintf("proj_%d_%s", projectID, sanitizeInput(req.TableName))

tableDef := domain.DynTableDef{
ProjectID: projectID,
Name: safeTableName,
Alias: req.TableName,
}

DynTable, err := ds.Repo.CreateDynTable(tx, &tableDef)
if err != nil {
tx.Rollback()
return nil, err
}

var queryBuilder strings.Builder
queryBuilder.WriteString(fmt.Sprintf("CREATE TABLE %s (", safeTableName))
queryBuilder.WriteString("id SERIAL PRIMARY KEY, ")
queryBuilder.WriteString("created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, ")

for _, col := range req.Columns {
safeColName := sanitizeInput(col.Name)
queryBuilder.WriteString(fmt.Sprintf("%s %s, ", safeColName, col.Type))
}

query := queryBuilder.String()
query = strings.TrimSuffix(query, ", ")
query += ");"

fmt.Println(query)
if err := ds.Repo.CreateDynamicDB(query); err != nil {
if err = ds.Repo.RunDynamicQuery(tx, query); err != nil {
tx.Rollback()
return nil, err
}
return DynTable, tx.Commit().Error
}

func (ds *DynamicDBService) CreateDynamicCols(tableID uint, projectID uint, req DDLTableReq) error {
tx := ds.DB.Gorm.Begin()

defer func() {
if r := recover(); r != nil {
tx.Rollback()
}
}()

safeTableName := fmt.Sprintf("proj_%d_%s", projectID, sanitizeInput(req.TableName))

if len(req.Columns) == 0 {
return fmt.Errorf("no columns specified")
}
var queryBuilder strings.Builder
queryBuilder.WriteString(fmt.Sprintf("ALTER TABLE %s ", safeTableName))

for i, col := range req.Columns {
safeColName := sanitizeInput(col.Name)
coldef := domain.DynColDef{
DynTableDefID: tableID,
Name: col.Name,
DataType: col.Type,
}
if err := ds.Repo.CreateDynCol(tx, &coldef); err != nil {
tx.Rollback()
return err
}

queryBuilder.WriteString(fmt.Sprintf("ADD COLUMN %s %s", safeColName, col.Type))
if i != len(req.Columns)-1 {
queryBuilder.WriteString(", ")
}
}
queryBuilder.WriteString(";")
query := queryBuilder.String()

if err := ds.Repo.RunDynamicQuery(tx, query); err != nil {
return err
}
return nil
return tx.Commit().Error
}

func (ds *DynamicDBService) GetDynamicTables(projectID uint) ([]domain.DynTableDef, error) {
tables, err := ds.Repo.GetTables(projectID)
if err != nil {
return nil, err
}
return tables, nil
}
16 changes: 14 additions & 2 deletions Lb-web/src/features/project/components/ProjectSidebar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,10 @@ interface ProjectSidebarProps {
export function ProjectSidebar({ project }: ProjectSidebarProps) {
const location = useLocation();

const isActive = (path: string) => {
const isActive = (path: string, exact = false) => {
if (exact) {
return location.pathname === path;
}
return location.pathname === path || location.pathname.startsWith(`${path}/`);
};

Expand All @@ -38,12 +41,21 @@ export function ProjectSidebar({ project }: ProjectSidebarProps) {
<Link to={`/project/${project.id}`}>
<Button
variant="ghost"
className={`w-full justify-start ${isActive(`/project/${project.id}`) && !location.pathname.includes('settings') ? 'bg-sky-100 text-sky-900' : 'text-sky-700 hover:text-sky-900 hover:bg-sky-50'}`}
className={`w-full justify-start ${isActive(`/project/${project.id}`, true) ? 'bg-sky-100 text-sky-900' : 'text-sky-700 hover:text-sky-900 hover:bg-sky-50'}`}
>
<LayoutDashboard className="mr-3 h-4 w-4" />
Dashboard
</Button>
</Link>
<Link to={`/project/${project.id}/database`}>
<Button
variant="ghost"
className={`w-full justify-start ${isActive(`/project/${project.id}/database`) ? 'bg-sky-100 text-sky-900' : 'text-sky-700 hover:text-sky-900 hover:bg-sky-50'}`}
>
<Database className="mr-3 h-4 w-4" />
Database
</Button>
</Link>
<Link to={`/project/${project.id}/settings/members`}>
<Button
variant="ghost"
Expand Down
Loading
Loading