From 8cd9b33906b674c0694768ec8f0a339289e8e731 Mon Sep 17 00:00:00 2001 From: monirzadeh <25131576+Monirzadeh@users.noreply.github.com> Date: Sat, 17 Aug 2024 16:54:07 +0330 Subject: [PATCH 01/22] just return bookmarks after specific timestamp --- internal/database/database.go | 1 + internal/database/sqlite.go | 6 +++ internal/http/routes/api/v1/bookmarks.go | 58 ++++++++++++++++++++++++ 3 files changed, 65 insertions(+) diff --git a/internal/database/database.go b/internal/database/database.go index 211a91919..7a83f4173 100644 --- a/internal/database/database.go +++ b/internal/database/database.go @@ -30,6 +30,7 @@ type GetBookmarksOptions struct { Tags []string ExcludedTags []string Keyword string + LastSync string WithContent bool OrderMethod OrderMethod Limit int diff --git a/internal/database/sqlite.go b/internal/database/sqlite.go index 0675db6aa..fdd908d12 100644 --- a/internal/database/sqlite.go +++ b/internal/database/sqlite.go @@ -315,6 +315,12 @@ func (db *SQLiteDatabase) GetBookmarks(ctx context.Context, opts GetBookmarksOpt args = append(args, opts.IDs) } + // Add where clause for LastSync + if opts.LastSync != "" { + query += ` AND b.modified_at >= ?` + args = append(args, opts.LastSync) + } + // Add where clause for search keyword if opts.Keyword != "" { query += ` AND (b.url LIKE '%' || ? || '%' OR b.excerpt LIKE '%' || ? || '%' OR b.id IN ( diff --git a/internal/http/routes/api/v1/bookmarks.go b/internal/http/routes/api/v1/bookmarks.go index a95ec538b..4edcdccfe 100644 --- a/internal/http/routes/api/v1/bookmarks.go +++ b/internal/http/routes/api/v1/bookmarks.go @@ -7,6 +7,7 @@ import ( fp "path/filepath" "strconv" "sync" + "time" "github.com/gin-gonic/gin" "github.com/go-shiori/shiori/internal/core" @@ -28,6 +29,8 @@ func (r *BookmarksAPIRoutes) Setup(g *gin.RouterGroup) model.Routes { g.Use(middleware.AuthenticationRequired()) g.PUT("/cache", r.updateCache) g.GET("/:id/readable", r.bookmarkReadable) + g.POST("/sync", r.sync) + return r } @@ -114,6 +117,61 @@ func (r *BookmarksAPIRoutes) bookmarkReadable(c *gin.Context) { }) } +type syncPayload struct { + Ids []int `json:"ids" validate:"required"` + LastSync int64 `json:"last_sync"` +} + +func (p *syncPayload) IsValid() error { + if len(p.Ids) == 0 { + return fmt.Errorf("id should not be empty") + } + for _, id := range p.Ids { + if id <= 0 { + return fmt.Errorf("id should not be 0 or negative") + } + } + return nil +} + +// Bookmark Sync godoc +// +// @Summary Get List of bookmark and last time of sync response bookmark change after that time and deleted bookmark. +// @Tags Auth +// @securityDefinitions.apikey ApiKeyAuth +// @Produce json +// @Success 200 {object} readableResponseMessage +// @Failure 403 {object} nil "Token not provided/invalid" +// @Router /api/v1/bookmarks/sync [post] +func (r *BookmarksAPIRoutes) sync(c *gin.Context) { + + var payload syncPayload + if err := c.ShouldBindJSON(&payload); err != nil { + response.SendInternalServerError(c) + return + } + + if err := payload.IsValid(); err != nil { + response.SendError(c, http.StatusBadRequest, err.Error()) + return + } + + //modifiedTime := time.Now().UTC().Format(model.DatabaseDateFormat) + lastsyncformat := time.Unix(payload.LastSync, 0).UTC().Format(model.DatabaseDateFormat) + + filter := database.GetBookmarksOptions{ + LastSync: lastsyncformat, + } + + bookmarks, err := r.deps.Database.GetBookmarks(c, filter) + if err != nil { + r.logger.WithError(err).Error("error getting bookmakrs") + response.SendInternalServerError(c) + return + } + response.Send(c, 200, bookmarks) +} + // updateCache godoc // // @Summary Update Cache and Ebook on server. From 89bea5b04104d6ed0335eb361073e8fe4b922a59 Mon Sep 17 00:00:00 2001 From: monirzadeh <25131576+Monirzadeh@users.noreply.github.com> Date: Sat, 17 Aug 2024 19:26:11 +0330 Subject: [PATCH 02/22] send modified and deleted item in database --- internal/database/database.go | 4 +++ internal/database/mysql.go | 5 ++++ internal/database/pg.go | 6 ++++ internal/database/sqlite.go | 37 ++++++++++++++++++++++++ internal/http/routes/api/v1/bookmarks.go | 22 ++++++++++++-- 5 files changed, 72 insertions(+), 2 deletions(-) diff --git a/internal/database/database.go b/internal/database/database.go index 7a83f4173..6cb63349b 100644 --- a/internal/database/database.go +++ b/internal/database/database.go @@ -29,6 +29,7 @@ type GetBookmarksOptions struct { IDs []int Tags []string ExcludedTags []string + IsDeleted []int Keyword string LastSync string WithContent bool @@ -80,6 +81,9 @@ type DB interface { // SaveBookmarks saves bookmarks data to database. SaveBookmarks(ctx context.Context, create bool, bookmarks ...model.BookmarkDTO) ([]model.BookmarkDTO, error) + // GetDeletedBookmarks fetch list of bookmarks based on submitted options. + GetDeletedBookmarks(ctx context.Context, opts GetBookmarksOptions) ([]int, error) + // GetBookmarks fetch list of bookmarks based on submitted options. GetBookmarks(ctx context.Context, opts GetBookmarksOptions) ([]model.BookmarkDTO, error) diff --git a/internal/database/mysql.go b/internal/database/mysql.go index 27db774d5..4237dad9f 100644 --- a/internal/database/mysql.go +++ b/internal/database/mysql.go @@ -282,6 +282,11 @@ func (db *MySQLDatabase) SaveBookmarks(ctx context.Context, create bool, bookmar return result, nil } +func (db *MySQLDatabase) GetDeletedBookmarks(ctx context.Context, opts GetBookmarksOptions) ([]int, error) { + var missingIDs []int + return missingIDs, nil +} + // GetBookmarks fetch list of bookmarks based on submitted options. func (db *MySQLDatabase) GetBookmarks(ctx context.Context, opts GetBookmarksOptions) ([]model.BookmarkDTO, error) { // Create initial query diff --git a/internal/database/pg.go b/internal/database/pg.go index 66df2dd8d..1af2fbd52 100644 --- a/internal/database/pg.go +++ b/internal/database/pg.go @@ -262,6 +262,12 @@ func (db *PGDatabase) SaveBookmarks(ctx context.Context, create bool, bookmarks return result, nil } +// GetDeletedBookmarks fetch list of bookmark that deleted from database. +func (db *PGDatabase) GetDeletedBookmarks(ctx context.Context, opts GetBookmarksOptions) ([]int, error) { + var missingIDs []int + return missingIDs, nil +} + // GetBookmarks fetch list of bookmarks based on submitted options. func (db *PGDatabase) GetBookmarks(ctx context.Context, opts GetBookmarksOptions) ([]model.BookmarkDTO, error) { // Create initial query diff --git a/internal/database/sqlite.go b/internal/database/sqlite.go index fdd908d12..56b87965a 100644 --- a/internal/database/sqlite.go +++ b/internal/database/sqlite.go @@ -290,6 +290,43 @@ func (db *SQLiteDatabase) SaveBookmarks(ctx context.Context, create bool, bookma return result, nil } +// GetDeletedBookmarks fetch list of bookmark that deleted from database. +func (db *SQLiteDatabase) GetDeletedBookmarks(ctx context.Context, opts GetBookmarksOptions) ([]int, error) { + var missingIDs []int + + // Construct the query using UNION ALL to create a temporary table of IDs + var unionQueries []string + for _, id := range opts.IsDeleted { + unionQueries = append(unionQueries, fmt.Sprintf("SELECT %d AS id", id)) + } + unionQuery := strings.Join(unionQueries, " UNION ALL ") + + query := fmt.Sprintf("SELECT temp.id FROM (%s) AS temp LEFT JOIN bookmark ON temp.id = bookmark.id WHERE bookmark.id IS NULL", unionQuery) + + // Execute the query + rows, err := db.QueryContext(ctx, query) + if err != nil { + return nil, err + } + defer rows.Close() + + // Scan the results into missingIDs + for rows.Next() { + var id int + if err := rows.Scan(&id); err != nil { + return nil, err + } + missingIDs = append(missingIDs, id) + } + + // Check for errors from iterating over rows + if err := rows.Err(); err != nil { + return nil, err + } + + return missingIDs, nil +} + // GetBookmarks fetch list of bookmarks based on submitted options. func (db *SQLiteDatabase) GetBookmarks(ctx context.Context, opts GetBookmarksOptions) ([]model.BookmarkDTO, error) { // Create initial query diff --git a/internal/http/routes/api/v1/bookmarks.go b/internal/http/routes/api/v1/bookmarks.go index 4edcdccfe..bc6bbe003 100644 --- a/internal/http/routes/api/v1/bookmarks.go +++ b/internal/http/routes/api/v1/bookmarks.go @@ -134,6 +134,11 @@ func (p *syncPayload) IsValid() error { return nil } +type syncResponseMessage struct { + Deleted []int `json:"deleted"` + Modified []model.BookmarkDTO `json:"modified"` +} + // Bookmark Sync godoc // // @Summary Get List of bookmark and last time of sync response bookmark change after that time and deleted bookmark. @@ -160,7 +165,8 @@ func (r *BookmarksAPIRoutes) sync(c *gin.Context) { lastsyncformat := time.Unix(payload.LastSync, 0).UTC().Format(model.DatabaseDateFormat) filter := database.GetBookmarksOptions{ - LastSync: lastsyncformat, + LastSync: lastsyncformat, + IsDeleted: payload.Ids, } bookmarks, err := r.deps.Database.GetBookmarks(c, filter) @@ -169,7 +175,19 @@ func (r *BookmarksAPIRoutes) sync(c *gin.Context) { response.SendInternalServerError(c) return } - response.Send(c, 200, bookmarks) + + dbookmarks, err := r.deps.Database.GetDeletedBookmarks(c, filter) + if err != nil { + r.logger.WithError(err).Error("error getting bookmakrs") + response.SendInternalServerError(c) + return + } + //response.Send(c, 200, dbookmarks) + + response.Send(c, 200, syncResponseMessage{ + Deleted: dbookmarks, + Modified: bookmarks, + }) } // updateCache godoc From caac394239cf4d5133184f88fb2d3dd6ec4b432f Mon Sep 17 00:00:00 2001 From: monirzadeh <25131576+Monirzadeh@users.noreply.github.com> Date: Sat, 17 Aug 2024 19:49:53 +0330 Subject: [PATCH 03/22] make swagger --- docs/swagger/docs.go | 22 ++++++++++++++++++++++ docs/swagger/swagger.json | 22 ++++++++++++++++++++++ docs/swagger/swagger.yaml | 15 +++++++++++++++ 3 files changed, 59 insertions(+) diff --git a/docs/swagger/docs.go b/docs/swagger/docs.go index 6c0915288..03bb5be23 100644 --- a/docs/swagger/docs.go +++ b/docs/swagger/docs.go @@ -181,6 +181,28 @@ const docTemplate = `{ } } }, + "/api/v1/bookmarks/sync": { + "post": { + "produces": [ + "application/json" + ], + "tags": [ + "Auth" + ], + "summary": "Get List of bookmark and last time of sync response bookmark change after that time and deleted bookmark.", + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/api_v1.readableResponseMessage" + } + }, + "403": { + "description": "Token not provided/invalid" + } + } + } + }, "/api/v1/system/info": { "get": { "description": "Get general system information like Shiori version, database, and OS", diff --git a/docs/swagger/swagger.json b/docs/swagger/swagger.json index 2d2978e26..63ca4fbef 100644 --- a/docs/swagger/swagger.json +++ b/docs/swagger/swagger.json @@ -170,6 +170,28 @@ } } }, + "/api/v1/bookmarks/sync": { + "post": { + "produces": [ + "application/json" + ], + "tags": [ + "Auth" + ], + "summary": "Get List of bookmark and last time of sync response bookmark change after that time and deleted bookmark.", + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/api_v1.readableResponseMessage" + } + }, + "403": { + "description": "Token not provided/invalid" + } + } + } + }, "/api/v1/system/info": { "get": { "description": "Get general system information like Shiori version, database, and OS", diff --git a/docs/swagger/swagger.yaml b/docs/swagger/swagger.yaml index 6917e65c5..f431107e8 100644 --- a/docs/swagger/swagger.yaml +++ b/docs/swagger/swagger.yaml @@ -257,6 +257,21 @@ paths: summary: Get readable version of bookmark. tags: - Auth + /api/v1/bookmarks/sync: + post: + produces: + - application/json + responses: + "200": + description: OK + schema: + $ref: '#/definitions/api_v1.readableResponseMessage' + "403": + description: Token not provided/invalid + summary: Get List of bookmark and last time of sync response bookmark change + after that time and deleted bookmark. + tags: + - Auth /api/v1/system/info: get: description: Get general system information like Shiori version, database, and From 939b6cc2136f63f7ff7cc76415215501727bc186 Mon Sep 17 00:00:00 2001 From: monirzadeh <25131576+Monirzadeh@users.noreply.github.com> Date: Mon, 16 Sep 2024 16:35:45 +0330 Subject: [PATCH 04/22] pagginate for sync request --- internal/database/sqlite.go | 8 +++- internal/http/routes/api/v1/bookmarks.go | 52 +++++++++++++++++++----- 2 files changed, 48 insertions(+), 12 deletions(-) diff --git a/internal/database/sqlite.go b/internal/database/sqlite.go index 56b87965a..cc9c2b309 100644 --- a/internal/database/sqlite.go +++ b/internal/database/sqlite.go @@ -459,7 +459,7 @@ func (db *SQLiteDatabase) GetBookmarks(ctx context.Context, opts GetBookmarksOpt } // store bookmark IDs for further enrichment - var bookmarkIds = make([]int, 0, len(bookmarks)) + bookmarkIds := make([]int, 0, len(bookmarks)) for _, book := range bookmarks { bookmarkIds = append(bookmarkIds, book.ID) } @@ -552,6 +552,12 @@ func (db *SQLiteDatabase) GetBookmarksCount(ctx context.Context, opts GetBookmar args = append(args, opts.IDs) } + // Add where clause for LastSync + if opts.LastSync != "" { + query += ` AND b.modified_at >= ?` + args = append(args, opts.LastSync) + } + // Add where clause for search keyword if opts.Keyword != "" { query += ` AND (b.url LIKE '%' || ? || '%' OR b.excerpt LIKE '%' || ? || '%' OR b.id IN ( diff --git a/internal/http/routes/api/v1/bookmarks.go b/internal/http/routes/api/v1/bookmarks.go index bc6bbe003..6f975bc92 100644 --- a/internal/http/routes/api/v1/bookmarks.go +++ b/internal/http/routes/api/v1/bookmarks.go @@ -2,6 +2,7 @@ package api_v1 import ( "fmt" + "math" "net/http" "os" fp "path/filepath" @@ -120,6 +121,7 @@ func (r *BookmarksAPIRoutes) bookmarkReadable(c *gin.Context) { type syncPayload struct { Ids []int `json:"ids" validate:"required"` LastSync int64 `json:"last_sync"` + Page int `json:"page"` } func (p *syncPayload) IsValid() error { @@ -134,9 +136,15 @@ func (p *syncPayload) IsValid() error { return nil } +type bookmarksModifiedResponse struct { + Bookmarks []model.BookmarkDTO `json:"bookmarks"` + Page int `json:"page"` + MaxPage int `json:"maxPage"` +} + type syncResponseMessage struct { - Deleted []int `json:"deleted"` - Modified []model.BookmarkDTO `json:"modified"` + Deleted []int `json:"deleted"` + Modified bookmarksModifiedResponse `json:"modified"` } // Bookmark Sync godoc @@ -145,11 +153,10 @@ type syncResponseMessage struct { // @Tags Auth // @securityDefinitions.apikey ApiKeyAuth // @Produce json -// @Success 200 {object} readableResponseMessage +// @Success 200 {object} syncResponseMessage // @Failure 403 {object} nil "Token not provided/invalid" // @Router /api/v1/bookmarks/sync [post] func (r *BookmarksAPIRoutes) sync(c *gin.Context) { - var payload syncPayload if err := c.ShouldBindJSON(&payload); err != nil { response.SendInternalServerError(c) @@ -161,14 +168,30 @@ func (r *BookmarksAPIRoutes) sync(c *gin.Context) { return } - //modifiedTime := time.Now().UTC().Format(model.DatabaseDateFormat) lastsyncformat := time.Unix(payload.LastSync, 0).UTC().Format(model.DatabaseDateFormat) + page := payload.Page + if payload.Page < 1 { + page = 1 + } + filter := database.GetBookmarksOptions{ LastSync: lastsyncformat, IsDeleted: payload.Ids, + Limit: 30, + Offset: (page - 1) * 30, } + // Calculate max page + nBookmarks, err := r.deps.Database.GetBookmarksCount(c, filter) + if err != nil { + r.logger.WithError(err).Error("error getting bookmakrs number") + response.SendInternalServerError(c) + return + } + + maxPage := int(math.Ceil(float64(nBookmarks) / 30)) + bookmarks, err := r.deps.Database.GetBookmarks(c, filter) if err != nil { r.logger.WithError(err).Error("error getting bookmakrs") @@ -176,18 +199,25 @@ func (r *BookmarksAPIRoutes) sync(c *gin.Context) { return } - dbookmarks, err := r.deps.Database.GetDeletedBookmarks(c, filter) + // Get Deleted Bookmarks + deletedBookmarks, err := r.deps.Database.GetDeletedBookmarks(c, filter) if err != nil { r.logger.WithError(err).Error("error getting bookmakrs") response.SendInternalServerError(c) return } - //response.Send(c, 200, dbookmarks) - response.Send(c, 200, syncResponseMessage{ - Deleted: dbookmarks, - Modified: bookmarks, - }) + // Create response using syncResponseMessage struct + resp := syncResponseMessage{ + Deleted: deletedBookmarks, + Modified: bookmarksModifiedResponse{ + Bookmarks: bookmarks, + Page: page, + MaxPage: maxPage, + }, + } + + response.Send(c, 200, resp) } // updateCache godoc From 7412ca372ab2615426d7c9464650ada6aa4808b5 Mon Sep 17 00:00:00 2001 From: monirzadeh <25131576+Monirzadeh@users.noreply.github.com> Date: Mon, 16 Sep 2024 16:38:17 +0330 Subject: [PATCH 05/22] make swagger --- docs/swagger/docs.go | 33 ++++++++++++++++++++++++++++++++- docs/swagger/swagger.json | 33 ++++++++++++++++++++++++++++++++- docs/swagger/swagger.yaml | 22 +++++++++++++++++++++- 3 files changed, 85 insertions(+), 3 deletions(-) diff --git a/docs/swagger/docs.go b/docs/swagger/docs.go index 03bb5be23..fec4ad01e 100644 --- a/docs/swagger/docs.go +++ b/docs/swagger/docs.go @@ -194,7 +194,7 @@ const docTemplate = `{ "200": { "description": "OK", "schema": { - "$ref": "#/definitions/api_v1.readableResponseMessage" + "$ref": "#/definitions/api_v1.syncResponseMessage" } }, "403": { @@ -273,6 +273,23 @@ const docTemplate = `{ } }, "definitions": { + "api_v1.bookmarksModifiedResponse": { + "type": "object", + "properties": { + "bookmarks": { + "type": "array", + "items": { + "$ref": "#/definitions/model.BookmarkDTO" + } + }, + "maxPage": { + "type": "integer" + }, + "page": { + "type": "integer" + } + } + }, "api_v1.infoResponse": { "type": "object", "properties": { @@ -351,6 +368,20 @@ const docTemplate = `{ } } }, + "api_v1.syncResponseMessage": { + "type": "object", + "properties": { + "deleted": { + "type": "array", + "items": { + "type": "integer" + } + }, + "modified": { + "$ref": "#/definitions/api_v1.bookmarksModifiedResponse" + } + } + }, "api_v1.updateCachePayload": { "type": "object", "required": [ diff --git a/docs/swagger/swagger.json b/docs/swagger/swagger.json index 63ca4fbef..a3999de5c 100644 --- a/docs/swagger/swagger.json +++ b/docs/swagger/swagger.json @@ -183,7 +183,7 @@ "200": { "description": "OK", "schema": { - "$ref": "#/definitions/api_v1.readableResponseMessage" + "$ref": "#/definitions/api_v1.syncResponseMessage" } }, "403": { @@ -262,6 +262,23 @@ } }, "definitions": { + "api_v1.bookmarksModifiedResponse": { + "type": "object", + "properties": { + "bookmarks": { + "type": "array", + "items": { + "$ref": "#/definitions/model.BookmarkDTO" + } + }, + "maxPage": { + "type": "integer" + }, + "page": { + "type": "integer" + } + } + }, "api_v1.infoResponse": { "type": "object", "properties": { @@ -340,6 +357,20 @@ } } }, + "api_v1.syncResponseMessage": { + "type": "object", + "properties": { + "deleted": { + "type": "array", + "items": { + "type": "integer" + } + }, + "modified": { + "$ref": "#/definitions/api_v1.bookmarksModifiedResponse" + } + } + }, "api_v1.updateCachePayload": { "type": "object", "required": [ diff --git a/docs/swagger/swagger.yaml b/docs/swagger/swagger.yaml index f431107e8..c92f7efe2 100644 --- a/docs/swagger/swagger.yaml +++ b/docs/swagger/swagger.yaml @@ -1,4 +1,15 @@ definitions: + api_v1.bookmarksModifiedResponse: + properties: + bookmarks: + items: + $ref: '#/definitions/model.BookmarkDTO' + type: array + maxPage: + type: integer + page: + type: integer + type: object api_v1.infoResponse: properties: database: @@ -50,6 +61,15 @@ definitions: config: $ref: '#/definitions/model.UserConfig' type: object + api_v1.syncResponseMessage: + properties: + deleted: + items: + type: integer + type: array + modified: + $ref: '#/definitions/api_v1.bookmarksModifiedResponse' + type: object api_v1.updateCachePayload: properties: create_archive: @@ -265,7 +285,7 @@ paths: "200": description: OK schema: - $ref: '#/definitions/api_v1.readableResponseMessage' + $ref: '#/definitions/api_v1.syncResponseMessage' "403": description: Token not provided/invalid summary: Get List of bookmark and last time of sync response bookmark change From 8e70402bb636a644d3b810a3dbd873768845a317 Mon Sep 17 00:00:00 2001 From: monirzadeh <25131576+Monirzadeh@users.noreply.github.com> Date: Mon, 16 Sep 2024 16:50:10 +0330 Subject: [PATCH 06/22] if ids be empty than just return modified bookmarks --- internal/http/routes/api/v1/bookmarks.go | 17 +++++++++-------- 1 file changed, 9 insertions(+), 8 deletions(-) diff --git a/internal/http/routes/api/v1/bookmarks.go b/internal/http/routes/api/v1/bookmarks.go index 6f975bc92..8d016b346 100644 --- a/internal/http/routes/api/v1/bookmarks.go +++ b/internal/http/routes/api/v1/bookmarks.go @@ -125,9 +125,6 @@ type syncPayload struct { } func (p *syncPayload) IsValid() error { - if len(p.Ids) == 0 { - return fmt.Errorf("id should not be empty") - } for _, id := range p.Ids { if id <= 0 { return fmt.Errorf("id should not be 0 or negative") @@ -200,11 +197,15 @@ func (r *BookmarksAPIRoutes) sync(c *gin.Context) { } // Get Deleted Bookmarks - deletedBookmarks, err := r.deps.Database.GetDeletedBookmarks(c, filter) - if err != nil { - r.logger.WithError(err).Error("error getting bookmakrs") - response.SendInternalServerError(c) - return + var deletedBookmarks []int + + if len(payload.Ids) > 0 { + deletedBookmarks, err = r.deps.Database.GetDeletedBookmarks(c, filter) + if err != nil { + r.logger.WithError(err).Error("error getting bookmakrs") + response.SendInternalServerError(c) + return + } } // Create response using syncResponseMessage struct From ed0926c9e5b27efe5b366f417ccdb3c800277678 Mon Sep 17 00:00:00 2001 From: monirzadeh <25131576+Monirzadeh@users.noreply.github.com> Date: Mon, 16 Sep 2024 18:13:36 +0330 Subject: [PATCH 07/22] show deletd bookmark once and just in first page --- internal/http/routes/api/v1/bookmarks.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/internal/http/routes/api/v1/bookmarks.go b/internal/http/routes/api/v1/bookmarks.go index 8d016b346..340bf4576 100644 --- a/internal/http/routes/api/v1/bookmarks.go +++ b/internal/http/routes/api/v1/bookmarks.go @@ -199,7 +199,7 @@ func (r *BookmarksAPIRoutes) sync(c *gin.Context) { // Get Deleted Bookmarks var deletedBookmarks []int - if len(payload.Ids) > 0 { + if len(payload.Ids) > 0 && page == 1 { deletedBookmarks, err = r.deps.Database.GetDeletedBookmarks(c, filter) if err != nil { r.logger.WithError(err).Error("error getting bookmakrs") From 3b73f02f6774bb55e14fc8ea6f08debbf59cf1ff Mon Sep 17 00:00:00 2001 From: monirzadeh <25131576+Monirzadeh@users.noreply.github.com> Date: Mon, 16 Sep 2024 18:47:45 +0330 Subject: [PATCH 08/22] update filter for LastSync and deleted function for postgress --- internal/database/pg.go | 44 ++++++++++++++++++++++++++++++++++++++++- 1 file changed, 43 insertions(+), 1 deletion(-) diff --git a/internal/database/pg.go b/internal/database/pg.go index 1af2fbd52..6275ca95e 100644 --- a/internal/database/pg.go +++ b/internal/database/pg.go @@ -265,6 +265,37 @@ func (db *PGDatabase) SaveBookmarks(ctx context.Context, create bool, bookmarks // GetDeletedBookmarks fetch list of bookmark that deleted from database. func (db *PGDatabase) GetDeletedBookmarks(ctx context.Context, opts GetBookmarksOptions) ([]int, error) { var missingIDs []int + + // Construct the query using UNION ALL to create a temporary table of IDs + var unionQueries []string + for _, id := range opts.IsDeleted { + unionQueries = append(unionQueries, fmt.Sprintf("SELECT %d AS id", id)) + } + unionQuery := strings.Join(unionQueries, " UNION ALL ") + + query := fmt.Sprintf("SELECT temp.id FROM (%s) AS temp LEFT JOIN bookmark ON temp.id = bookmark.id WHERE bookmark.id IS NULL", unionQuery) + + // Execute the query + rows, err := db.QueryContext(ctx, query) + if err != nil { + return nil, err + } + defer rows.Close() + + // Scan the results into missingIDs + for rows.Next() { + var id int + if err := rows.Scan(&id); err != nil { + return nil, err + } + missingIDs = append(missingIDs, id) + } + + // Check for errors from iterating over rows + if err := rows.Err(); err != nil { + return nil, err + } + return missingIDs, nil } @@ -298,6 +329,12 @@ func (db *PGDatabase) GetBookmarks(ctx context.Context, opts GetBookmarksOptions arg["ids"] = opts.IDs } + // Add where clause for LastSync + if opts.LastSync != "" { + query += ` AND modified_at >= :last_sync` + arg["last_sync"] = opts.LastSync + } + // Add where clause for search keyword if opts.Keyword != "" { query += ` AND ( @@ -432,6 +469,12 @@ func (db *PGDatabase) GetBookmarksCount(ctx context.Context, opts GetBookmarksOp arg["ids"] = opts.IDs } + // Add where clause for LastSync + if opts.LastSync != "" { + query += ` AND modified_at >= :last_sync` + arg["last_sync"] = opts.LastSync + } + // Add where clause for search keyword if opts.Keyword != "" { query += ` AND ( @@ -616,7 +659,6 @@ func (db *PGDatabase) SaveAccount(ctx context.Context, account model.Account) (e // SaveAccountSettings update settings for specific account in database. Returns error if any happened func (db *PGDatabase) SaveAccountSettings(ctx context.Context, account model.Account) (err error) { - // Insert account to database _, err = db.ExecContext(ctx, `UPDATE account SET config = $1 From 760c9204561b6735d56fbeb3673f97005b63b637 Mon Sep 17 00:00:00 2001 From: monirzadeh <25131576+Monirzadeh@users.noreply.github.com> Date: Mon, 16 Sep 2024 20:00:01 +0330 Subject: [PATCH 09/22] test autentication for sync endpoint --- internal/http/routes/api/v1/bookmarks_test.go | 28 +++++++++++++++++++ 1 file changed, 28 insertions(+) diff --git a/internal/http/routes/api/v1/bookmarks_test.go b/internal/http/routes/api/v1/bookmarks_test.go index 7af902565..f597459d9 100644 --- a/internal/http/routes/api/v1/bookmarks_test.go +++ b/internal/http/routes/api/v1/bookmarks_test.go @@ -93,5 +93,33 @@ func TestReadableeBookmarkContent(t *testing.T) { require.Equal(t, response, w.Body.String()) require.Equal(t, http.StatusOK, w.Code) }) +} + +func TestSync(t *testing.T) { + logger := logrus.New() + ctx := context.TODO() + g := gin.New() + + _, deps := testutil.GetTestConfigurationAndDependencies(t, ctx, logger) + g.Use(middleware.AuthMiddleware(deps)) + + router := NewBookmarksAPIRoutes(logger, deps) + router.Setup(g.Group("/")) + + account := model.Account{ + Username: "test", + Password: "test", + Owner: false, + } + require.NoError(t, deps.Database.SaveAccount(ctx, account)) + + bookmark := testutil.GetValidBookmark() + _, err := deps.Database.SaveBookmarks(ctx, true, *bookmark) + require.NoError(t, err) + + t.Run("require authentication", func(t *testing.T) { + w := testutil.PerformRequest(g, "PUT", "/sync") + require.Equal(t, http.StatusUnauthorized, w.Code) + }) } From 246a6d1d884900ddcb2845d36205bdd45ba42c1c Mon Sep 17 00:00:00 2001 From: monirzadeh <25131576+Monirzadeh@users.noreply.github.com> Date: Mon, 16 Sep 2024 20:04:32 +0330 Subject: [PATCH 10/22] fix method in unittest --- internal/http/routes/api/v1/bookmarks_test.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/internal/http/routes/api/v1/bookmarks_test.go b/internal/http/routes/api/v1/bookmarks_test.go index f597459d9..3f235cca2 100644 --- a/internal/http/routes/api/v1/bookmarks_test.go +++ b/internal/http/routes/api/v1/bookmarks_test.go @@ -119,7 +119,7 @@ func TestSync(t *testing.T) { require.NoError(t, err) t.Run("require authentication", func(t *testing.T) { - w := testutil.PerformRequest(g, "PUT", "/sync") + w := testutil.PerformRequest(g, "POST", "/sync") require.Equal(t, http.StatusUnauthorized, w.Code) }) } From a1c3bc3e135203ce8a7c862485e464230a98557e Mon Sep 17 00:00:00 2001 From: monirzadeh <25131576+Monirzadeh@users.noreply.github.com> Date: Mon, 16 Sep 2024 21:17:02 +0330 Subject: [PATCH 11/22] add unittest for invalid id in sync request --- internal/http/routes/api/v1/bookmarks_test.go | 27 ++++++++++++++++++- 1 file changed, 26 insertions(+), 1 deletion(-) diff --git a/internal/http/routes/api/v1/bookmarks_test.go b/internal/http/routes/api/v1/bookmarks_test.go index 3f235cca2..897592d25 100644 --- a/internal/http/routes/api/v1/bookmarks_test.go +++ b/internal/http/routes/api/v1/bookmarks_test.go @@ -2,6 +2,7 @@ package api_v1 import ( "context" + "encoding/json" "net/http" "testing" "time" @@ -113,13 +114,37 @@ func TestSync(t *testing.T) { Owner: false, } require.NoError(t, deps.Database.SaveAccount(ctx, account)) + token, err := deps.Domains.Auth.CreateTokenForAccount(&account, time.Now().Add(time.Minute)) + require.NoError(t, err) + payloadInvalidID := syncPayload{ + Ids: []int{0, -1}, + LastSync: 0, + Page: 1, + } + payloadJSON, err := json.Marshal(payloadInvalidID) + if err != nil { + logrus.Printf("can't create a valid json") + } bookmark := testutil.GetValidBookmark() - _, err := deps.Database.SaveBookmarks(ctx, true, *bookmark) + _, err = deps.Database.SaveBookmarks(ctx, true, *bookmark) require.NoError(t, err) t.Run("require authentication", func(t *testing.T) { w := testutil.PerformRequest(g, "POST", "/sync") require.Equal(t, http.StatusUnauthorized, w.Code) }) + + t.Run("get content but invalid id", func(t *testing.T) { + w := testutil.PerformRequest(g, "POST", "/sync", testutil.WithHeader(model.AuthorizationHeader, model.AuthorizationTokenType+" "+token), testutil.WithBody(string(payloadJSON))) + require.Equal(t, http.StatusBadRequest, w.Code) + + // Check the response body + var response map[string]interface{} + err := json.Unmarshal(w.Body.Bytes(), &response) + require.NoError(t, err, "failed to unmarshal response body") + + // Assert that the response message is as expected for 0 or negative id + require.Equal(t, "id should not be 0 or negative", response["message"]) + }) } From 4774b5a43a686ff86d5a855c333fcb0957c3f81a Mon Sep 17 00:00:00 2001 From: monirzadeh <25131576+Monirzadeh@users.noreply.github.com> Date: Mon, 16 Sep 2024 21:31:35 +0330 Subject: [PATCH 12/22] fix typo --- internal/http/routes/api/v1/bookmarks_test.go | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/internal/http/routes/api/v1/bookmarks_test.go b/internal/http/routes/api/v1/bookmarks_test.go index 897592d25..6ed1dc708 100644 --- a/internal/http/routes/api/v1/bookmarks_test.go +++ b/internal/http/routes/api/v1/bookmarks_test.go @@ -121,7 +121,7 @@ func TestSync(t *testing.T) { LastSync: 0, Page: 1, } - payloadJSON, err := json.Marshal(payloadInvalidID) + payloadJSONInvalidID, err := json.Marshal(payloadInvalidID) if err != nil { logrus.Printf("can't create a valid json") } @@ -135,8 +135,8 @@ func TestSync(t *testing.T) { require.Equal(t, http.StatusUnauthorized, w.Code) }) - t.Run("get content but invalid id", func(t *testing.T) { - w := testutil.PerformRequest(g, "POST", "/sync", testutil.WithHeader(model.AuthorizationHeader, model.AuthorizationTokenType+" "+token), testutil.WithBody(string(payloadJSON))) + t.Run("invalid id", func(t *testing.T) { + w := testutil.PerformRequest(g, "POST", "/sync", testutil.WithHeader(model.AuthorizationHeader, model.AuthorizationTokenType+" "+token), testutil.WithBody(string(payloadJSONInvalidID))) require.Equal(t, http.StatusBadRequest, w.Code) // Check the response body From 0f8eb2899283ed0c88249034a2f1ea68f45f08b6 Mon Sep 17 00:00:00 2001 From: monirzadeh <25131576+Monirzadeh@users.noreply.github.com> Date: Tue, 17 Sep 2024 12:58:56 +0330 Subject: [PATCH 13/22] unittest sync api return expected id --- internal/http/routes/api/v1/bookmarks_test.go | 54 ++++++++++++++++++- 1 file changed, 52 insertions(+), 2 deletions(-) diff --git a/internal/http/routes/api/v1/bookmarks_test.go b/internal/http/routes/api/v1/bookmarks_test.go index 6ed1dc708..630be1836 100644 --- a/internal/http/routes/api/v1/bookmarks_test.go +++ b/internal/http/routes/api/v1/bookmarks_test.go @@ -116,20 +116,40 @@ func TestSync(t *testing.T) { require.NoError(t, deps.Database.SaveAccount(ctx, account)) token, err := deps.Domains.Auth.CreateTokenForAccount(&account, time.Now().Add(time.Minute)) require.NoError(t, err) + + // all payloads need payloadInvalidID := syncPayload{ Ids: []int{0, -1}, LastSync: 0, Page: 1, } + + payloadValid := syncPayload{ + Ids: []int{}, + LastSync: 0, + Page: 1, + } + + // Json format of payloads payloadJSONInvalidID, err := json.Marshal(payloadInvalidID) if err != nil { logrus.Printf("can't create a valid json") } + payloadJSONValid, err := json.Marshal(payloadValid) + if err != nil { + logrus.Printf("can't create a valid json") + } - bookmark := testutil.GetValidBookmark() - _, err = deps.Database.SaveBookmarks(ctx, true, *bookmark) + // Add bookmarks to the database + bookmarkFirst := testutil.GetValidBookmark() + _, err = deps.Database.SaveBookmarks(ctx, true, *bookmarkFirst) require.NoError(t, err) + bookmarkSecond := testutil.GetValidBookmark() + _, err = deps.Database.SaveBookmarks(ctx, true, *bookmarkSecond) + require.NoError(t, err) + bookmarkSecond.Title = "second bookmark" + t.Run("require authentication", func(t *testing.T) { w := testutil.PerformRequest(g, "POST", "/sync") require.Equal(t, http.StatusUnauthorized, w.Code) @@ -147,4 +167,34 @@ func TestSync(t *testing.T) { // Assert that the response message is as expected for 0 or negative id require.Equal(t, "id should not be 0 or negative", response["message"]) }) + + t.Run("retun both bookmark with sync api", func(t *testing.T) { + w := testutil.PerformRequest(g, "POST", "/sync", testutil.WithHeader(model.AuthorizationHeader, model.AuthorizationTokenType+" "+token), testutil.WithBody(string(payloadJSONValid))) + require.Equal(t, http.StatusOK, w.Code) + + // Check the response body + var response map[string]interface{} + err := json.Unmarshal(w.Body.Bytes(), &response) + require.NoError(t, err, "failed to unmarshal response body") + + // Assert that the response message is as expected + require.Equal(t, true, response["ok"]) + + // Access the bookmarks + message := response["message"].(map[string]interface{}) + modified := message["modified"].(map[string]interface{}) + bookmarks := modified["bookmarks"].([]interface{}) + + // Check the IDs of the bookmarks + var ids []int + for _, bookmark := range bookmarks { + bookmarkMap := bookmark.(map[string]interface{}) + id := int(bookmarkMap["id"].(float64)) + ids = append(ids, id) + } + + // Assert that the IDs are as expected + expectedIDs := []int{1, 2} + require.ElementsMatch(t, expectedIDs, ids, "bookmark IDs do not match") + }) } From 03df459a7fcbf47e016b02bdfce40c61cf2ae9c9 Mon Sep 17 00:00:00 2001 From: monirzadeh <25131576+Monirzadeh@users.noreply.github.com> Date: Tue, 17 Sep 2024 13:37:41 +0330 Subject: [PATCH 14/22] get correct modified bookmark in sync api --- internal/http/routes/api/v1/bookmarks_test.go | 26 ++++++++++--------- 1 file changed, 14 insertions(+), 12 deletions(-) diff --git a/internal/http/routes/api/v1/bookmarks_test.go b/internal/http/routes/api/v1/bookmarks_test.go index 630be1836..38c0b3b42 100644 --- a/internal/http/routes/api/v1/bookmarks_test.go +++ b/internal/http/routes/api/v1/bookmarks_test.go @@ -124,21 +124,11 @@ func TestSync(t *testing.T) { Page: 1, } - payloadValid := syncPayload{ - Ids: []int{}, - LastSync: 0, - Page: 1, - } - // Json format of payloads payloadJSONInvalidID, err := json.Marshal(payloadInvalidID) if err != nil { logrus.Printf("can't create a valid json") } - payloadJSONValid, err := json.Marshal(payloadValid) - if err != nil { - logrus.Printf("can't create a valid json") - } // Add bookmarks to the database bookmarkFirst := testutil.GetValidBookmark() @@ -146,10 +136,22 @@ func TestSync(t *testing.T) { require.NoError(t, err) bookmarkSecond := testutil.GetValidBookmark() + bookmarkSecond.Title = "second bookmark" + unixTimestampOneSecondLater := time.Now().UTC().Add(1 * time.Second).Unix() + bookmarkSecond.ModifiedAt = time.Unix(unixTimestampOneSecondLater, 0).UTC().Format(model.DatabaseDateFormat) _, err = deps.Database.SaveBookmarks(ctx, true, *bookmarkSecond) require.NoError(t, err) - bookmarkSecond.Title = "second bookmark" + payloadValid := syncPayload{ + Ids: []int{}, + LastSync: unixTimestampOneSecondLater, + Page: 1, + } + + payloadJSONValid, err := json.Marshal(payloadValid) + if err != nil { + logrus.Printf("can't create a valid json") + } t.Run("require authentication", func(t *testing.T) { w := testutil.PerformRequest(g, "POST", "/sync") require.Equal(t, http.StatusUnauthorized, w.Code) @@ -194,7 +196,7 @@ func TestSync(t *testing.T) { } // Assert that the IDs are as expected - expectedIDs := []int{1, 2} + expectedIDs := []int{2} require.ElementsMatch(t, expectedIDs, ids, "bookmark IDs do not match") }) } From 33f91a24199f56f86c4c8b83129750c43521a819 Mon Sep 17 00:00:00 2001 From: monirzadeh <25131576+Monirzadeh@users.noreply.github.com> Date: Tue, 17 Sep 2024 13:55:59 +0330 Subject: [PATCH 15/22] fix unittest discription --- internal/http/routes/api/v1/bookmarks_test.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/internal/http/routes/api/v1/bookmarks_test.go b/internal/http/routes/api/v1/bookmarks_test.go index 38c0b3b42..337d98f3a 100644 --- a/internal/http/routes/api/v1/bookmarks_test.go +++ b/internal/http/routes/api/v1/bookmarks_test.go @@ -170,7 +170,7 @@ func TestSync(t *testing.T) { require.Equal(t, "id should not be 0 or negative", response["message"]) }) - t.Run("retun both bookmark with sync api", func(t *testing.T) { + t.Run("retun just second bookmark with LastSync option sync api", func(t *testing.T) { w := testutil.PerformRequest(g, "POST", "/sync", testutil.WithHeader(model.AuthorizationHeader, model.AuthorizationTokenType+" "+token), testutil.WithBody(string(payloadJSONValid))) require.Equal(t, http.StatusOK, w.Code) From 92307dc344bbdba70645c5fb4d1e1e9b3d80d46a Mon Sep 17 00:00:00 2001 From: monirzadeh <25131576+Monirzadeh@users.noreply.github.com> Date: Wed, 18 Sep 2024 13:55:23 +0330 Subject: [PATCH 16/22] first database LastSync unittest --- internal/database/database_test.go | 35 ++++++++++++++++++++++++++++++ 1 file changed, 35 insertions(+) diff --git a/internal/database/database_test.go b/internal/database/database_test.go index e05034739..489e051b0 100644 --- a/internal/database/database_test.go +++ b/internal/database/database_test.go @@ -39,6 +39,8 @@ func testDatabase(t *testing.T, dbFactory testDatabaseFactory) { "testSaveAccountSetting": testSaveAccountSettings, "testGetAccount": testGetAccount, "testGetAccounts": testGetAccounts, + // Sync + "testSync": testSync, } for testName, testCase := range tests { @@ -516,3 +518,36 @@ func testGetBoomarksWithTimeFilters(t *testing.T, db DB) { // Second id should be 2 if order them by id assert.Equal(t, booksOrderById[1].ID, 2) } + +func testSync(t *testing.T, db DB) { + ctx := context.TODO() + + // First Bookmark + book1 := model.BookmarkDTO{ + URL: "https://github.com/go-shiori/shiori/one", + Title: "first bookmark", + } + + _, err := db.SaveBookmarks(ctx, true, book1) + assert.NoError(t, err, "Save bookmarks must not fail") + + // Second bookmark + unixTimestampOneSecondLater := time.Now().UTC().Add(2 * time.Second).Unix() + book2 := model.BookmarkDTO{ + URL: "https://github.com/go-shiori/shiori/second", + Title: "second bookmark", + ModifiedAt: time.Unix(unixTimestampOneSecondLater, 0).UTC().Format(model.DatabaseDateFormat), + } + + _, err = db.SaveBookmarks(ctx, true, book2) + assert.NoError(t, err, "Save bookmarks must not fail") + + t.Run("get correct bookmarks based on LastSync", func(t *testing.T) { + booksAfterSpecificDate, err := db.GetBookmarks(ctx, GetBookmarksOptions{ + LastSync: book2.ModifiedAt, + }) + assert.NoError(t, err, "Get bookmarks must not fail") + assert.Equal(t, booksAfterSpecificDate[0].ID, 2) + assert.Len(t, booksAfterSpecificDate, 1) + }) +} From 5eda5f6088139cbc1c1189ee2fab242d07c65e26 Mon Sep 17 00:00:00 2001 From: monirzadeh <25131576+Monirzadeh@users.noreply.github.com> Date: Tue, 24 Sep 2024 13:08:40 +0330 Subject: [PATCH 17/22] add mysql logic --- internal/database/mysql.go | 43 ++++++++++++++++++++++++++++++++++++++ internal/database/pg.go | 1 + 2 files changed, 44 insertions(+) diff --git a/internal/database/mysql.go b/internal/database/mysql.go index 4237dad9f..41d491eb5 100644 --- a/internal/database/mysql.go +++ b/internal/database/mysql.go @@ -284,6 +284,37 @@ func (db *MySQLDatabase) SaveBookmarks(ctx context.Context, create bool, bookmar func (db *MySQLDatabase) GetDeletedBookmarks(ctx context.Context, opts GetBookmarksOptions) ([]int, error) { var missingIDs []int + + // Construct the query using UNION ALL to create a temporary table of IDs + var unionQueries []string + for _, id := range opts.IsDeleted { + unionQueries = append(unionQueries, fmt.Sprintf("SELECT %d AS id", id)) + } + unionQuery := strings.Join(unionQueries, " UNION ALL ") + + query := fmt.Sprintf("SELECT temp.id FROM (%s) AS temp LEFT JOIN bookmark ON temp.id = bookmark.id WHERE bookmark.id IS NULL", unionQuery) + + // Execute the query + rows, err := db.QueryContext(ctx, query) + if err != nil { + return nil, err + } + defer rows.Close() + + // Scan the results into missingIDs + for rows.Next() { + var id int + if err := rows.Scan(&id); err != nil { + return nil, err + } + missingIDs = append(missingIDs, id) + } + + // Check for errors from iterating over rows + if err := rows.Err(); err != nil { + return nil, err + } + return missingIDs, nil } @@ -317,6 +348,12 @@ func (db *MySQLDatabase) GetBookmarks(ctx context.Context, opts GetBookmarksOpti args = append(args, opts.IDs) } + // Add where clause for LastSync + if opts.LastSync != "" { + query += ` AND modified_at >= ?` + args = append(args, opts.LastSync) + } + // Add where clause for search keyword if opts.Keyword != "" { query += ` AND ( @@ -445,6 +482,12 @@ func (db *MySQLDatabase) GetBookmarksCount(ctx context.Context, opts GetBookmark args = append(args, opts.IDs) } + // Add where clause for LastSync + if opts.LastSync != "" { + query += ` AND modified_at >= ?` + args = append(args, opts.LastSync) + } + // Add where clause for search keyword if opts.Keyword != "" { query += ` AND ( diff --git a/internal/database/pg.go b/internal/database/pg.go index 6275ca95e..1e2d25b7c 100644 --- a/internal/database/pg.go +++ b/internal/database/pg.go @@ -659,6 +659,7 @@ func (db *PGDatabase) SaveAccount(ctx context.Context, account model.Account) (e // SaveAccountSettings update settings for specific account in database. Returns error if any happened func (db *PGDatabase) SaveAccountSettings(ctx context.Context, account model.Account) (err error) { + // Insert account to database _, err = db.ExecContext(ctx, `UPDATE account SET config = $1 From 4322edbda243b8b062d31630d97e4898426227c3 Mon Sep 17 00:00:00 2001 From: monirzadeh <25131576+Monirzadeh@users.noreply.github.com> Date: Tue, 24 Sep 2024 14:22:13 +0330 Subject: [PATCH 18/22] unittest for deleted bookmark --- internal/http/routes/api/v1/bookmarks_test.go | 46 +++++++++++++++++++ 1 file changed, 46 insertions(+) diff --git a/internal/http/routes/api/v1/bookmarks_test.go b/internal/http/routes/api/v1/bookmarks_test.go index 337d98f3a..14795f8fa 100644 --- a/internal/http/routes/api/v1/bookmarks_test.go +++ b/internal/http/routes/api/v1/bookmarks_test.go @@ -148,10 +148,21 @@ func TestSync(t *testing.T) { Page: 1, } + payloadValidWithIDs := syncPayload{ + Ids: []int{3, 2}, + LastSync: unixTimestampOneSecondLater, + Page: 1, + } + payloadJSONValid, err := json.Marshal(payloadValid) if err != nil { logrus.Printf("can't create a valid json") } + payloadJSONValidWithIDs, err := json.Marshal(payloadValidWithIDs) + if err != nil { + logrus.Printf("can't create a valid json") + } + t.Run("require authentication", func(t *testing.T) { w := testutil.PerformRequest(g, "POST", "/sync") require.Equal(t, http.StatusUnauthorized, w.Code) @@ -184,6 +195,39 @@ func TestSync(t *testing.T) { // Access the bookmarks message := response["message"].(map[string]interface{}) + deleted := message["deleted"] + modified := message["modified"].(map[string]interface{}) + bookmarks := modified["bookmarks"].([]interface{}) + + // Check the IDs of the bookmarks + var ids []int + for _, bookmark := range bookmarks { + bookmarkMap := bookmark.(map[string]interface{}) + id := int(bookmarkMap["id"].(float64)) + ids = append(ids, id) + } + + // Assert that the IDs are as expected + expectedIDs := []int{2} + require.ElementsMatch(t, expectedIDs, ids, "bookmark IDs do not match") + require.Nil(t, deleted, "deleted bookmark is not nil") + }) + + t.Run("retun deleted bookmark", func(t *testing.T) { + w := testutil.PerformRequest(g, "POST", "/sync", testutil.WithHeader(model.AuthorizationHeader, model.AuthorizationTokenType+" "+token), testutil.WithBody(string(payloadJSONValid))) + require.Equal(t, http.StatusOK, w.Code) + + // Check the response body + var response map[string]interface{} + err := json.Unmarshal(w.Body.Bytes(), &response) + require.NoError(t, err, "failed to unmarshal response body") + + // Assert that the response message is as expected + require.Equal(t, true, response["ok"]) + + // Access the bookmarks + message := response["message"].(map[string]interface{}) + deleted := message["deleted"] modified := message["modified"].(map[string]interface{}) bookmarks := modified["bookmarks"].([]interface{}) @@ -197,6 +241,8 @@ func TestSync(t *testing.T) { // Assert that the IDs are as expected expectedIDs := []int{2} + deletedIDs := []int{3} require.ElementsMatch(t, expectedIDs, ids, "bookmark IDs do not match") + require.ElementsMatch(t, deletedIDs, deleted, "deleted bookmark IDs do not match") }) } From d76009d1a17a706bc201c945b00e8fe469241f4a Mon Sep 17 00:00:00 2001 From: monirzadeh <25131576+Monirzadeh@users.noreply.github.com> Date: Tue, 24 Sep 2024 14:25:58 +0330 Subject: [PATCH 19/22] use correct payload --- internal/http/routes/api/v1/bookmarks_test.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/internal/http/routes/api/v1/bookmarks_test.go b/internal/http/routes/api/v1/bookmarks_test.go index 14795f8fa..122e71b7d 100644 --- a/internal/http/routes/api/v1/bookmarks_test.go +++ b/internal/http/routes/api/v1/bookmarks_test.go @@ -214,7 +214,7 @@ func TestSync(t *testing.T) { }) t.Run("retun deleted bookmark", func(t *testing.T) { - w := testutil.PerformRequest(g, "POST", "/sync", testutil.WithHeader(model.AuthorizationHeader, model.AuthorizationTokenType+" "+token), testutil.WithBody(string(payloadJSONValid))) + w := testutil.PerformRequest(g, "POST", "/sync", testutil.WithHeader(model.AuthorizationHeader, model.AuthorizationTokenType+" "+token), testutil.WithBody(string(payloadJSONValidWithIDs))) require.Equal(t, http.StatusOK, w.Code) // Check the response body From 01cb7ef2b007d3a51c3ffdb5ac06efcc6fc6df89 Mon Sep 17 00:00:00 2001 From: monirzadeh <25131576+Monirzadeh@users.noreply.github.com> Date: Tue, 24 Sep 2024 14:36:03 +0330 Subject: [PATCH 20/22] fix deleted bookamrks in unittest --- internal/http/routes/api/v1/bookmarks_test.go | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/internal/http/routes/api/v1/bookmarks_test.go b/internal/http/routes/api/v1/bookmarks_test.go index 122e71b7d..bc793b4a5 100644 --- a/internal/http/routes/api/v1/bookmarks_test.go +++ b/internal/http/routes/api/v1/bookmarks_test.go @@ -149,7 +149,7 @@ func TestSync(t *testing.T) { } payloadValidWithIDs := syncPayload{ - Ids: []int{3, 2}, + Ids: []int{3, 2, 7}, LastSync: unixTimestampOneSecondLater, Page: 1, } @@ -227,7 +227,7 @@ func TestSync(t *testing.T) { // Access the bookmarks message := response["message"].(map[string]interface{}) - deleted := message["deleted"] + deleted := message["deleted"].([]interface{}) modified := message["modified"].(map[string]interface{}) bookmarks := modified["bookmarks"].([]interface{}) @@ -238,11 +238,17 @@ func TestSync(t *testing.T) { id := int(bookmarkMap["id"].(float64)) ids = append(ids, id) } + // Convert deleted IDs to int + var deletedIDs []int + for _, del := range deleted { + deletedID := int(del.(float64)) // Convert each deleted ID to int + deletedIDs = append(deletedIDs, deletedID) + } // Assert that the IDs are as expected expectedIDs := []int{2} - deletedIDs := []int{3} + expectedDeletedIDs := []int{3, 7} require.ElementsMatch(t, expectedIDs, ids, "bookmark IDs do not match") - require.ElementsMatch(t, deletedIDs, deleted, "deleted bookmark IDs do not match") + require.ElementsMatch(t, expectedDeletedIDs, deletedIDs, "deleted bookmark IDs do not match") }) } From e16ea205410d8472003bec25d1fad9ded46098d5 Mon Sep 17 00:00:00 2001 From: monirzadeh <25131576+Monirzadeh@users.noreply.github.com> Date: Tue, 24 Sep 2024 14:49:25 +0330 Subject: [PATCH 21/22] deletedBookarksIDs unittest --- internal/database/database_test.go | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/internal/database/database_test.go b/internal/database/database_test.go index 489e051b0..249b4c296 100644 --- a/internal/database/database_test.go +++ b/internal/database/database_test.go @@ -550,4 +550,13 @@ func testSync(t *testing.T, db DB) { assert.Equal(t, booksAfterSpecificDate[0].ID, 2) assert.Len(t, booksAfterSpecificDate, 1) }) + + t.Run("get deleted bookmarks id", func(t *testing.T) { + deletedBookarksIDs, err := db.GetDeletedBookmarks(ctx, GetBookmarksOptions{ + IsDeleted: []int{1, 5, 10}, + }) + assert.NoError(t, err, "Get deleted bookmarks must not fail") + assert.Equal(t, deletedBookarksIDs, []int{5, 10}) + assert.Len(t, deletedBookarksIDs, 2) + }) } From 16bdf2e26f7a10c00507c5b9f5bb32a3e1873677 Mon Sep 17 00:00:00 2001 From: monirzadeh <25131576+Monirzadeh@users.noreply.github.com> Date: Thu, 26 Sep 2024 17:38:20 +0330 Subject: [PATCH 22/22] add missing swagger payload --- docs/swagger/docs.go | 31 ++++++++++++++++++++++++ docs/swagger/swagger.json | 31 ++++++++++++++++++++++++ docs/swagger/swagger.yaml | 21 ++++++++++++++++ internal/http/routes/api/v1/bookmarks.go | 1 + 4 files changed, 84 insertions(+) diff --git a/docs/swagger/docs.go b/docs/swagger/docs.go index fec4ad01e..f81f9da5e 100644 --- a/docs/swagger/docs.go +++ b/docs/swagger/docs.go @@ -190,6 +190,17 @@ const docTemplate = `{ "Auth" ], "summary": "Get List of bookmark and last time of sync response bookmark change after that time and deleted bookmark.", + "parameters": [ + { + "description": "Bookmarks id in client side and last sync timestamp and page for pagination", + "name": "payload", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/api_v1.syncPayload" + } + } + ], "responses": { "200": { "description": "OK", @@ -368,6 +379,26 @@ const docTemplate = `{ } } }, + "api_v1.syncPayload": { + "type": "object", + "required": [ + "ids" + ], + "properties": { + "ids": { + "type": "array", + "items": { + "type": "integer" + } + }, + "last_sync": { + "type": "integer" + }, + "page": { + "type": "integer" + } + } + }, "api_v1.syncResponseMessage": { "type": "object", "properties": { diff --git a/docs/swagger/swagger.json b/docs/swagger/swagger.json index a3999de5c..50ffcbcc4 100644 --- a/docs/swagger/swagger.json +++ b/docs/swagger/swagger.json @@ -179,6 +179,17 @@ "Auth" ], "summary": "Get List of bookmark and last time of sync response bookmark change after that time and deleted bookmark.", + "parameters": [ + { + "description": "Bookmarks id in client side and last sync timestamp and page for pagination", + "name": "payload", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/api_v1.syncPayload" + } + } + ], "responses": { "200": { "description": "OK", @@ -357,6 +368,26 @@ } } }, + "api_v1.syncPayload": { + "type": "object", + "required": [ + "ids" + ], + "properties": { + "ids": { + "type": "array", + "items": { + "type": "integer" + } + }, + "last_sync": { + "type": "integer" + }, + "page": { + "type": "integer" + } + } + }, "api_v1.syncResponseMessage": { "type": "object", "properties": { diff --git a/docs/swagger/swagger.yaml b/docs/swagger/swagger.yaml index c92f7efe2..f9c547c3e 100644 --- a/docs/swagger/swagger.yaml +++ b/docs/swagger/swagger.yaml @@ -61,6 +61,19 @@ definitions: config: $ref: '#/definitions/model.UserConfig' type: object + api_v1.syncPayload: + properties: + ids: + items: + type: integer + type: array + last_sync: + type: integer + page: + type: integer + required: + - ids + type: object api_v1.syncResponseMessage: properties: deleted: @@ -279,6 +292,14 @@ paths: - Auth /api/v1/bookmarks/sync: post: + parameters: + - description: Bookmarks id in client side and last sync timestamp and page + for pagination + in: body + name: payload + required: true + schema: + $ref: '#/definitions/api_v1.syncPayload' produces: - application/json responses: diff --git a/internal/http/routes/api/v1/bookmarks.go b/internal/http/routes/api/v1/bookmarks.go index 340bf4576..73fc09488 100644 --- a/internal/http/routes/api/v1/bookmarks.go +++ b/internal/http/routes/api/v1/bookmarks.go @@ -149,6 +149,7 @@ type syncResponseMessage struct { // @Summary Get List of bookmark and last time of sync response bookmark change after that time and deleted bookmark. // @Tags Auth // @securityDefinitions.apikey ApiKeyAuth +// @Param payload body syncPayload true "Bookmarks id in client side and last sync timestamp and page for pagination"` // @Produce json // @Success 200 {object} syncResponseMessage // @Failure 403 {object} nil "Token not provided/invalid"