refactor: raw struct for store

This commit is contained in:
boojack 2022-05-19 18:32:04 +08:00
parent 0b50122aac
commit bc22f69ac5
22 changed files with 687 additions and 188 deletions

21
api/api.go Normal file
View File

@ -0,0 +1,21 @@
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 ""
}

View File

@ -4,13 +4,14 @@ type Memo struct {
ID int `json:"id"`
// Standard fields
CreatedTs int64 `json:"createdTs"`
UpdatedTs int64 `json:"updatedTs"`
RowStatus string `json:"rowStatus"`
RowStatus RowStatus `json:"rowStatus"`
CreatorID int `json:"creatorId"`
CreatedTs int64 `json:"createdTs"`
UpdatedTs int64 `json:"updatedTs"`
// Domain specific fields
Content string `json:"content"`
CreatorID int `json:"creatorId"`
Content string `json:"content"`
Pinned bool `json:"pinned"`
}
type MemoCreate struct {
@ -27,7 +28,7 @@ type MemoPatch struct {
ID int
// Standard fields
RowStatus *string `json:"rowStatus"`
RowStatus *RowStatus `json:"rowStatus"`
// Domain specific fields
Content *string `json:"content"`
@ -37,8 +38,11 @@ type MemoFind struct {
ID *int `json:"id"`
// Standard fields
CreatorID *int `json:"creatorId"`
RowStatus *string `json:"rowStatus"`
RowStatus *RowStatus `json:"rowStatus"`
CreatorID *int `json:"creatorId"`
// Domain specific fields
Pinned *bool
}
type MemoDelete struct {

21
api/memo_organizer.go Normal file
View File

@ -0,0 +1,21 @@
package api
type MemoOrganizer struct {
ID int
// Domain specific fields
MemoID int
UserID int
Pinned bool
}
type MemoOrganizerFind struct {
MemoID int
UserID int
}
type MemoOrganizerUpsert struct {
MemoID int
UserID int
Pinned bool `json:"pinned"`
}

View File

@ -4,10 +4,10 @@ type Shortcut struct {
ID int `json:"id"`
// Standard fields
CreatorID int
CreatedTs int64 `json:"createdTs"`
UpdatedTs int64 `json:"updatedTs"`
RowStatus string `json:"rowStatus"`
RowStatus RowStatus `json:"rowStatus"`
CreatorID int `json:"creatorId"`
CreatedTs int64 `json:"createdTs"`
UpdatedTs int64 `json:"updatedTs"`
// Domain specific fields
Title string `json:"title"`
@ -27,7 +27,7 @@ type ShortcutPatch struct {
ID int
// Standard fields
RowStatus *string `json:"rowStatus"`
RowStatus *RowStatus `json:"rowStatus"`
// Domain specific fields
Title *string `json:"title"`

View File

@ -10,12 +10,23 @@ const (
NormalUser Role = "USER"
)
func (e Role) String() string {
switch e {
case Owner:
return "OWNER"
case NormalUser:
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"`
CreatedTs int64 `json:"createdTs"`
UpdatedTs int64 `json:"updatedTs"`
// Domain specific fields
Email string `json:"email"`
@ -38,6 +49,9 @@ type UserCreate struct {
type UserPatch struct {
ID int
// Standard fields
RowStatus *RowStatus `json:"rowStatus"`
// Domain specific fields
Email *string `json:"email"`
Name *string `json:"name"`
@ -50,6 +64,9 @@ type UserPatch struct {
type UserFind struct {
ID *int `json:"id"`
// Standard fields
RowStatus *RowStatus `json:"rowStatus"`
// Domain specific fields
Email *string `json:"email"`
Role *Role

View File

@ -27,6 +27,8 @@ func (s *Server) registerAuthRoutes(g *echo.Group) {
}
if user == nil {
return echo.NewHTTPError(http.StatusUnauthorized, fmt.Sprintf("User not found with email %s", login.Email))
} else if user.RowStatus == api.Archived {
return echo.NewHTTPError(http.StatusForbidden, fmt.Sprintf("User has been archived with email %s", login.Email))
}
// Compare the stored hashed password, with the hashed version of the password that was received.

View File

@ -85,6 +85,8 @@ func BasicAuthMiddleware(s *Server, next echo.HandlerFunc) echo.HandlerFunc {
}
if user == nil {
return echo.NewHTTPError(http.StatusUnauthorized, fmt.Sprintf("Not found user ID: %d", userID))
} else if user.RowStatus == api.Archived {
return echo.NewHTTPError(http.StatusForbidden, fmt.Sprintf("User has been archived with email %s", user.Email))
}
// Stores userID into context.

View File

@ -65,10 +65,16 @@ func (s *Server) registerMemoRoutes(g *echo.Group) {
memoFind := &api.MemoFind{
CreatorID: &userID,
}
rowStatus := c.QueryParam("rowStatus")
rowStatus := api.RowStatus(c.QueryParam("rowStatus"))
if rowStatus != "" {
memoFind.RowStatus = &rowStatus
}
pinnedStr := c.QueryParam("pinned")
if pinnedStr != "" {
pinned := pinnedStr == "true"
memoFind.Pinned = &pinned
}
list, err := s.Store.FindMemoList(memoFind)
if err != nil {
@ -83,6 +89,45 @@ func (s *Server) registerMemoRoutes(g *echo.Group) {
return nil
})
g.POST("/memo/:memoId/organizer", func(c echo.Context) error {
memoID, err := strconv.Atoi(c.Param("memoId"))
if err != nil {
return echo.NewHTTPError(http.StatusBadRequest, fmt.Sprintf("ID is not a number: %s", c.Param("memoId"))).SetInternal(err)
}
userID := c.Get(getUserIDContextKey()).(int)
memoOrganizerUpsert := &api.MemoOrganizerUpsert{
MemoID: memoID,
UserID: userID,
}
if err := json.NewDecoder(c.Request().Body).Decode(memoOrganizerUpsert); err != nil {
return echo.NewHTTPError(http.StatusBadRequest, "Malformatted post memo organizer request").SetInternal(err)
}
err = s.Store.UpsertMemoOrganizer(memoOrganizerUpsert)
if err != nil {
return echo.NewHTTPError(http.StatusInternalServerError, "Failed to upsert memo organizer").SetInternal(err)
}
memo, err := s.Store.FindMemo(&api.MemoFind{
ID: &memoID,
})
if err != nil {
if common.ErrorCode(err) == common.NotFound {
return echo.NewHTTPError(http.StatusNotFound, fmt.Sprintf("Memo ID not found: %d", memoID)).SetInternal(err)
}
return echo.NewHTTPError(http.StatusInternalServerError, fmt.Sprintf("Failed to find memo by ID: %v", memoID)).SetInternal(err)
}
c.Response().Header().Set(echo.HeaderContentType, echo.MIMEApplicationJSONCharsetUTF8)
if err := json.NewEncoder(c.Response().Writer).Encode(composeResponse(memo)); err != nil {
return echo.NewHTTPError(http.StatusInternalServerError, "Failed to encode memo response").SetInternal(err)
}
return nil
})
g.GET("/memo/:memoId", func(c echo.Context) error {
memoID, err := strconv.Atoi(c.Param("memoId"))
if err != nil {

View File

@ -6,6 +6,7 @@ import (
"memos/api"
"memos/common"
"net/http"
"strconv"
"github.com/labstack/echo/v4"
"golang.org/x/crypto/bcrypt"
@ -84,19 +85,6 @@ func (s *Server) registerUserRoutes(g *echo.Group) {
return echo.NewHTTPError(http.StatusBadRequest, "Malformatted patch user request").SetInternal(err)
}
if userPatch.Email != nil {
userFind := api.UserFind{
Email: userPatch.Email,
}
user, err := s.Store.FindUser(&userFind)
if err != nil {
return echo.NewHTTPError(http.StatusInternalServerError, fmt.Sprintf("Failed to find user by email %s", *userPatch.Email)).SetInternal(err)
}
if user != nil {
return echo.NewHTTPError(http.StatusBadRequest, fmt.Sprintf("User with email %s existed", *userPatch.Email)).SetInternal(err)
}
}
if userPatch.Password != nil && *userPatch.Password != "" {
passwordHash, err := bcrypt.GenerateFromPassword([]byte(*userPatch.Password), bcrypt.DefaultCost)
if err != nil {
@ -124,4 +112,53 @@ func (s *Server) registerUserRoutes(g *echo.Group) {
return nil
})
g.PATCH("/user/:userId", func(c echo.Context) error {
currentUserID := c.Get(getUserIDContextKey()).(int)
currentUser, err := s.Store.FindUser(&api.UserFind{
ID: &currentUserID,
})
if err != nil {
return echo.NewHTTPError(http.StatusInternalServerError, "Failed to find user").SetInternal(err)
}
if currentUser == nil {
return echo.NewHTTPError(http.StatusBadRequest, fmt.Sprintf("Current session user not found with ID: %d", currentUserID)).SetInternal(err)
} else if currentUser.Role != api.Owner {
return echo.NewHTTPError(http.StatusForbidden, "Access forbidden for current session user").SetInternal(err)
}
userID, err := strconv.Atoi(c.Param("userId"))
if err != nil {
return echo.NewHTTPError(http.StatusBadRequest, fmt.Sprintf("ID is not a number: %s", c.Param("userId"))).SetInternal(err)
}
userPatch := &api.UserPatch{
ID: userID,
}
if err := json.NewDecoder(c.Request().Body).Decode(userPatch); err != nil {
return echo.NewHTTPError(http.StatusBadRequest, "Malformatted patch user request").SetInternal(err)
}
if userPatch.Password != nil && *userPatch.Password != "" {
passwordHash, err := bcrypt.GenerateFromPassword([]byte(*userPatch.Password), bcrypt.DefaultCost)
if err != nil {
return echo.NewHTTPError(http.StatusInternalServerError, "Failed to generate password hash").SetInternal(err)
}
passwordHashStr := string(passwordHash)
userPatch.PasswordHash = &passwordHashStr
}
user, err := s.Store.PatchUser(userPatch)
if err != nil {
return echo.NewHTTPError(http.StatusInternalServerError, "Failed to patch user").SetInternal(err)
}
c.Response().Header().Set(echo.HeaderContentType, echo.MIMEApplicationJSONCharsetUTF8)
if err := json.NewEncoder(c.Response().Writer).Encode(composeResponse(user)); err != nil {
return echo.NewHTTPError(http.StatusInternalServerError, "Failed to encode user response").SetInternal(err)
}
return nil
})
}

View File

@ -67,7 +67,7 @@ func (s *Server) registerWebhookRoutes(g *echo.Group) {
memoFind := &api.MemoFind{
CreatorID: &user.ID,
}
rowStatus := c.QueryParam("rowStatus")
rowStatus := api.RowStatus(c.QueryParam("rowStatus"))
if rowStatus != "" {
memoFind.RowStatus = &rowStatus
}

View File

@ -7,8 +7,45 @@ import (
"strings"
)
// memoRaw is the store model for an Memo.
// Fields have exactly the same meanings as Memo.
type memoRaw struct {
ID int
// Standard fields
RowStatus api.RowStatus
CreatorID int
CreatedTs int64
UpdatedTs int64
// Domain specific fields
Content string
}
// toMemo creates an instance of Memo based on the memoRaw.
// This is intended to be called when we need to compose an Memo relationship.
func (raw *memoRaw) toMemo() *api.Memo {
return &api.Memo{
ID: raw.ID,
// Standard fields
RowStatus: raw.RowStatus,
CreatorID: raw.CreatorID,
CreatedTs: raw.CreatedTs,
UpdatedTs: raw.UpdatedTs,
// Domain specific fields
Content: raw.Content,
}
}
func (s *Store) CreateMemo(create *api.MemoCreate) (*api.Memo, error) {
memo, err := createMemo(s.db, create)
memoRaw, err := createMemoRaw(s.db, create)
if err != nil {
return nil, err
}
memo, err := s.composeMemo(memoRaw)
if err != nil {
return nil, err
}
@ -17,7 +54,12 @@ func (s *Store) CreateMemo(create *api.MemoCreate) (*api.Memo, error) {
}
func (s *Store) PatchMemo(patch *api.MemoPatch) (*api.Memo, error) {
memo, err := patchMemo(s.db, patch)
memoRaw, err := patchMemoRaw(s.db, patch)
if err != nil {
return nil, err
}
memo, err := s.composeMemo(memoRaw)
if err != nil {
return nil, err
}
@ -26,16 +68,26 @@ func (s *Store) PatchMemo(patch *api.MemoPatch) (*api.Memo, error) {
}
func (s *Store) FindMemoList(find *api.MemoFind) ([]*api.Memo, error) {
list, err := findMemoList(s.db, find)
memoRawList, err := findMemoRawList(s.db, find)
if err != nil {
return nil, err
}
list := []*api.Memo{}
for _, raw := range memoRawList {
memo, err := s.composeMemo(raw)
if err != nil {
return nil, err
}
list = append(list, memo)
}
return list, nil
}
func (s *Store) FindMemo(find *api.MemoFind) (*api.Memo, error) {
list, err := findMemoList(s.db, find)
list, err := findMemoRawList(s.db, find)
if err != nil {
return nil, err
}
@ -44,7 +96,12 @@ func (s *Store) FindMemo(find *api.MemoFind) (*api.Memo, error) {
return nil, &common.Error{Code: common.NotFound, Err: fmt.Errorf("not found")}
}
return list[0], nil
memo, err := s.composeMemo(list[0])
if err != nil {
return nil, err
}
return memo, nil
}
func (s *Store) DeleteMemo(delete *api.MemoDelete) error {
@ -56,7 +113,7 @@ func (s *Store) DeleteMemo(delete *api.MemoDelete) error {
return nil
}
func createMemo(db *DB, create *api.MemoCreate) (*api.Memo, error) {
func createMemoRaw(db *DB, create *api.MemoCreate) (*memoRaw, error) {
set := []string{"creator_id", "content"}
placeholder := []string{"?", "?"}
args := []interface{}{create.CreatorID, create.Content}
@ -79,26 +136,23 @@ func createMemo(db *DB, create *api.MemoCreate) (*api.Memo, error) {
}
defer row.Close()
if !row.Next() {
return nil, &common.Error{Code: common.NotFound, Err: fmt.Errorf("not found")}
}
var memo api.Memo
row.Next()
var memoRaw memoRaw
if err := row.Scan(
&memo.ID,
&memo.CreatorID,
&memo.CreatedTs,
&memo.UpdatedTs,
&memo.Content,
&memo.RowStatus,
&memoRaw.ID,
&memoRaw.CreatorID,
&memoRaw.CreatedTs,
&memoRaw.UpdatedTs,
&memoRaw.Content,
&memoRaw.RowStatus,
); err != nil {
return nil, FormatError(err)
}
return &memo, nil
return &memoRaw, nil
}
func patchMemo(db *DB, patch *api.MemoPatch) (*api.Memo, error) {
func patchMemoRaw(db *DB, patch *api.MemoPatch) (*memoRaw, error) {
set, args := []string{}, []interface{}{}
if v := patch.Content; v != nil {
@ -125,21 +179,21 @@ func patchMemo(db *DB, patch *api.MemoPatch) (*api.Memo, error) {
return nil, &common.Error{Code: common.NotFound, Err: fmt.Errorf("not found")}
}
var memo api.Memo
var memoRaw memoRaw
if err := row.Scan(
&memo.ID,
&memo.CreatedTs,
&memo.UpdatedTs,
&memo.Content,
&memo.RowStatus,
&memoRaw.ID,
&memoRaw.CreatedTs,
&memoRaw.UpdatedTs,
&memoRaw.Content,
&memoRaw.RowStatus,
); err != nil {
return nil, FormatError(err)
}
return &memo, nil
return &memoRaw, nil
}
func findMemoList(db *DB, find *api.MemoFind) ([]*api.Memo, error) {
func findMemoRawList(db *DB, find *api.MemoFind) ([]*memoRaw, error) {
where, args := []string{"1 = 1"}, []interface{}{}
if v := find.ID; v != nil {
@ -151,6 +205,9 @@ func findMemoList(db *DB, find *api.MemoFind) ([]*api.Memo, error) {
if v := find.RowStatus; v != nil {
where, args = append(where, "row_status = ?"), append(args, *v)
}
if v := find.Pinned; v != nil {
where = append(where, "id in (SELECT memo_id FROM memo_organizer WHERE pinned = 1 AND user_id = memo.creator_id )")
}
rows, err := db.Db.Query(`
SELECT
@ -169,28 +226,28 @@ func findMemoList(db *DB, find *api.MemoFind) ([]*api.Memo, error) {
}
defer rows.Close()
list := make([]*api.Memo, 0)
memoRawList := make([]*memoRaw, 0)
for rows.Next() {
var memo api.Memo
var memoRaw memoRaw
if err := rows.Scan(
&memo.ID,
&memo.CreatorID,
&memo.CreatedTs,
&memo.UpdatedTs,
&memo.Content,
&memo.RowStatus,
&memoRaw.ID,
&memoRaw.CreatorID,
&memoRaw.CreatedTs,
&memoRaw.UpdatedTs,
&memoRaw.Content,
&memoRaw.RowStatus,
); err != nil {
return nil, FormatError(err)
}
list = append(list, &memo)
memoRawList = append(memoRawList, &memoRaw)
}
if err := rows.Err(); err != nil {
return nil, FormatError(err)
}
return list, nil
return memoRawList, nil
}
func deleteMemo(db *DB, delete *api.MemoDelete) error {
@ -206,3 +263,19 @@ func deleteMemo(db *DB, delete *api.MemoDelete) error {
return nil
}
func (s *Store) composeMemo(raw *memoRaw) (*api.Memo, error) {
memo := raw.toMemo()
memoOrganizer, err := s.FindMemoOrganizer(&api.MemoOrganizerFind{
MemoID: memo.ID,
UserID: memo.CreatorID,
})
if err != nil && common.ErrorCode(err) != common.NotFound {
return nil, err
} else if memoOrganizer != nil {
memo.Pinned = memoOrganizer.Pinned
}
return memo, nil
}

117
store/memo_organizer.go Normal file
View File

@ -0,0 +1,117 @@
package store
import (
"fmt"
"memos/api"
"memos/common"
)
// memoOrganizerRaw is the store model for an MemoOrganizer.
// Fields have exactly the same meanings as MemoOrganizer.
type memoOrganizerRaw struct {
ID int
// Domain specific fields
MemoID int
UserID int
Pinned bool
}
func (raw *memoOrganizerRaw) toMemoOrganizer() *api.MemoOrganizer {
return &api.MemoOrganizer{
ID: raw.ID,
MemoID: raw.MemoID,
UserID: raw.UserID,
Pinned: raw.Pinned,
}
}
func (s *Store) FindMemoOrganizer(find *api.MemoOrganizerFind) (*api.MemoOrganizer, error) {
memoOrganizerRaw, err := findMemoOrganizer(s.db, find)
if err != nil {
return nil, err
}
memoOrganizer := memoOrganizerRaw.toMemoOrganizer()
return memoOrganizer, nil
}
func (s *Store) UpsertMemoOrganizer(upsert *api.MemoOrganizerUpsert) error {
err := upsertMemoOrganizer(s.db, upsert)
if err != nil {
return err
}
return nil
}
func findMemoOrganizer(db *DB, find *api.MemoOrganizerFind) (*memoOrganizerRaw, error) {
row, err := db.Db.Query(`
SELECT
id,
memo_id,
user_id,
pinned
FROM memo_organizer
WHERE memo_id = ? AND user_id = ?
`, find.MemoID, find.UserID)
if err != nil {
return nil, FormatError(err)
}
defer row.Close()
if !row.Next() {
return nil, &common.Error{Code: common.NotFound, Err: fmt.Errorf("not found")}
}
var memoOrganizerRaw memoOrganizerRaw
if err := row.Scan(
&memoOrganizerRaw.ID,
&memoOrganizerRaw.MemoID,
&memoOrganizerRaw.UserID,
&memoOrganizerRaw.Pinned,
); err != nil {
return nil, FormatError(err)
}
return &memoOrganizerRaw, nil
}
func upsertMemoOrganizer(db *DB, upsert *api.MemoOrganizerUpsert) error {
row, err := db.Db.Query(`
INSERT INTO memo_organizer (
memo_id,
user_id,
pinned
)
VALUES (?, ?, ?)
ON CONFLICT(memo_id, user_id) DO UPDATE
SET
pinned = EXCLUDED.pinned
RETURNING id, memo_id, user_id, pinned
`,
upsert.MemoID,
upsert.UserID,
upsert.Pinned,
)
if err != nil {
return FormatError(err)
}
defer row.Close()
row.Next()
var memoOrganizer api.MemoOrganizer
if err := row.Scan(
&memoOrganizer.ID,
&memoOrganizer.MemoID,
&memoOrganizer.UserID,
&memoOrganizer.Pinned,
); err != nil {
return FormatError(err)
}
return nil
}

View File

@ -1,3 +1,4 @@
DROP TABLE IF EXISTS `memo_organizer`;
DROP TABLE IF EXISTS `memo`;
DROP TABLE IF EXISTS `shortcut`;
DROP TABLE IF EXISTS `resource`;

View File

@ -3,6 +3,8 @@ CREATE TABLE user (
id INTEGER PRIMARY KEY AUTOINCREMENT,
created_ts BIGINT NOT NULL DEFAULT (strftime('%s', 'now')),
updated_ts BIGINT NOT NULL DEFAULT (strftime('%s', 'now')),
-- allowed row status are 'NORMAL', 'ARCHIVED'.
row_status TEXT NOT NULL CHECK (row_status IN ('NORMAL', 'ARCHIVED')) DEFAULT 'NORMAL',
email TEXT NOT NULL UNIQUE,
role TEXT NOT NULL CHECK (role IN ('OWNER', 'USER')) DEFAULT 'USER',
name TEXT NOT NULL,
@ -33,10 +35,9 @@ CREATE TABLE memo (
creator_id INTEGER NOT NULL,
created_ts BIGINT NOT NULL DEFAULT (strftime('%s', 'now')),
updated_ts BIGINT NOT NULL DEFAULT (strftime('%s', 'now')),
-- allowed row status are 'NORMAL', 'ARCHIVED', 'HIDDEN'.
row_status TEXT NOT NULL DEFAULT 'NORMAL',
row_status TEXT NOT NULL CHECK (row_status IN ('NORMAL', 'ARCHIVED')) DEFAULT 'NORMAL',
content TEXT NOT NULL DEFAULT '',
FOREIGN KEY(creator_id) REFERENCES users(id)
FOREIGN KEY(creator_id) REFERENCES user(id)
);
INSERT INTO
@ -56,17 +57,32 @@ WHERE
rowid = old.rowid;
END;
-- memo_organizer
CREATE TABLE memo_organizer (
id INTEGER PRIMARY KEY AUTOINCREMENT,
memo_id INTEGER NOT NULL,
user_id INTEGER NOT NULL,
pinned INTEGER NOT NULL CHECK (pinned IN (0, 1)) DEFAULT 0,
FOREIGN KEY(memo_id) REFERENCES memo(id),
FOREIGN KEY(user_id) REFERENCES user(id),
UNIQUE(memo_id, user_id)
);
INSERT INTO
sqlite_sequence (name, seq)
VALUES
('memo_organizer', 100);
-- shortcut
CREATE TABLE shortcut (
id INTEGER PRIMARY KEY AUTOINCREMENT,
creator_id INTEGER NOT NULL,
created_ts BIGINT NOT NULL DEFAULT (strftime('%s', 'now')),
updated_ts BIGINT NOT NULL DEFAULT (strftime('%s', 'now')),
row_status TEXT NOT NULL CHECK (row_status IN ('NORMAL', 'ARCHIVED')) DEFAULT 'NORMAL',
title TEXT NOT NULL DEFAULT '',
payload TEXT NOT NULL DEFAULT '{}',
-- allowed row status are 'NORMAL', 'ARCHIVED'.
row_status TEXT NOT NULL DEFAULT 'NORMAL',
FOREIGN KEY(creator_id) REFERENCES users(id)
FOREIGN KEY(creator_id) REFERENCES user(id)
);
INSERT INTO
@ -96,7 +112,7 @@ CREATE TABLE resource (
blob BLOB NOT NULL,
type TEXT NOT NULL DEFAULT '',
size INTEGER NOT NULL DEFAULT 0,
FOREIGN KEY(creator_id) REFERENCES users(id)
FOREIGN KEY(creator_id) REFERENCES user(id)
);
INSERT INTO

View File

@ -7,22 +7,63 @@ import (
"strings"
)
// resourceRaw is the store model for an Resource.
// Fields have exactly the same meanings as Resource.
type resourceRaw struct {
ID int
// Standard fields
CreatorID int
CreatedTs int64
UpdatedTs int64
// Domain specific fields
Filename string
Blob []byte
Type string
Size int64
}
func (raw *resourceRaw) toResource() *api.Resource {
return &api.Resource{
ID: raw.ID,
// Standard fields
CreatorID: raw.CreatorID,
CreatedTs: raw.CreatedTs,
UpdatedTs: raw.UpdatedTs,
// Domain specific fields
Filename: raw.Filename,
Blob: raw.Blob,
Type: raw.Type,
Size: raw.Size,
}
}
func (s *Store) CreateResource(create *api.ResourceCreate) (*api.Resource, error) {
resource, err := createResource(s.db, create)
resourceRaw, err := createResource(s.db, create)
if err != nil {
return nil, err
}
resource := resourceRaw.toResource()
return resource, nil
}
func (s *Store) FindResourceList(find *api.ResourceFind) ([]*api.Resource, error) {
list, err := findResourceList(s.db, find)
resourceRawList, err := findResourceList(s.db, find)
if err != nil {
return nil, err
}
return list, nil
resourceList := []*api.Resource{}
for _, raw := range resourceRawList {
resourceList = append(resourceList, raw.toResource())
}
return resourceList, nil
}
func (s *Store) FindResource(find *api.ResourceFind) (*api.Resource, error) {
@ -35,7 +76,9 @@ func (s *Store) FindResource(find *api.ResourceFind) (*api.Resource, error) {
return nil, &common.Error{Code: common.NotFound, Err: fmt.Errorf("not found")}
}
return list[0], nil
resource := list[0].toResource()
return resource, nil
}
func (s *Store) DeleteResource(delete *api.ResourceDelete) error {
@ -47,7 +90,7 @@ func (s *Store) DeleteResource(delete *api.ResourceDelete) error {
return nil
}
func createResource(db *DB, create *api.ResourceCreate) (*api.Resource, error) {
func createResource(db *DB, create *api.ResourceCreate) (*resourceRaw, error) {
row, err := db.Db.Query(`
INSERT INTO resource (
filename,
@ -70,27 +113,24 @@ func createResource(db *DB, create *api.ResourceCreate) (*api.Resource, error) {
}
defer row.Close()
if !row.Next() {
return nil, &common.Error{Code: common.NotFound, Err: fmt.Errorf("not found")}
}
var resource api.Resource
row.Next()
var resourceRaw resourceRaw
if err := row.Scan(
&resource.ID,
&resource.Filename,
&resource.Blob,
&resource.Type,
&resource.Size,
&resource.CreatedTs,
&resource.UpdatedTs,
&resourceRaw.ID,
&resourceRaw.Filename,
&resourceRaw.Blob,
&resourceRaw.Type,
&resourceRaw.Size,
&resourceRaw.CreatedTs,
&resourceRaw.UpdatedTs,
); err != nil {
return nil, FormatError(err)
}
return &resource, nil
return &resourceRaw, nil
}
func findResourceList(db *DB, find *api.ResourceFind) ([]*api.Resource, error) {
func findResourceList(db *DB, find *api.ResourceFind) ([]*resourceRaw, error) {
where, args := []string{"1 = 1"}, []interface{}{}
if v := find.ID; v != nil {
@ -121,29 +161,29 @@ func findResourceList(db *DB, find *api.ResourceFind) ([]*api.Resource, error) {
}
defer rows.Close()
list := make([]*api.Resource, 0)
resourceRawList := make([]*resourceRaw, 0)
for rows.Next() {
var resource api.Resource
var resourceRaw resourceRaw
if err := rows.Scan(
&resource.ID,
&resource.Filename,
&resource.Blob,
&resource.Type,
&resource.Size,
&resource.CreatedTs,
&resource.UpdatedTs,
&resourceRaw.ID,
&resourceRaw.Filename,
&resourceRaw.Blob,
&resourceRaw.Type,
&resourceRaw.Size,
&resourceRaw.CreatedTs,
&resourceRaw.UpdatedTs,
); err != nil {
return nil, FormatError(err)
}
list = append(list, &resource)
resourceRawList = append(resourceRawList, &resourceRaw)
}
if err := rows.Err(); err != nil {
return nil, FormatError(err)
}
return list, nil
return resourceRawList, nil
}
func deleteResource(db *DB, delete *api.ResourceDelete) error {

View File

@ -1,3 +1,4 @@
DELETE FROM memo_organizer;
DELETE FROM resource;
DELETE FROM shortcut;
DELETE FROM memo;

View File

@ -12,6 +12,6 @@ VALUES
'guest',
'guest@example.com',
'guest_open_id',
-- "secret"
-- raw password: secret
'$2a$14$ajq8Q7fbtFRQvXpdCq7Jcuy.Rx1h/L4J60Otx.gyNLbAYctGMJ9tK'
);

View File

@ -1,23 +1,25 @@
INSERT INTO
memo (
`id`,
`content`,
`creator_id`
)
VALUES
(
101,
'#memos 👋 Welcome to memos',
101
);
INSERT INTO
memo (
`id`,
`content`,
`creator_id`,
`row_status`
`creator_id`
)
VALUES
(
102,
'好好学习,天天向上。',
101,
'ARCHIVED'
101
);

View File

@ -0,0 +1,12 @@
INSERT INTO
memo_organizer (
`memo_id`,
`user_id`,
`pinned`
)
VALUES
(
102,
101,
1
);

View File

@ -7,30 +7,69 @@ import (
"strings"
)
// shortcutRaw is the store model for an Shortcut.
// Fields have exactly the same meanings as Shortcut.
type shortcutRaw struct {
ID int
// Standard fields
RowStatus api.RowStatus
CreatorID int
CreatedTs int64
UpdatedTs int64
// Domain specific fields
Title string
Payload string
}
func (raw *shortcutRaw) toShortcut() *api.Shortcut {
return &api.Shortcut{
ID: raw.ID,
RowStatus: raw.RowStatus,
CreatorID: raw.CreatorID,
CreatedTs: raw.CreatedTs,
UpdatedTs: raw.UpdatedTs,
Title: raw.Title,
Payload: raw.Payload,
}
}
func (s *Store) CreateShortcut(create *api.ShortcutCreate) (*api.Shortcut, error) {
shortcut, err := createShortcut(s.db, create)
shortcutRaw, err := createShortcut(s.db, create)
if err != nil {
return nil, err
}
shortcut := shortcutRaw.toShortcut()
return shortcut, nil
}
func (s *Store) PatchShortcut(patch *api.ShortcutPatch) (*api.Shortcut, error) {
shortcut, err := patchShortcut(s.db, patch)
shortcutRaw, err := patchShortcut(s.db, patch)
if err != nil {
return nil, err
}
shortcut := shortcutRaw.toShortcut()
return shortcut, nil
}
func (s *Store) FindShortcutList(find *api.ShortcutFind) ([]*api.Shortcut, error) {
list, err := findShortcutList(s.db, find)
shortcutRawList, err := findShortcutList(s.db, find)
if err != nil {
return nil, err
}
list := []*api.Shortcut{}
for _, raw := range shortcutRawList {
list = append(list, raw.toShortcut())
}
return list, nil
}
@ -44,7 +83,9 @@ func (s *Store) FindShortcut(find *api.ShortcutFind) (*api.Shortcut, error) {
return nil, &common.Error{Code: common.NotFound, Err: fmt.Errorf("not found")}
}
return list[0], nil
shortcut := list[0].toShortcut()
return shortcut, nil
}
func (s *Store) DeleteShortcut(delete *api.ShortcutDelete) error {
@ -56,7 +97,7 @@ func (s *Store) DeleteShortcut(delete *api.ShortcutDelete) error {
return nil
}
func createShortcut(db *DB, create *api.ShortcutCreate) (*api.Shortcut, error) {
func createShortcut(db *DB, create *api.ShortcutCreate) (*shortcutRaw, error) {
row, err := db.Db.Query(`
INSERT INTO shortcut (
title,
@ -70,30 +111,29 @@ func createShortcut(db *DB, create *api.ShortcutCreate) (*api.Shortcut, error) {
create.Payload,
create.CreatorID,
)
if err != nil {
return nil, FormatError(err)
}
defer row.Close()
row.Next()
var shortcut api.Shortcut
var shortcutRaw shortcutRaw
if err := row.Scan(
&shortcut.ID,
&shortcut.Title,
&shortcut.Payload,
&shortcut.CreatorID,
&shortcut.CreatedTs,
&shortcut.UpdatedTs,
&shortcut.RowStatus,
&shortcutRaw.ID,
&shortcutRaw.Title,
&shortcutRaw.Payload,
&shortcutRaw.CreatorID,
&shortcutRaw.CreatedTs,
&shortcutRaw.UpdatedTs,
&shortcutRaw.RowStatus,
); err != nil {
return nil, FormatError(err)
}
return &shortcut, nil
return &shortcutRaw, nil
}
func patchShortcut(db *DB, patch *api.ShortcutPatch) (*api.Shortcut, error) {
func patchShortcut(db *DB, patch *api.ShortcutPatch) (*shortcutRaw, error) {
set, args := []string{}, []interface{}{}
if v := patch.Title; v != nil {
@ -123,22 +163,22 @@ func patchShortcut(db *DB, patch *api.ShortcutPatch) (*api.Shortcut, error) {
return nil, &common.Error{Code: common.NotFound, Err: fmt.Errorf("not found")}
}
var shortcut api.Shortcut
var shortcutRaw shortcutRaw
if err := row.Scan(
&shortcut.ID,
&shortcut.Title,
&shortcut.Payload,
&shortcut.CreatedTs,
&shortcut.UpdatedTs,
&shortcut.RowStatus,
&shortcutRaw.ID,
&shortcutRaw.Title,
&shortcutRaw.Payload,
&shortcutRaw.CreatedTs,
&shortcutRaw.UpdatedTs,
&shortcutRaw.RowStatus,
); err != nil {
return nil, FormatError(err)
}
return &shortcut, nil
return &shortcutRaw, nil
}
func findShortcutList(db *DB, find *api.ShortcutFind) ([]*api.Shortcut, error) {
func findShortcutList(db *DB, find *api.ShortcutFind) ([]*shortcutRaw, error) {
where, args := []string{"1 = 1"}, []interface{}{}
if v := find.ID; v != nil {
@ -169,29 +209,29 @@ func findShortcutList(db *DB, find *api.ShortcutFind) ([]*api.Shortcut, error) {
}
defer rows.Close()
list := make([]*api.Shortcut, 0)
shortcutRawList := make([]*shortcutRaw, 0)
for rows.Next() {
var shortcut api.Shortcut
var shortcutRaw shortcutRaw
if err := rows.Scan(
&shortcut.ID,
&shortcut.Title,
&shortcut.Payload,
&shortcut.CreatorID,
&shortcut.CreatedTs,
&shortcut.UpdatedTs,
&shortcut.RowStatus,
&shortcutRaw.ID,
&shortcutRaw.Title,
&shortcutRaw.Payload,
&shortcutRaw.CreatorID,
&shortcutRaw.CreatedTs,
&shortcutRaw.UpdatedTs,
&shortcutRaw.RowStatus,
); err != nil {
return nil, FormatError(err)
}
list = append(list, &shortcut)
shortcutRawList = append(shortcutRawList, &shortcutRaw)
}
if err := rows.Err(); err != nil {
return nil, FormatError(err)
}
return list, nil
return shortcutRawList, nil
}
func deleteShortcut(db *DB, delete *api.ShortcutDelete) error {

View File

@ -7,30 +7,73 @@ import (
"strings"
)
// userRaw is the store model for an User.
// Fields have exactly the same meanings as User.
type userRaw struct {
ID int
// Standard fields
RowStatus api.RowStatus
CreatedTs int64
UpdatedTs int64
// Domain specific fields
Email string
Role api.Role
Name string
PasswordHash string
OpenID string
}
func (raw *userRaw) toUser() *api.User {
return &api.User{
ID: raw.ID,
RowStatus: raw.RowStatus,
CreatedTs: raw.CreatedTs,
UpdatedTs: raw.UpdatedTs,
Email: raw.Email,
Role: raw.Role,
Name: raw.Name,
PasswordHash: raw.PasswordHash,
OpenID: raw.OpenID,
}
}
func (s *Store) CreateUser(create *api.UserCreate) (*api.User, error) {
user, err := createUser(s.db, create)
userRaw, err := createUser(s.db, create)
if err != nil {
return nil, err
}
user := userRaw.toUser()
return user, nil
}
func (s *Store) PatchUser(patch *api.UserPatch) (*api.User, error) {
user, err := patchUser(s.db, patch)
userRaw, err := patchUser(s.db, patch)
if err != nil {
return nil, err
}
user := userRaw.toUser()
return user, nil
}
func (s *Store) FindUserList(find *api.UserFind) ([]*api.User, error) {
list, err := findUserList(s.db, find)
userRawList, err := findUserList(s.db, find)
if err != nil {
return nil, err
}
list := []*api.User{}
for _, raw := range userRawList {
list = append(list, raw.toUser())
}
return list, nil
}
@ -46,10 +89,12 @@ func (s *Store) FindUser(find *api.UserFind) (*api.User, error) {
return nil, &common.Error{Code: common.Conflict, Err: fmt.Errorf("found %d users with filter %+v, expect 1. ", len(list), find)}
}
return list[0], nil
user := list[0].toUser()
return user, nil
}
func createUser(db *DB, create *api.UserCreate) (*api.User, error) {
func createUser(db *DB, create *api.UserCreate) (*userRaw, error) {
row, err := db.Db.Query(`
INSERT INTO user (
email,
@ -73,37 +118,40 @@ func createUser(db *DB, create *api.UserCreate) (*api.User, error) {
defer row.Close()
row.Next()
var user api.User
var userRaw userRaw
if err := row.Scan(
&user.ID,
&user.Email,
&user.Role,
&user.Name,
&user.PasswordHash,
&user.OpenID,
&user.CreatedTs,
&user.UpdatedTs,
&userRaw.ID,
&userRaw.Email,
&userRaw.Role,
&userRaw.Name,
&userRaw.PasswordHash,
&userRaw.OpenID,
&userRaw.CreatedTs,
&userRaw.UpdatedTs,
); err != nil {
return nil, FormatError(err)
}
return &user, nil
return &userRaw, nil
}
func patchUser(db *DB, patch *api.UserPatch) (*api.User, error) {
func patchUser(db *DB, patch *api.UserPatch) (*userRaw, error) {
set, args := []string{}, []interface{}{}
if v := patch.RowStatus; v != nil {
set, args = append(set, "row_status = ?"), append(args, *v)
}
if v := patch.Email; v != nil {
set, args = append(set, "email = ?"), append(args, v)
set, args = append(set, "email = ?"), append(args, *v)
}
if v := patch.Name; v != nil {
set, args = append(set, "name = ?"), append(args, v)
set, args = append(set, "name = ?"), append(args, *v)
}
if v := patch.PasswordHash; v != nil {
set, args = append(set, "password_hash = ?"), append(args, v)
set, args = append(set, "password_hash = ?"), append(args, *v)
}
if v := patch.OpenID; v != nil {
set, args = append(set, "open_id = ?"), append(args, v)
set, args = append(set, "open_id = ?"), append(args, *v)
}
args = append(args, patch.ID)
@ -120,27 +168,27 @@ func patchUser(db *DB, patch *api.UserPatch) (*api.User, error) {
defer row.Close()
if row.Next() {
var user api.User
var userRaw userRaw
if err := row.Scan(
&user.ID,
&user.Email,
&user.Role,
&user.Name,
&user.PasswordHash,
&user.OpenID,
&user.CreatedTs,
&user.UpdatedTs,
&userRaw.ID,
&userRaw.Email,
&userRaw.Role,
&userRaw.Name,
&userRaw.PasswordHash,
&userRaw.OpenID,
&userRaw.CreatedTs,
&userRaw.UpdatedTs,
); err != nil {
return nil, FormatError(err)
}
return &user, nil
return &userRaw, nil
}
return nil, &common.Error{Code: common.NotFound, Err: fmt.Errorf("user ID not found: %d", patch.ID)}
}
func findUserList(db *DB, find *api.UserFind) ([]*api.User, error) {
func findUserList(db *DB, find *api.UserFind) ([]*userRaw, error) {
where, args := []string{"1 = 1"}, []interface{}{}
if v := find.ID; v != nil {
@ -178,29 +226,29 @@ func findUserList(db *DB, find *api.UserFind) ([]*api.User, error) {
}
defer rows.Close()
list := make([]*api.User, 0)
userRawList := make([]*userRaw, 0)
for rows.Next() {
var user api.User
var userRaw userRaw
if err := rows.Scan(
&user.ID,
&user.Email,
&user.Role,
&user.Name,
&user.PasswordHash,
&user.OpenID,
&user.CreatedTs,
&user.UpdatedTs,
&userRaw.ID,
&userRaw.Email,
&userRaw.Role,
&userRaw.Name,
&userRaw.PasswordHash,
&userRaw.OpenID,
&userRaw.CreatedTs,
&userRaw.UpdatedTs,
); err != nil {
fmt.Println(err)
return nil, FormatError(err)
}
list = append(list, &user)
userRawList = append(userRawList, &userRaw)
}
if err := rows.Err(); err != nil {
return nil, FormatError(err)
}
return list, nil
return userRawList, nil
}