From 92d50eabf3c852d686c9ae5ba6048b61d90e10e0 Mon Sep 17 00:00:00 2001 From: Steven Date: Tue, 20 Jun 2023 18:02:53 +0800 Subject: [PATCH] feat: migrate api to v1 package --- api/api.go | 21 -- api/auth.go | 12 - api/shortcut.go | 91 ------ api/user.go | 46 --- api/user_setting.go | 69 ---- api/v1/shortcut.go | 227 +++++++++++++ api/v1/v1.go | 1 + api/v1/workspace.go | 2 +- api/v1/workspace_user.go | 4 +- server/common.go | 10 - server/server.go | 8 +- server/shortcut.go | 186 ----------- store/db/migration/dev/LATEST__SCHEMA.sql | 3 +- store/db/migration/prod/LATEST__SCHEMA.sql | 3 +- store/shortcut.go | 355 +-------------------- store/store.go | 1 - store/workspace_user.go | 4 +- 17 files changed, 244 insertions(+), 799 deletions(-) delete mode 100644 api/api.go delete mode 100644 api/auth.go delete mode 100644 api/shortcut.go delete mode 100644 api/user.go delete mode 100644 api/user_setting.go create mode 100644 api/v1/shortcut.go delete mode 100644 server/shortcut.go diff --git a/api/api.go b/api/api.go deleted file mode 100644 index c2b5f68..0000000 --- a/api/api.go +++ /dev/null @@ -1,21 +0,0 @@ -package api - -// RowStatus is the status for a row. -type RowStatus string - -const ( - // Normal is the status for a normal row. - Normal RowStatus = "NORMAL" - // Archived is the status for an archived row. - Archived RowStatus = "ARCHIVED" -) - -func (e RowStatus) String() string { - switch e { - case Normal: - return "NORMAL" - case Archived: - return "ARCHIVED" - } - return "" -} diff --git a/api/auth.go b/api/auth.go deleted file mode 100644 index 077865a..0000000 --- a/api/auth.go +++ /dev/null @@ -1,12 +0,0 @@ -package api - -type Signup struct { - Email string `json:"email"` - DisplayName string `json:"displayName"` - Password string `json:"password"` -} - -type Signin struct { - Email string `json:"email"` - Password string `json:"password"` -} diff --git a/api/shortcut.go b/api/shortcut.go deleted file mode 100644 index 49cfa7a..0000000 --- a/api/shortcut.go +++ /dev/null @@ -1,91 +0,0 @@ -package api - -// Visibility is the type of a visibility. -type Visibility string - -const ( - // VisibilityPublic is the PUBLIC visibility. - VisibilityPublic Visibility = "PUBLIC" - // VisibilityWorkspace is the WORKSPACE visibility. - VisibilityWorkspace Visibility = "WORKSPACE" - // VisibilityPrivite is the PRIVATE visibility. - VisibilityPrivite Visibility = "PRIVATE" -) - -func (e Visibility) String() string { - switch e { - case VisibilityPublic: - return "PUBLIC" - case VisibilityWorkspace: - return "WORKSPACE" - case VisibilityPrivite: - return "PRIVATE" - } - return "PRIVATE" -} - -type Shortcut struct { - ID int `json:"id"` - - // Standard fields - CreatorID int `json:"creatorId"` - Creator *User `json:"creator"` - CreatedTs int64 `json:"createdTs"` - UpdatedTs int64 `json:"updatedTs"` - WorkspaceID int `json:"workspaceId"` - RowStatus RowStatus `json:"rowStatus"` - - // Domain specific fields - Name string `json:"name"` - Link string `json:"link"` - Description string `json:"description"` - Visibility Visibility `json:"visibility"` -} - -type ShortcutCreate struct { - // Standard fields - CreatorID int - WorkspaceID int `json:"workspaceId"` - - // Domain specific fields - Name string `json:"name"` - Link string `json:"link"` - Description string `json:"description"` - Visibility Visibility `json:"visibility"` -} - -type ShortcutPatch struct { - ID int - - // Standard fields - RowStatus *RowStatus `json:"rowStatus"` - - // Domain specific fields - Name *string `json:"name"` - Link *string `json:"link"` - Description *string `json:"description"` - Visibility *Visibility `json:"visibility"` -} - -type ShortcutFind struct { - ID *int - - // Standard fields - CreatorID *int `json:"creatorId"` - WorkspaceID *int `json:"workspaceId"` - - // Domain specific fields - Name *string - Link *string - Description *string - MemberID *int - VisibilityList []Visibility -} - -type ShortcutDelete struct { - ID *int - - // Standard fields - CreatorID *int - WorkspaceID *int -} diff --git a/api/user.go b/api/user.go deleted file mode 100644 index a7aea12..0000000 --- a/api/user.go +++ /dev/null @@ -1,46 +0,0 @@ -package api - -// Role is the type of a role. -type Role string - -const ( - // RoleAdmin is the ADMIN role. - RoleAdmin Role = "ADMIN" - // RoleUser is the USER role. - RoleUser Role = "USER" -) - -func (e Role) String() string { - switch e { - case RoleAdmin: - return "ADMIN" - case RoleUser: - return "USER" - } - return "USER" -} - -type User struct { - ID int `json:"id"` - - // Standard fields - CreatedTs int64 `json:"createdTs"` - UpdatedTs int64 `json:"updatedTs"` - RowStatus RowStatus `json:"rowStatus"` - - // Domain specific fields - Email string `json:"email"` - DisplayName string `json:"displayName"` - PasswordHash string `json:"-"` - OpenID string `json:"openId"` - Role Role `json:"role"` -} - -type UserCreate struct { - Email string `json:"email"` - DisplayName string `json:"displayName"` - Password string `json:"password"` - PasswordHash string `json:"-"` - OpenID string `json:"-"` - Role Role `json:"-"` -} diff --git a/api/user_setting.go b/api/user_setting.go deleted file mode 100644 index 294a847..0000000 --- a/api/user_setting.go +++ /dev/null @@ -1,69 +0,0 @@ -package api - -import ( - "encoding/json" - "fmt" -) - -type UserSettingKey string - -const ( - // UserSettingLocaleKey is the key type for user locale. - UserSettingLocaleKey UserSettingKey = "locale" -) - -// String returns the string format of UserSettingKey type. -func (key UserSettingKey) String() string { - if key == UserSettingLocaleKey { - return "locale" - } - return "" -} - -var ( - UserSettingLocaleValue = []string{"en", "zh"} -) - -type UserSetting struct { - UserID int - Key UserSettingKey `json:"key"` - // Value is a JSON string with basic value - Value string `json:"value"` -} - -type UserSettingUpsert struct { - UserID int - Key UserSettingKey `json:"key"` - Value string `json:"value"` -} - -func (upsert UserSettingUpsert) Validate() error { - if upsert.Key == UserSettingLocaleKey { - localeValue := "en" - err := json.Unmarshal([]byte(upsert.Value), &localeValue) - if err != nil { - return fmt.Errorf("failed to unmarshal user setting locale value") - } - - invalid := true - for _, value := range UserSettingLocaleValue { - if localeValue == value { - invalid = false - break - } - } - if invalid { - return fmt.Errorf("invalid user setting locale value") - } - } else { - return fmt.Errorf("invalid user setting key") - } - - return nil -} - -type UserSettingFind struct { - UserID int - - Key *UserSettingKey `json:"key"` -} diff --git a/api/v1/shortcut.go b/api/v1/shortcut.go new file mode 100644 index 0000000..af73de7 --- /dev/null +++ b/api/v1/shortcut.go @@ -0,0 +1,227 @@ +package v1 + +import ( + "encoding/json" + "fmt" + "net/http" + "strconv" + + "github.com/boojack/shortify/store" + + "github.com/labstack/echo/v4" +) + +// Visibility is the type of a visibility. +type Visibility string + +const ( + // VisibilityPublic is the PUBLIC visibility. + VisibilityPublic Visibility = "PUBLIC" + // VisibilityWorkspace is the WORKSPACE visibility. + VisibilityWorkspace Visibility = "WORKSPACE" + // VisibilityPrivate is the PRIVATE visibility. + VisibilityPrivate Visibility = "PRIVATE" +) + +func (e Visibility) String() string { + switch e { + case VisibilityPublic: + return "PUBLIC" + case VisibilityWorkspace: + return "WORKSPACE" + case VisibilityPrivate: + return "PRIVATE" + } + return "PRIVATE" +} + +type Shortcut struct { + ID int `json:"id"` + + // Standard fields + CreatorID int `json:"creatorId"` + Creator *User `json:"creator"` + CreatedTs int64 `json:"createdTs"` + UpdatedTs int64 `json:"updatedTs"` + WorkspaceID int `json:"workspaceId"` + RowStatus RowStatus `json:"rowStatus"` + + // Domain specific fields + Name string `json:"name"` + Link string `json:"link"` + Description string `json:"description"` + Visibility Visibility `json:"visibility"` +} + +type CreateShortcutRequest struct { + WorkspaceID int `json:"workspaceId"` + Name string `json:"name"` + Link string `json:"link"` + Description string `json:"description"` + Visibility Visibility `json:"visibility"` +} + +type PatchShortcutRequest struct { + RowStatus *RowStatus `json:"rowStatus"` + Name *string `json:"name"` + Link *string `json:"link"` + Description *string `json:"description"` + Visibility *Visibility `json:"visibility"` +} + +func (s *APIV1Service) registerShortcutRoutes(g *echo.Group) { + g.POST("/shortcut", func(c echo.Context) error { + ctx := c.Request().Context() + userID, ok := c.Get(getUserIDContextKey()).(int) + if !ok { + return echo.NewHTTPError(http.StatusUnauthorized, "Missing user in session") + } + create := &CreateShortcutRequest{} + if err := json.NewDecoder(c.Request().Body).Decode(create); err != nil { + return echo.NewHTTPError(http.StatusBadRequest, "Malformatted post shortcut request").SetInternal(err) + } + + shortcut, err := s.Store.CreateShortcut(ctx, &store.Shortcut{ + CreatorID: userID, + WorkspaceID: create.WorkspaceID, + Name: create.Name, + Link: create.Link, + Description: create.Description, + Visibility: convertVisibilityToStore(create.Visibility), + }) + if err != nil { + return echo.NewHTTPError(http.StatusInternalServerError, "Failed to create shortcut").SetInternal(err) + } + + return c.JSON(http.StatusOK, shortcut) + }) + + g.PATCH("/shortcut/:shortcutId", func(c echo.Context) error { + ctx := c.Request().Context() + shortcutID, err := strconv.Atoi(c.Param("shortcutId")) + if err != nil { + return echo.NewHTTPError(http.StatusBadRequest, fmt.Sprintf("ID is not a number: %s", c.Param("shortcutId"))).SetInternal(err) + } + userID, ok := c.Get(getUserIDContextKey()).(int) + if !ok { + return echo.NewHTTPError(http.StatusUnauthorized, "Missing user in session") + } + + shortcut, err := s.Store.GetShortcut(ctx, &store.FindShortcut{ + ID: &shortcutID, + }) + if err != nil { + return echo.NewHTTPError(http.StatusInternalServerError, "Failed to find shortcut").SetInternal(err) + } + + workspaceUser, err := s.Store.GetWorkspaceUser(ctx, &store.FindWorkspaceUser{ + UserID: &userID, + WorkspaceID: &shortcut.WorkspaceID, + }) + if err != nil { + return echo.NewHTTPError(http.StatusInternalServerError, "Failed to find workspace user").SetInternal(err) + } + + if shortcut.CreatorID != userID && workspaceUser.Role != store.RoleAdmin { + return echo.NewHTTPError(http.StatusForbidden, "Forbidden to patch shortcut") + } + + patch := &PatchShortcutRequest{} + if err := json.NewDecoder(c.Request().Body).Decode(patch); err != nil { + return echo.NewHTTPError(http.StatusBadRequest, "Malformatted patch shortcut request").SetInternal(err) + } + + shortcut, err = s.Store.UpdateShortcut(ctx, &store.UpdateShortcut{ + ID: shortcutID, + RowStatus: (*store.RowStatus)(patch.RowStatus), + Name: patch.Name, + Link: patch.Link, + Visibility: (*store.Visibility)(patch.Visibility), + }) + if err != nil { + return echo.NewHTTPError(http.StatusInternalServerError, "Failed to patch shortcut").SetInternal(err) + } + + return c.JSON(http.StatusOK, shortcut) + }) + + g.GET("/shortcut", func(c echo.Context) error { + ctx := c.Request().Context() + userID, ok := c.Get(getUserIDContextKey()).(int) + if !ok { + return echo.NewHTTPError(http.StatusUnauthorized, "Missing user in session") + } + + find := &store.FindShortcut{} + if workspaceID, err := strconv.Atoi(c.QueryParam("workspaceId")); err == nil { + find.WorkspaceID = &workspaceID + } + if name := c.QueryParam("name"); name != "" { + find.Name = &name + } + + list := []*store.Shortcut{} + find.VisibilityList = []store.Visibility{store.VisibilityWorkspace, store.VisibilityPublic} + visibleShortcutList, err := s.Store.ListShortcuts(ctx, find) + if err != nil { + return echo.NewHTTPError(http.StatusInternalServerError, "Failed to fetch shortcut list").SetInternal(err) + } + list = append(list, visibleShortcutList...) + + find.VisibilityList = []store.Visibility{store.VisibilityPrivate} + find.CreatorID = &userID + privateShortcutList, err := s.Store.ListShortcuts(ctx, find) + if err != nil { + return echo.NewHTTPError(http.StatusInternalServerError, "Failed to fetch private shortcut list").SetInternal(err) + } + list = append(list, privateShortcutList...) + + return c.JSON(http.StatusOK, list) + }) + + g.GET("/shortcut/:id", func(c echo.Context) error { + ctx := c.Request().Context() + shortcutID, err := strconv.Atoi(c.Param("id")) + if err != nil { + return echo.NewHTTPError(http.StatusBadRequest, fmt.Sprintf("ID is not a number: %s", c.Param("id"))).SetInternal(err) + } + + shortcut, err := s.Store.GetShortcut(ctx, &store.FindShortcut{ + ID: &shortcutID, + }) + if err != nil { + return echo.NewHTTPError(http.StatusInternalServerError, fmt.Sprintf("Failed to fetch shortcut by ID %d", shortcutID)).SetInternal(err) + } + + return c.JSON(http.StatusOK, shortcut) + }) + + g.DELETE("/shortcut/:id", func(c echo.Context) error { + ctx := c.Request().Context() + shortcutID, err := strconv.Atoi(c.Param("id")) + if err != nil { + return echo.NewHTTPError(http.StatusBadRequest, fmt.Sprintf("ID is not a number: %s", c.Param("id"))).SetInternal(err) + } + + if err := s.Store.DeleteShortcut(ctx, &store.DeleteShortcut{ + ID: shortcutID, + }); err != nil { + return echo.NewHTTPError(http.StatusInternalServerError, "Failed to delete shortcut").SetInternal(err) + } + + return c.JSON(http.StatusOK, true) + }) +} + +func convertVisibilityToStore(visibility Visibility) store.Visibility { + switch visibility { + case VisibilityPrivate: + return store.VisibilityPrivate + case VisibilityWorkspace: + return store.VisibilityWorkspace + case VisibilityPublic: + return store.VisibilityPublic + default: + return store.VisibilityPrivate + } +} diff --git a/api/v1/v1.go b/api/v1/v1.go index 3b48b65..d172015 100644 --- a/api/v1/v1.go +++ b/api/v1/v1.go @@ -25,4 +25,5 @@ func (s *APIV1Service) Start(apiV1Group *echo.Group, secret string) { s.registerUserRoutes(apiV1Group) s.registerWorkspaceRoutes(apiV1Group) s.registerWorkspaceUserRoutes(apiV1Group) + s.registerShortcutRoutes(apiV1Group) } diff --git a/api/v1/workspace.go b/api/v1/workspace.go index 9e5662b..db5b42c 100644 --- a/api/v1/workspace.go +++ b/api/v1/workspace.go @@ -81,7 +81,7 @@ func (s *APIV1Service) registerWorkspaceRoutes(g *echo.Group) { return echo.NewHTTPError(http.StatusInternalServerError, "Failed to create workspace").SetInternal(err) } - _, err = s.Store.UpsertWorkspaceUserV1(ctx, &store.WorkspaceUser{ + _, err = s.Store.UpsertWorkspaceUser(ctx, &store.WorkspaceUser{ WorkspaceID: workspace.ID, UserID: userID, Role: store.RoleAdmin, diff --git a/api/v1/workspace_user.go b/api/v1/workspace_user.go index 2569046..9785213 100644 --- a/api/v1/workspace_user.go +++ b/api/v1/workspace_user.go @@ -59,7 +59,7 @@ func (s *APIV1Service) registerWorkspaceUserRoutes(g *echo.Group) { return echo.NewHTTPError(http.StatusBadRequest, "Malformatted post workspace user request").SetInternal(err) } - workspaceUser, err := s.Store.UpsertWorkspaceUserV1(ctx, &store.WorkspaceUser{ + workspaceUser, err := s.Store.UpsertWorkspaceUser(ctx, &store.WorkspaceUser{ WorkspaceID: upsert.WorkspaceID, UserID: upsert.UserID, Role: convertRoleToStore(upsert.Role), @@ -157,7 +157,7 @@ func (s *APIV1Service) registerWorkspaceUserRoutes(g *echo.Group) { return echo.NewHTTPError(http.StatusForbidden, "Access forbidden to delete workspace user").SetInternal(err) } - if err := s.Store.DeleteWorkspaceUserV1(ctx, &store.DeleteWorkspaceUser{ + if err := s.Store.DeleteWorkspaceUser(ctx, &store.DeleteWorkspaceUser{ WorkspaceID: workspaceID, UserID: userID, }); err != nil { diff --git a/server/common.go b/server/common.go index cc41560..1ee22d1 100644 --- a/server/common.go +++ b/server/common.go @@ -6,16 +6,6 @@ import ( "github.com/labstack/echo/v4" ) -func composeResponse(data any) any { - type R struct { - Data any `json:"data"` - } - - return R{ - Data: data, - } -} - // hasPrefixes returns true if the string s has any of the given prefixes. func hasPrefixes(src string, prefixes ...string) bool { for _, prefix := range prefixes { diff --git a/server/server.go b/server/server.go index ce15301..d2dce2a 100644 --- a/server/server.go +++ b/server/server.go @@ -60,15 +60,9 @@ func NewServer(profile *profile.Profile, store *store.Store) (*Server, error) { } e.Use(session.Middleware(sessions.NewCookieStore([]byte(secret)))) - apiGroup := e.Group("/api") - apiGroup.Use(func(next echo.HandlerFunc) echo.HandlerFunc { - return JWTMiddleware(s, next, string(secret)) - }) - s.registerShortcutRoutes(apiGroup) - // Register API v1 routes. apiV1Service := apiv1.NewAPIV1Service(profile, store) - apiV1Group := apiGroup.Group("/api/v1") + apiV1Group := e.Group("/api/v1") apiV1Group.Use(func(next echo.HandlerFunc) echo.HandlerFunc { return JWTMiddleware(s, next, string(secret)) }) diff --git a/server/shortcut.go b/server/shortcut.go deleted file mode 100644 index eec36cb..0000000 --- a/server/shortcut.go +++ /dev/null @@ -1,186 +0,0 @@ -package server - -import ( - "encoding/json" - "fmt" - "net/http" - "strconv" - - "github.com/boojack/shortify/api" - "github.com/boojack/shortify/store" - - "github.com/labstack/echo/v4" -) - -func (s *Server) registerShortcutRoutes(g *echo.Group) { - g.POST("/shortcut", func(c echo.Context) error { - ctx := c.Request().Context() - userID, ok := c.Get(getUserIDContextKey()).(int) - if !ok { - return echo.NewHTTPError(http.StatusUnauthorized, "Missing user in session") - } - shortcutCreate := &api.ShortcutCreate{ - CreatorID: userID, - } - if err := json.NewDecoder(c.Request().Body).Decode(shortcutCreate); err != nil { - return echo.NewHTTPError(http.StatusBadRequest, "Malformatted post shortcut request").SetInternal(err) - } - - existingShortcut, err := s.Store.FindShortcut(ctx, &api.ShortcutFind{ - Name: &shortcutCreate.Name, - WorkspaceID: &shortcutCreate.WorkspaceID, - }) - if err != nil { - return echo.NewHTTPError(http.StatusInternalServerError, "Failed to find shortcut").SetInternal(err) - } - if existingShortcut != nil { - return echo.NewHTTPError(http.StatusBadRequest, fmt.Sprintf("Shortcut with name %s already exists", shortcutCreate.Name)) - } - - shortcut, err := s.Store.CreateShortcut(ctx, shortcutCreate) - if err != nil { - return echo.NewHTTPError(http.StatusInternalServerError, "Failed to create shortcut").SetInternal(err) - } - - if err := s.Store.ComposeShortcut(ctx, shortcut); err != nil { - return echo.NewHTTPError(http.StatusInternalServerError, "Failed to compose shortcut").SetInternal(err) - } - - return c.JSON(http.StatusOK, composeResponse(shortcut)) - }) - - g.PATCH("/shortcut/:shortcutId", func(c echo.Context) error { - ctx := c.Request().Context() - shortcutID, err := strconv.Atoi(c.Param("shortcutId")) - if err != nil { - return echo.NewHTTPError(http.StatusBadRequest, fmt.Sprintf("ID is not a number: %s", c.Param("shortcutId"))).SetInternal(err) - } - userID, ok := c.Get(getUserIDContextKey()).(int) - if !ok { - return echo.NewHTTPError(http.StatusUnauthorized, "Missing user in session") - } - - shortcut, err := s.Store.FindShortcut(ctx, &api.ShortcutFind{ - ID: &shortcutID, - }) - if err != nil { - return echo.NewHTTPError(http.StatusInternalServerError, "Failed to find shortcut").SetInternal(err) - } - - workspaceUser, err := s.Store.GetWorkspaceUser(ctx, &store.FindWorkspaceUser{ - UserID: &userID, - WorkspaceID: &shortcut.WorkspaceID, - }) - if err != nil { - return echo.NewHTTPError(http.StatusInternalServerError, "Failed to find workspace user").SetInternal(err) - } - - if shortcut.CreatorID != userID && workspaceUser.Role != store.RoleAdmin { - return echo.NewHTTPError(http.StatusForbidden, "Forbidden to patch shortcut") - } - - shortcutPatch := &api.ShortcutPatch{ - ID: shortcutID, - } - if err := json.NewDecoder(c.Request().Body).Decode(shortcutPatch); err != nil { - return echo.NewHTTPError(http.StatusBadRequest, "Malformatted patch shortcut request").SetInternal(err) - } - - shortcut, err = s.Store.PatchShortcut(ctx, shortcutPatch) - if err != nil { - return echo.NewHTTPError(http.StatusInternalServerError, "Failed to patch shortcut").SetInternal(err) - } - - if err := s.Store.ComposeShortcut(ctx, shortcut); err != nil { - return echo.NewHTTPError(http.StatusInternalServerError, "Failed to compose shortcut").SetInternal(err) - } - - return c.JSON(http.StatusOK, composeResponse(shortcut)) - }) - - g.GET("/shortcut", func(c echo.Context) error { - ctx := c.Request().Context() - userID, ok := c.Get(getUserIDContextKey()).(int) - if !ok { - return echo.NewHTTPError(http.StatusUnauthorized, "Missing user in session") - } - - shortcutFind := &api.ShortcutFind{} - if workspaceID, err := strconv.Atoi(c.QueryParam("workspaceId")); err == nil { - shortcutFind.WorkspaceID = &workspaceID - } - if name := c.QueryParam("name"); name != "" { - shortcutFind.Name = &name - } - if link := c.QueryParam("link"); link != "" { - shortcutFind.Link = &link - } - - list := []*api.Shortcut{} - - if shortcutFind.WorkspaceID == nil { - shortcutFind.MemberID = &userID - } - shortcutFind.VisibilityList = []api.Visibility{api.VisibilityWorkspace, api.VisibilityPublic} - visibleShortcutList, err := s.Store.FindShortcutList(ctx, shortcutFind) - if err != nil { - return echo.NewHTTPError(http.StatusInternalServerError, "Failed to fetch shortcut list").SetInternal(err) - } - list = append(list, visibleShortcutList...) - - shortcutFind.VisibilityList = []api.Visibility{api.VisibilityPrivite} - shortcutFind.CreatorID = &userID - privateShortcutList, err := s.Store.FindShortcutList(ctx, shortcutFind) - if err != nil { - return echo.NewHTTPError(http.StatusInternalServerError, "Failed to fetch private shortcut list").SetInternal(err) - } - list = append(list, privateShortcutList...) - - for _, shortcut := range list { - if err := s.Store.ComposeShortcut(ctx, shortcut); err != nil { - return echo.NewHTTPError(http.StatusInternalServerError, "Failed to compose shortcut").SetInternal(err) - } - } - - return c.JSON(http.StatusOK, composeResponse(list)) - }) - - g.GET("/shortcut/:id", func(c echo.Context) error { - ctx := c.Request().Context() - shortcutID, err := strconv.Atoi(c.Param("id")) - if err != nil { - return echo.NewHTTPError(http.StatusBadRequest, fmt.Sprintf("ID is not a number: %s", c.Param("id"))).SetInternal(err) - } - - shortcutFind := &api.ShortcutFind{ - ID: &shortcutID, - } - shortcut, err := s.Store.FindShortcut(ctx, shortcutFind) - if err != nil { - return echo.NewHTTPError(http.StatusInternalServerError, fmt.Sprintf("Failed to fetch shortcut by ID %d", *shortcutFind.ID)).SetInternal(err) - } - - if err := s.Store.ComposeShortcut(ctx, shortcut); err != nil { - return echo.NewHTTPError(http.StatusInternalServerError, "Failed to compose shortcut").SetInternal(err) - } - - return c.JSON(http.StatusOK, composeResponse(shortcut)) - }) - - g.DELETE("/shortcut/:id", func(c echo.Context) error { - ctx := c.Request().Context() - shortcutID, err := strconv.Atoi(c.Param("id")) - if err != nil { - return echo.NewHTTPError(http.StatusBadRequest, fmt.Sprintf("ID is not a number: %s", c.Param("id"))).SetInternal(err) - } - - shortcutDelete := &api.ShortcutDelete{ - ID: &shortcutID, - } - if err := s.Store.DeleteShortcut(ctx, shortcutDelete); err != nil { - return echo.NewHTTPError(http.StatusInternalServerError, "Failed to delete shortcut").SetInternal(err) - } - - return c.JSON(http.StatusOK, true) - }) -} diff --git a/store/db/migration/dev/LATEST__SCHEMA.sql b/store/db/migration/dev/LATEST__SCHEMA.sql index da0e1c5..5eec13e 100644 --- a/store/db/migration/dev/LATEST__SCHEMA.sql +++ b/store/db/migration/dev/LATEST__SCHEMA.sql @@ -73,7 +73,8 @@ CREATE TABLE shortcut ( name TEXT NOT NULL, link TEXT NOT NULL, description TEXT NOT NULL DEFAULT '', - visibility TEXT NOT NULL CHECK (visibility IN ('PRIVATE', 'WORKSPACE', 'PUBLIC')) DEFAULT 'PRIVATE' + visibility TEXT NOT NULL CHECK (visibility IN ('PRIVATE', 'WORKSPACE', 'PUBLIC')) DEFAULT 'PRIVATE', + UNIQUE(workspace_id, name) ); INSERT INTO diff --git a/store/db/migration/prod/LATEST__SCHEMA.sql b/store/db/migration/prod/LATEST__SCHEMA.sql index da0e1c5..5eec13e 100644 --- a/store/db/migration/prod/LATEST__SCHEMA.sql +++ b/store/db/migration/prod/LATEST__SCHEMA.sql @@ -73,7 +73,8 @@ CREATE TABLE shortcut ( name TEXT NOT NULL, link TEXT NOT NULL, description TEXT NOT NULL DEFAULT '', - visibility TEXT NOT NULL CHECK (visibility IN ('PRIVATE', 'WORKSPACE', 'PUBLIC')) DEFAULT 'PRIVATE' + visibility TEXT NOT NULL CHECK (visibility IN ('PRIVATE', 'WORKSPACE', 'PUBLIC')) DEFAULT 'PRIVATE', + UNIQUE(workspace_id, name) ); INSERT INTO diff --git a/store/shortcut.go b/store/shortcut.go index c1715e8..db04c48 100644 --- a/store/shortcut.go +++ b/store/shortcut.go @@ -5,9 +5,6 @@ import ( "database/sql" "fmt" "strings" - - "github.com/boojack/shortify/api" - "github.com/boojack/shortify/internal/errorutil" ) // Visibility is the type of a visibility. @@ -18,8 +15,8 @@ const ( VisibilityPublic Visibility = "PUBLIC" // VisibilityWorkspace is the WORKSPACE visibility. VisibilityWorkspace Visibility = "WORKSPACE" - // VisibilityPrivite is the PRIVATE visibility. - VisibilityPrivite Visibility = "PRIVATE" + // VisibilityPrivate is the PRIVATE visibility. + VisibilityPrivate Visibility = "PRIVATE" ) func (e Visibility) String() string { @@ -28,7 +25,7 @@ func (e Visibility) String() string { return "PUBLIC" case VisibilityWorkspace: return "WORKSPACE" - case VisibilityPrivite: + case VisibilityPrivate: return "PRIVATE" } return "PRIVATE" @@ -74,7 +71,7 @@ type DeleteShortcut struct { ID int } -func (s *Store) CreateShortcutV1(ctx context.Context, create *Shortcut) (*Shortcut, error) { +func (s *Store) CreateShortcut(ctx context.Context, create *Shortcut) (*Shortcut, error) { tx, err := s.db.BeginTx(ctx, nil) if err != nil { return nil, err @@ -108,7 +105,7 @@ func (s *Store) CreateShortcutV1(ctx context.Context, create *Shortcut) (*Shortc return create, nil } -func (s *Store) UpdateShortcutV1(ctx context.Context, update *UpdateShortcut) (*Shortcut, error) { +func (s *Store) UpdateShortcut(ctx context.Context, update *UpdateShortcut) (*Shortcut, error) { tx, err := s.db.BeginTx(ctx, nil) if err != nil { return nil, err @@ -209,7 +206,7 @@ func (s *Store) GetShortcut(ctx context.Context, find *FindShortcut) (*Shortcut, return shortcuts[0], nil } -func (s *Store) DeleteShortcutV1(ctx context.Context, delete *DeleteShortcut) error { +func (s *Store) DeleteShortcut(ctx context.Context, delete *DeleteShortcut) error { tx, err := s.db.BeginTx(ctx, nil) if err != nil { return err @@ -306,343 +303,3 @@ func listShortcuts(ctx context.Context, tx *sql.Tx, find *FindShortcut) ([]*Shor return list, nil } - -// shortcutRaw is the store model for an Shortcut. -// Fields have exactly the same meanings as Shortcut. -type shortcutRaw struct { - ID int - - // Standard fields - CreatorID int - CreatedTs int64 - UpdatedTs int64 - WorkspaceID int - RowStatus api.RowStatus - - // Domain specific fields - Name string - Link string - Description string - Visibility api.Visibility -} - -func (raw *shortcutRaw) toShortcut() *api.Shortcut { - return &api.Shortcut{ - ID: raw.ID, - - CreatorID: raw.CreatorID, - CreatedTs: raw.CreatedTs, - UpdatedTs: raw.UpdatedTs, - WorkspaceID: raw.WorkspaceID, - RowStatus: raw.RowStatus, - - Name: raw.Name, - Link: raw.Link, - Description: raw.Description, - Visibility: raw.Visibility, - } -} - -func (*Store) ComposeShortcut(_ context.Context, _ *api.Shortcut) error { - // TODO: implement this. - return nil -} - -func (s *Store) CreateShortcut(ctx context.Context, create *api.ShortcutCreate) (*api.Shortcut, error) { - tx, err := s.db.BeginTx(ctx, nil) - if err != nil { - return nil, err - } - defer tx.Rollback() - - shortcutRaw, err := createShortcut(ctx, tx, create) - if err != nil { - return nil, err - } - - if err := tx.Commit(); err != nil { - return nil, err - } - - s.shortcutCache.Store(shortcutRaw.ID, shortcutRaw) - shortcut := shortcutRaw.toShortcut() - return shortcut, nil -} - -func (s *Store) PatchShortcut(ctx context.Context, patch *api.ShortcutPatch) (*api.Shortcut, error) { - tx, err := s.db.BeginTx(ctx, nil) - if err != nil { - return nil, err - } - defer tx.Rollback() - - shortcutRaw, err := patchShortcut(ctx, tx, patch) - if err != nil { - return nil, err - } - - if err := tx.Commit(); err != nil { - return nil, err - } - - s.shortcutCache.Store(shortcutRaw.ID, shortcutRaw) - shortcut := shortcutRaw.toShortcut() - return shortcut, nil -} - -func (s *Store) FindShortcutList(ctx context.Context, find *api.ShortcutFind) ([]*api.Shortcut, error) { - tx, err := s.db.BeginTx(ctx, nil) - if err != nil { - return nil, err - } - defer tx.Rollback() - - shortcutRawList, err := findShortcutList(ctx, tx, find) - if err != nil { - return nil, err - } - - list := []*api.Shortcut{} - for _, shortcutRaw := range shortcutRawList { - s.shortcutCache.Store(shortcutRaw.ID, shortcutRaw) - list = append(list, shortcutRaw.toShortcut()) - } - - return list, nil -} - -func (s *Store) FindShortcut(ctx context.Context, find *api.ShortcutFind) (*api.Shortcut, error) { - if find.ID != nil { - if cache, ok := s.shortcutCache.Load(*find.ID); ok { - return cache.(*shortcutRaw).toShortcut(), nil - } - } - - tx, err := s.db.BeginTx(ctx, nil) - if err != nil { - return nil, err - } - defer tx.Rollback() - - list, err := findShortcutList(ctx, tx, find) - if err != nil { - return nil, err - } - - if len(list) == 0 { - return nil, &errorutil.Error{Code: errorutil.NotFound, Err: fmt.Errorf("not found")} - } - - shortcutRaw := list[0] - s.shortcutCache.Store(shortcutRaw.ID, shortcutRaw) - shortcut := shortcutRaw.toShortcut() - return shortcut, nil -} - -func (s *Store) DeleteShortcut(ctx context.Context, delete *api.ShortcutDelete) error { - tx, err := s.db.BeginTx(ctx, nil) - if err != nil { - return err - } - defer tx.Rollback() - - err = deleteShortcut(ctx, tx, delete) - if err != nil { - return err - } - - if err := tx.Commit(); err != nil { - return err - } - - if delete.ID != nil { - s.shortcutCache.Delete(*delete.ID) - } - - return nil -} - -func createShortcut(ctx context.Context, tx *sql.Tx, create *api.ShortcutCreate) (*shortcutRaw, error) { - query := ` - INSERT INTO shortcut ( - creator_id, - workspace_id, - name, - link, - description, - visibility - ) - VALUES (?, ?, ?, ?, ?, ?) - RETURNING id, creator_id, created_ts, updated_ts, workspace_id, row_status, name, link, description, visibility - ` - var shortcutRaw shortcutRaw - if err := tx.QueryRowContext(ctx, query, create.CreatorID, create.WorkspaceID, create.Name, create.Link, create.Description, create.Visibility).Scan( - &shortcutRaw.ID, - &shortcutRaw.CreatorID, - &shortcutRaw.CreatedTs, - &shortcutRaw.UpdatedTs, - &shortcutRaw.WorkspaceID, - &shortcutRaw.RowStatus, - &shortcutRaw.Name, - &shortcutRaw.Link, - &shortcutRaw.Description, - &shortcutRaw.Visibility, - ); err != nil { - return nil, err - } - - return &shortcutRaw, nil -} - -func patchShortcut(ctx context.Context, tx *sql.Tx, patch *api.ShortcutPatch) (*shortcutRaw, error) { - set, args := []string{}, []any{} - - if v := patch.Name; v != nil { - set, args = append(set, "name = ?"), append(args, *v) - } - if v := patch.Link; v != nil { - set, args = append(set, "link = ?"), append(args, *v) - } - if v := patch.Description; v != nil { - set, args = append(set, "description = ?"), append(args, *v) - } - if v := patch.Visibility; v != nil { - set, args = append(set, "visibility = ?"), append(args, *v) - } - - args = append(args, patch.ID) - - query := ` - UPDATE shortcut - SET ` + strings.Join(set, ", ") + ` - WHERE id = ? - RETURNING id, creator_id, created_ts, updated_ts, workspace_id, row_status, name, link, description, visibility - ` - var shortcutRaw shortcutRaw - if err := tx.QueryRowContext(ctx, query, args...).Scan( - &shortcutRaw.ID, - &shortcutRaw.CreatorID, - &shortcutRaw.CreatedTs, - &shortcutRaw.UpdatedTs, - &shortcutRaw.WorkspaceID, - &shortcutRaw.RowStatus, - &shortcutRaw.Name, - &shortcutRaw.Link, - &shortcutRaw.Description, - &shortcutRaw.Visibility, - ); err != nil { - return nil, err - } - - return &shortcutRaw, nil -} - -func findShortcutList(ctx context.Context, tx *sql.Tx, find *api.ShortcutFind) ([]*shortcutRaw, error) { - where, args := []string{"1 = 1"}, []any{} - - if v := find.ID; v != nil { - where, args = append(where, "id = ?"), append(args, *v) - } - if v := find.CreatorID; v != nil { - where, args = append(where, "creator_id = ?"), append(args, *v) - } - if v := find.WorkspaceID; v != nil { - where, args = append(where, "workspace_id = ?"), append(args, *v) - } - if v := find.Name; v != nil { - where, args = append(where, "name = ?"), append(args, *v) - } - if v := find.Link; v != nil { - where, args = append(where, "link = ?"), append(args, *v) - } - if v := find.Description; v != nil { - where, args = append(where, "description = ?"), append(args, *v) - } - if v := find.VisibilityList; len(v) != 0 { - list := []string{} - for _, visibility := range v { - list = append(list, fmt.Sprintf("$%d", len(args)+1)) - args = append(args, visibility) - } - where = append(where, fmt.Sprintf("visibility in (%s)", strings.Join(list, ","))) - } - if v := find.MemberID; v != nil { - where, args = append(where, "workspace_id IN (SELECT workspace_id FROM workspace_user WHERE user_id = ?)"), append(args, *v) - } - - rows, err := tx.QueryContext(ctx, ` - SELECT - id, - creator_id, - created_ts, - updated_ts, - workspace_id, - row_status, - name, - link, - description, - visibility - FROM shortcut - WHERE `+strings.Join(where, " AND ")+` - ORDER BY created_ts DESC`, - args..., - ) - if err != nil { - return nil, err - } - defer rows.Close() - - shortcutRawList := make([]*shortcutRaw, 0) - for rows.Next() { - var shortcutRaw shortcutRaw - if err := rows.Scan( - &shortcutRaw.ID, - &shortcutRaw.CreatorID, - &shortcutRaw.CreatedTs, - &shortcutRaw.UpdatedTs, - &shortcutRaw.WorkspaceID, - &shortcutRaw.RowStatus, - &shortcutRaw.Name, - &shortcutRaw.Link, - &shortcutRaw.Description, - &shortcutRaw.Visibility, - ); err != nil { - return nil, err - } - - shortcutRawList = append(shortcutRawList, &shortcutRaw) - } - - if err := rows.Err(); err != nil { - return nil, err - } - - return shortcutRawList, nil -} - -func deleteShortcut(ctx context.Context, tx *sql.Tx, delete *api.ShortcutDelete) error { - where, args := []string{"1 = 1"}, []any{} - - if v := delete.ID; v != nil { - where, args = append(where, "id = ?"), append(args, *v) - } - if v := delete.CreatorID; v != nil { - where, args = append(where, "creator_id = ?"), append(args, *v) - } - if v := delete.WorkspaceID; v != nil { - where, args = append(where, "workspace_id = ?"), append(args, *v) - } - - result, err := tx.ExecContext(ctx, ` - DELETE FROM shortcut WHERE `+strings.Join(where, " AND "), args...) - if err != nil { - return err - } - - rows, _ := result.RowsAffected() - if rows == 0 { - return &errorutil.Error{Code: errorutil.NotFound, Err: fmt.Errorf("not found")} - } - - return nil -} diff --git a/store/store.go b/store/store.go index 936d79a..24fb9d8 100644 --- a/store/store.go +++ b/store/store.go @@ -14,7 +14,6 @@ type Store struct { userCache sync.Map // map[int]*userRaw workspaceCache sync.Map // map[int]*workspaceRaw - shortcutCache sync.Map // map[int]*shortcutRaw } // New creates a new instance of Store. diff --git a/store/workspace_user.go b/store/workspace_user.go index d9daddc..39f9fbc 100644 --- a/store/workspace_user.go +++ b/store/workspace_user.go @@ -33,7 +33,7 @@ type DeleteWorkspaceUser struct { UserID int } -func (s *Store) UpsertWorkspaceUserV1(ctx context.Context, upsert *WorkspaceUser) (*WorkspaceUser, error) { +func (s *Store) UpsertWorkspaceUser(ctx context.Context, upsert *WorkspaceUser) (*WorkspaceUser, error) { tx, err := s.db.BeginTx(ctx, nil) if err != nil { return nil, err @@ -103,7 +103,7 @@ func (s *Store) GetWorkspaceUser(ctx context.Context, find *FindWorkspaceUser) ( return workspaceUser, nil } -func (s *Store) DeleteWorkspaceUserV1(ctx context.Context, delete *DeleteWorkspaceUser) error { +func (s *Store) DeleteWorkspaceUser(ctx context.Context, delete *DeleteWorkspaceUser) error { tx, err := s.db.BeginTx(ctx, nil) if err != nil { return err