mirror of
https://github.com/go-shiori/shiori.git
synced 2024-12-27 02:04:27 +08:00
feat: store created and modified time separately on database for bookmarks (#896)
* sqlite migrate script * create time just when bookmark added and modified update if change happen * show added and modified time in footer instead of header * add bun.lockb that missing * add migrate for postgres * add pg support of created time * change modifed to modifed_at and create to created_at in sqlite * change modifed to modifed_at and create to created_at in postgre * add created_at to mariadb * fix migration file names * better variable name and more clear code for add modified time if created and modified is not in same day * add unittest * add unittest to sure filters work as expected * index for created_at and modified_at * build new styles.css * update swagger documents * make styles * change Created and Modified to CreatedAt and ModifiedAt * fix missing Modified * fix typo * missing Modified * fix typo * make swagger * run tests parallel Co-authored-by: Felipe Martin <812088+fmartingr@users.noreply.github.com> * remove t.Parallel() * remove dayjs dependency and combine two function * better unittest name * fix typo * diffrnt footer style for login and content page * use class instead of id * back parallel * change duplicate url * remvoe run Parallel * make styles --------- Co-authored-by: Felipe Martin <812088+fmartingr@users.noreply.github.com>
This commit is contained in:
parent
a3d4a687aa
commit
4a5564d60b
26 changed files with 284 additions and 99 deletions
|
@ -389,6 +389,9 @@ const docTemplate = `{
|
|||
"description": "TODO: migrate outside the DTO",
|
||||
"type": "boolean"
|
||||
},
|
||||
"createdAt": {
|
||||
"type": "string"
|
||||
},
|
||||
"excerpt": {
|
||||
"type": "string"
|
||||
},
|
||||
|
@ -410,7 +413,7 @@ const docTemplate = `{
|
|||
"imageURL": {
|
||||
"type": "string"
|
||||
},
|
||||
"modified": {
|
||||
"modifiedAt": {
|
||||
"type": "string"
|
||||
},
|
||||
"public": {
|
||||
|
|
|
@ -378,6 +378,9 @@
|
|||
"description": "TODO: migrate outside the DTO",
|
||||
"type": "boolean"
|
||||
},
|
||||
"createdAt": {
|
||||
"type": "string"
|
||||
},
|
||||
"excerpt": {
|
||||
"type": "string"
|
||||
},
|
||||
|
@ -399,7 +402,7 @@
|
|||
"imageURL": {
|
||||
"type": "string"
|
||||
},
|
||||
"modified": {
|
||||
"modifiedAt": {
|
||||
"type": "string"
|
||||
},
|
||||
"public": {
|
||||
|
|
|
@ -90,6 +90,8 @@ definitions:
|
|||
create_ebook:
|
||||
description: 'TODO: migrate outside the DTO'
|
||||
type: boolean
|
||||
createdAt:
|
||||
type: string
|
||||
excerpt:
|
||||
type: string
|
||||
hasArchive:
|
||||
|
@ -104,7 +106,7 @@ definitions:
|
|||
type: integer
|
||||
imageURL:
|
||||
type: string
|
||||
modified:
|
||||
modifiedAt:
|
||||
type: string
|
||||
public:
|
||||
type: integer
|
||||
|
|
|
@ -62,7 +62,7 @@ func exportHandler(cmd *cobra.Command, args []string) {
|
|||
|
||||
for _, book := range bookmarks {
|
||||
// Create Unix timestamp for bookmark
|
||||
modifiedTime, err := time.Parse(model.DatabaseDateFormat, book.Modified)
|
||||
modifiedTime, err := time.Parse(model.DatabaseDateFormat, book.ModifiedAt)
|
||||
if err != nil {
|
||||
modifiedTime = time.Now()
|
||||
}
|
||||
|
|
|
@ -136,10 +136,10 @@ func importHandler(cmd *cobra.Command, args []string) {
|
|||
|
||||
// Add item to list
|
||||
bookmark := model.BookmarkDTO{
|
||||
URL: url,
|
||||
Title: title,
|
||||
Tags: tags,
|
||||
Modified: modifiedDate.Format(model.DatabaseDateFormat),
|
||||
URL: url,
|
||||
Title: title,
|
||||
Tags: tags,
|
||||
ModifiedAt: modifiedDate.Format(model.DatabaseDateFormat),
|
||||
}
|
||||
|
||||
mapURL[url] = struct{}{}
|
||||
|
|
|
@ -94,10 +94,10 @@ func pocketHandler(cmd *cobra.Command, args []string) {
|
|||
|
||||
// Add item to list
|
||||
bookmark := model.BookmarkDTO{
|
||||
URL: url,
|
||||
Title: title,
|
||||
Modified: modified.Format(model.DatabaseDateFormat),
|
||||
Tags: tags,
|
||||
URL: url,
|
||||
Title: title,
|
||||
ModifiedAt: modified.Format(model.DatabaseDateFormat),
|
||||
Tags: tags,
|
||||
}
|
||||
|
||||
mapURL[url] = struct{}{}
|
||||
|
|
|
@ -120,6 +120,7 @@ func ProcessBookmark(deps *dependencies.Dependencies, req ProcessRequest) (book
|
|||
}
|
||||
|
||||
book.HasContent = book.Content != ""
|
||||
book.ModifiedAt = ""
|
||||
}
|
||||
|
||||
// Save article image to local disk
|
||||
|
@ -137,6 +138,7 @@ func ProcessBookmark(deps *dependencies.Dependencies, req ProcessRequest) (book
|
|||
}
|
||||
if err == nil {
|
||||
book.ImageURL = fp.Join("/", "bookmark", strID, "thumb")
|
||||
book.ModifiedAt = ""
|
||||
break
|
||||
}
|
||||
}
|
||||
|
@ -154,6 +156,7 @@ func ProcessBookmark(deps *dependencies.Dependencies, req ProcessRequest) (book
|
|||
return book, true, errors.Wrap(err, "failed to create ebook")
|
||||
}
|
||||
book.HasEbook = true
|
||||
book.ModifiedAt = ""
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -186,6 +189,7 @@ func ProcessBookmark(deps *dependencies.Dependencies, req ProcessRequest) (book
|
|||
}
|
||||
|
||||
book.HasArchive = true
|
||||
book.ModifiedAt = ""
|
||||
}
|
||||
|
||||
return book, false, nil
|
||||
|
|
|
@ -3,6 +3,7 @@ package database
|
|||
import (
|
||||
"context"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/go-shiori/shiori/internal/model"
|
||||
"github.com/stretchr/testify/assert"
|
||||
|
@ -15,19 +16,21 @@ type testDatabaseFactory func(t *testing.T, ctx context.Context) (DB, error)
|
|||
func testDatabase(t *testing.T, dbFactory testDatabaseFactory) {
|
||||
tests := map[string]databaseTestCase{
|
||||
// Bookmarks
|
||||
"testBookmarkAutoIncrement": testBookmarkAutoIncrement,
|
||||
"testCreateBookmark": testCreateBookmark,
|
||||
"testCreateBookmarkWithContent": testCreateBookmarkWithContent,
|
||||
"testCreateBookmarkTwice": testCreateBookmarkTwice,
|
||||
"testCreateBookmarkWithTag": testCreateBookmarkWithTag,
|
||||
"testCreateTwoDifferentBookmarks": testCreateTwoDifferentBookmarks,
|
||||
"testUpdateBookmark": testUpdateBookmark,
|
||||
"testUpdateBookmarkWithContent": testUpdateBookmarkWithContent,
|
||||
"testGetBookmark": testGetBookmark,
|
||||
"testGetBookmarkNotExistent": testGetBookmarkNotExistent,
|
||||
"testGetBookmarks": testGetBookmarks,
|
||||
"testGetBookmarksWithSQLCharacters": testGetBookmarksWithSQLCharacters,
|
||||
"testGetBookmarksCount": testGetBookmarksCount,
|
||||
"testBookmarkAutoIncrement": testBookmarkAutoIncrement,
|
||||
"testCreateBookmark": testCreateBookmark,
|
||||
"testCreateBookmarkWithContent": testCreateBookmarkWithContent,
|
||||
"testCreateBookmarkTwice": testCreateBookmarkTwice,
|
||||
"testCreateBookmarkWithTag": testCreateBookmarkWithTag,
|
||||
"testCreateTwoDifferentBookmarks": testCreateTwoDifferentBookmarks,
|
||||
"testUpdateBookmark": testUpdateBookmark,
|
||||
"testUpdateBookmarkUpdatesModifiedTime": testUpdateBookmarkUpdatesModifiedTime,
|
||||
"testGetBoomarksWithTimeFilters": testGetBoomarksWithTimeFilters,
|
||||
"testUpdateBookmarkWithContent": testUpdateBookmarkWithContent,
|
||||
"testGetBookmark": testGetBookmark,
|
||||
"testGetBookmarkNotExistent": testGetBookmarkNotExistent,
|
||||
"testGetBookmarks": testGetBookmarks,
|
||||
"testGetBookmarksWithSQLCharacters": testGetBookmarksWithSQLCharacters,
|
||||
"testGetBookmarksCount": testGetBookmarksCount,
|
||||
// Tags
|
||||
"testCreateTag": testCreateTag,
|
||||
"testCreateTags": testCreateTags,
|
||||
|
@ -424,3 +427,92 @@ func testGetAccounts(t *testing.T, db DB) {
|
|||
assert.Equal(t, 0, len(accounts))
|
||||
})
|
||||
}
|
||||
|
||||
// TODO: Consider using `t.Parallel()` once we have automated database tests spawning databases using testcontainers.
|
||||
func testUpdateBookmarkUpdatesModifiedTime(t *testing.T, db DB) {
|
||||
ctx := context.TODO()
|
||||
|
||||
book := model.BookmarkDTO{
|
||||
URL: "https://github.com/go-shiori/shiori",
|
||||
Title: "shiori",
|
||||
}
|
||||
|
||||
resultBook, err := db.SaveBookmarks(ctx, true, book)
|
||||
assert.NoError(t, err, "Save bookmarks must not fail")
|
||||
|
||||
updatedBook := resultBook[0]
|
||||
updatedBook.Title = "modified"
|
||||
updatedBook.ModifiedAt = ""
|
||||
|
||||
time.Sleep(1 * time.Second)
|
||||
resultUpdatedBooks, err := db.SaveBookmarks(ctx, false, updatedBook)
|
||||
assert.NoError(t, err, "Save bookmarks must not fail")
|
||||
|
||||
assert.NotEqual(t, resultBook[0].ModifiedAt, resultUpdatedBooks[0].ModifiedAt)
|
||||
assert.Equal(t, resultBook[0].CreatedAt, resultUpdatedBooks[0].CreatedAt)
|
||||
assert.Equal(t, resultBook[0].CreatedAt, resultBook[0].ModifiedAt)
|
||||
assert.NoError(t, err, "Get bookmarks must not fail")
|
||||
|
||||
assert.Equal(t, updatedBook.Title, resultUpdatedBooks[0].Title, "Saved bookmark must have updated Title")
|
||||
}
|
||||
|
||||
// TODO: Consider using `t.Parallel()` once we have automated database tests spawning databases using testcontainers.
|
||||
func testGetBoomarksWithTimeFilters(t *testing.T, db DB) {
|
||||
ctx := context.TODO()
|
||||
|
||||
book1 := model.BookmarkDTO{
|
||||
URL: "https://github.com/go-shiori/shiori/one",
|
||||
Title: "Added First but Modified Last",
|
||||
}
|
||||
book2 := model.BookmarkDTO{
|
||||
URL: "https://github.com/go-shiori/shiori/second",
|
||||
Title: "Added Last but Modified First",
|
||||
}
|
||||
|
||||
// create two new bookmark
|
||||
resultBook1, err := db.SaveBookmarks(ctx, true, book1)
|
||||
assert.NoError(t, err, "Save bookmarks must not fail")
|
||||
time.Sleep(1 * time.Second)
|
||||
resultBook2, err := db.SaveBookmarks(ctx, true, book2)
|
||||
assert.NoError(t, err, "Save bookmarks must not fail")
|
||||
|
||||
// update those bookmarks
|
||||
updatedBook1 := resultBook1[0]
|
||||
updatedBook1.Title = "Added First but Modified Last Updated Title"
|
||||
updatedBook1.ModifiedAt = ""
|
||||
|
||||
updatedBook2 := resultBook2[0]
|
||||
updatedBook2.Title = "Last Added but modified First Updated Title"
|
||||
updatedBook2.ModifiedAt = ""
|
||||
|
||||
// modified bookmark2 first after one second modified bookmark1
|
||||
resultUpdatedBook2, err := db.SaveBookmarks(ctx, false, updatedBook2)
|
||||
assert.NoError(t, err, "Save bookmarks must not fail")
|
||||
time.Sleep(1 * time.Second)
|
||||
resultUpdatedBook1, err := db.SaveBookmarks(ctx, false, updatedBook1)
|
||||
assert.NoError(t, err, "Save bookmarks must not fail")
|
||||
|
||||
// get diffrent filteter combination
|
||||
booksOrderByLastAdded, err := db.GetBookmarks(ctx, GetBookmarksOptions{
|
||||
IDs: []int{resultUpdatedBook1[0].ID, resultUpdatedBook2[0].ID},
|
||||
OrderMethod: 1,
|
||||
})
|
||||
assert.NoError(t, err, "Get bookmarks must not fail")
|
||||
booksOrderByLastModified, err := db.GetBookmarks(ctx, GetBookmarksOptions{
|
||||
IDs: []int{resultUpdatedBook1[0].ID, resultUpdatedBook2[0].ID},
|
||||
OrderMethod: 2,
|
||||
})
|
||||
assert.NoError(t, err, "Get bookmarks must not fail")
|
||||
booksOrderById, err := db.GetBookmarks(ctx, GetBookmarksOptions{
|
||||
IDs: []int{resultUpdatedBook1[0].ID, resultUpdatedBook2[0].ID},
|
||||
OrderMethod: 0,
|
||||
})
|
||||
assert.NoError(t, err, "Get bookmarks must not fail")
|
||||
|
||||
// Check Last Added
|
||||
assert.Equal(t, booksOrderByLastAdded[0].Title, updatedBook2.Title)
|
||||
// Check Last Modified
|
||||
assert.Equal(t, booksOrderByLastModified[0].Title, updatedBook1.Title)
|
||||
// Second id should be 2 if order them by id
|
||||
assert.Equal(t, booksOrderById[1].ID, 2)
|
||||
}
|
||||
|
|
|
@ -0,0 +1 @@
|
|||
ALTER TABLE bookmark RENAME COLUMN modified to created_at;
|
|
@ -0,0 +1,2 @@
|
|||
ALTER TABLE bookmark
|
||||
MODIFY created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP;
|
|
@ -0,0 +1,2 @@
|
|||
ALTER TABLE bookmark
|
||||
ADD COLUMN modified_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP;
|
|
@ -0,0 +1,3 @@
|
|||
UPDATE bookmark
|
||||
SET modified_at = COALESCE(created_at, CURRENT_TIMESTAMP)
|
||||
WHERE created_at IS NOT NULL;
|
|
@ -0,0 +1 @@
|
|||
CREATE INDEX idx_created_at ON bookmark (created_at);
|
|
@ -0,0 +1 @@
|
|||
CREATE INDEX idx_modified_at ON bookmark (modified_at);
|
|
@ -0,0 +1,16 @@
|
|||
-- Rename "modified" column to "created_at"
|
||||
ALTER TABLE bookmark
|
||||
RENAME COLUMN modified to created_at;
|
||||
|
||||
-- Add the "modified_at" column to the bookmark table
|
||||
ALTER TABLE bookmark
|
||||
ADD COLUMN modified_at TIMESTAMP(0) NOT NULL DEFAULT CURRENT_TIMESTAMP;
|
||||
|
||||
-- Update the "modified_at" column with the value from the "created_at" column if it is not null
|
||||
UPDATE bookmark
|
||||
SET modified_at = COALESCE(created_at, CURRENT_TIMESTAMP)
|
||||
WHERE created_at IS NOT NULL;
|
||||
|
||||
-- Index for "created_at" "modified_at""
|
||||
CREATE INDEX idx_created_at ON bookmark(created_at);
|
||||
CREATE INDEX idx_modified_at ON bookmark(modified_at);
|
12
internal/database/migrations/sqlite/0004_created_time.up.sql
Normal file
12
internal/database/migrations/sqlite/0004_created_time.up.sql
Normal file
|
@ -0,0 +1,12 @@
|
|||
ALTER TABLE bookmark
|
||||
RENAME COLUMN modified to created_at;
|
||||
|
||||
ALTER TABLE bookmark
|
||||
ADD COLUMN modified_at TEXT NULL;
|
||||
|
||||
UPDATE bookmark
|
||||
SET modified_at = bookmark.created_at
|
||||
WHERE created_at IS NOT NULL;
|
||||
|
||||
CREATE INDEX idx_created_at ON bookmark(created_at);
|
||||
CREATE INDEX idx_modified_at ON bookmark(modified_at);
|
|
@ -61,6 +61,12 @@ var mysqlMigrations = []migration{
|
|||
|
||||
return nil
|
||||
}),
|
||||
newFileMigration("0.7.0", "0.8.0", "mysql/0005_rename_to_created_at"),
|
||||
newFileMigration("0.8.0", "0.8.1", "mysql/0006_change_created_at_settings"),
|
||||
newFileMigration("0.8.1", "0.8.2", "mysql/0007_add_modified_at"),
|
||||
newFileMigration("0.8.2", "0.8.3", "mysql/0008_set_modified_at_equal_created_at"),
|
||||
newFileMigration("0.8.3", "0.8.4", "mysql/0009_index_for_created_at"),
|
||||
newFileMigration("0.8.4", "0.8.5", "mysql/0010_index_for_modified_at"),
|
||||
}
|
||||
|
||||
// MySQLDatabase is implementation of Database interface
|
||||
|
@ -131,8 +137,8 @@ func (db *MySQLDatabase) SaveBookmarks(ctx context.Context, create bool, bookmar
|
|||
if err := db.withTx(ctx, func(tx *sqlx.Tx) error {
|
||||
// Prepare statement
|
||||
stmtInsertBook, err := tx.Preparex(`INSERT INTO bookmark
|
||||
(url, title, excerpt, author, public, content, html, modified)
|
||||
VALUES(?, ?, ?, ?, ?, ?, ?, ?)`)
|
||||
(url, title, excerpt, author, public, content, html, modified_at, created_at)
|
||||
VALUES(?, ?, ?, ?, ?, ?, ?, ?, ?)`)
|
||||
if err != nil {
|
||||
return errors.WithStack(err)
|
||||
}
|
||||
|
@ -145,7 +151,7 @@ func (db *MySQLDatabase) SaveBookmarks(ctx context.Context, create bool, bookmar
|
|||
public = ?,
|
||||
content = ?,
|
||||
html = ?,
|
||||
modified = ?
|
||||
modified_at = ?
|
||||
WHERE id = ?`)
|
||||
if err != nil {
|
||||
return errors.WithStack(err)
|
||||
|
@ -189,17 +195,18 @@ func (db *MySQLDatabase) SaveBookmarks(ctx context.Context, create bool, bookmar
|
|||
}
|
||||
|
||||
// Set modified time
|
||||
if book.Modified == "" {
|
||||
book.Modified = modifiedTime
|
||||
if book.ModifiedAt == "" {
|
||||
book.ModifiedAt = modifiedTime
|
||||
}
|
||||
|
||||
// Save bookmark
|
||||
var err error
|
||||
if create {
|
||||
book.CreatedAt = modifiedTime
|
||||
var res sql.Result
|
||||
res, err = stmtInsertBook.ExecContext(ctx,
|
||||
book.URL, book.Title, book.Excerpt, book.Author,
|
||||
book.Public, book.Content, book.HTML, book.Modified)
|
||||
book.Public, book.Content, book.HTML, book.ModifiedAt, book.CreatedAt)
|
||||
if err != nil {
|
||||
return errors.WithStack(err)
|
||||
}
|
||||
|
@ -211,7 +218,7 @@ func (db *MySQLDatabase) SaveBookmarks(ctx context.Context, create bool, bookmar
|
|||
} else {
|
||||
_, err = stmtUpdateBook.ExecContext(ctx,
|
||||
book.URL, book.Title, book.Excerpt, book.Author,
|
||||
book.Public, book.Content, book.HTML, book.Modified, book.ID)
|
||||
book.Public, book.Content, book.HTML, book.ModifiedAt, book.ID)
|
||||
}
|
||||
if err != nil {
|
||||
return errors.WithStack(err)
|
||||
|
@ -285,7 +292,8 @@ func (db *MySQLDatabase) GetBookmarks(ctx context.Context, opts GetBookmarksOpti
|
|||
`excerpt`,
|
||||
`author`,
|
||||
`public`,
|
||||
`modified`,
|
||||
`created_at`,
|
||||
`modified_at`,
|
||||
`content <> "" has_content`}
|
||||
|
||||
if opts.WithContent {
|
||||
|
@ -371,7 +379,7 @@ func (db *MySQLDatabase) GetBookmarks(ctx context.Context, opts GetBookmarksOpti
|
|||
case ByLastAdded:
|
||||
query += ` ORDER BY id DESC`
|
||||
case ByLastModified:
|
||||
query += ` ORDER BY modified DESC`
|
||||
query += ` ORDER BY modified_at DESC`
|
||||
default:
|
||||
query += ` ORDER BY id`
|
||||
}
|
||||
|
@ -564,7 +572,7 @@ func (db *MySQLDatabase) GetBookmark(ctx context.Context, id int, url string) (m
|
|||
args := []interface{}{id}
|
||||
query := `SELECT
|
||||
id, url, title, excerpt, author, public,
|
||||
content, html, modified, content <> '' has_content
|
||||
content, html, modified_at, created_at, content <> '' has_content
|
||||
FROM bookmark WHERE id = ?`
|
||||
|
||||
if url != "" {
|
||||
|
|
|
@ -57,6 +57,7 @@ var postgresMigrations = []migration{
|
|||
|
||||
return nil
|
||||
}),
|
||||
newFileMigration("0.3.0", "0.4.0", "postgres/0002_created_time"),
|
||||
}
|
||||
|
||||
// PGDatabase is implementation of Database interface
|
||||
|
@ -128,8 +129,8 @@ func (db *PGDatabase) SaveBookmarks(ctx context.Context, create bool, bookmarks
|
|||
if err := db.withTx(ctx, func(tx *sqlx.Tx) error {
|
||||
// Prepare statement
|
||||
stmtInsertBook, err := tx.Preparex(`INSERT INTO bookmark
|
||||
(url, title, excerpt, author, public, content, html, modified)
|
||||
VALUES($1, $2, $3, $4, $5, $6, $7, $8)
|
||||
(url, title, excerpt, author, public, content, html, modified_at, created_at)
|
||||
VALUES($1, $2, $3, $4, $5, $6, $7, $8, $9)
|
||||
RETURNING id`)
|
||||
if err != nil {
|
||||
return errors.WithStack(err)
|
||||
|
@ -143,7 +144,7 @@ func (db *PGDatabase) SaveBookmarks(ctx context.Context, create bool, bookmarks
|
|||
public = $5,
|
||||
content = $6,
|
||||
html = $7,
|
||||
modified = $8
|
||||
modified_at = $8
|
||||
WHERE id = $9`)
|
||||
if err != nil {
|
||||
return errors.WithStack(err)
|
||||
|
@ -187,20 +188,21 @@ func (db *PGDatabase) SaveBookmarks(ctx context.Context, create bool, bookmarks
|
|||
}
|
||||
|
||||
// Set modified time
|
||||
if book.Modified == "" {
|
||||
book.Modified = modifiedTime
|
||||
if book.ModifiedAt == "" {
|
||||
book.ModifiedAt = modifiedTime
|
||||
}
|
||||
|
||||
// Save bookmark
|
||||
var err error
|
||||
if create {
|
||||
book.CreatedAt = modifiedTime
|
||||
err = stmtInsertBook.QueryRowContext(ctx,
|
||||
book.URL, book.Title, book.Excerpt, book.Author,
|
||||
book.Public, book.Content, book.HTML, book.Modified).Scan(&book.ID)
|
||||
book.Public, book.Content, book.HTML, book.ModifiedAt, book.CreatedAt).Scan(&book.ID)
|
||||
} else {
|
||||
_, err = stmtUpdateBook.ExecContext(ctx,
|
||||
book.URL, book.Title, book.Excerpt, book.Author,
|
||||
book.Public, book.Content, book.HTML, book.Modified, book.ID)
|
||||
book.Public, book.Content, book.HTML, book.ModifiedAt, book.ID)
|
||||
}
|
||||
if err != nil {
|
||||
return errors.WithStack(err)
|
||||
|
@ -270,7 +272,8 @@ func (db *PGDatabase) GetBookmarks(ctx context.Context, opts GetBookmarksOptions
|
|||
`excerpt`,
|
||||
`author`,
|
||||
`public`,
|
||||
`modified`,
|
||||
`created_at`,
|
||||
`modified_at`,
|
||||
`content <> '' has_content`}
|
||||
|
||||
if opts.WithContent {
|
||||
|
@ -359,7 +362,7 @@ func (db *PGDatabase) GetBookmarks(ctx context.Context, opts GetBookmarksOptions
|
|||
case ByLastAdded:
|
||||
query += ` ORDER BY id DESC`
|
||||
case ByLastModified:
|
||||
query += ` ORDER BY modified DESC`
|
||||
query += ` ORDER BY modified_at DESC`
|
||||
default:
|
||||
query += ` ORDER BY id`
|
||||
}
|
||||
|
@ -570,7 +573,7 @@ func (db *PGDatabase) GetBookmark(ctx context.Context, id int, url string) (mode
|
|||
args := []interface{}{id}
|
||||
query := `SELECT
|
||||
id, url, title, excerpt, author, public,
|
||||
content, html, modified, content <> '' has_content
|
||||
content, html, modified_at, created_at, content <> '' has_content
|
||||
FROM bookmark WHERE id = $1`
|
||||
|
||||
if url != "" {
|
||||
|
|
|
@ -60,6 +60,7 @@ var sqliteMigrations = []migration{
|
|||
}),
|
||||
newFileMigration("0.3.0", "0.4.0", "sqlite/0002_denormalize_content"),
|
||||
newFileMigration("0.4.0", "0.5.0", "sqlite/0003_uniq_id"),
|
||||
newFileMigration("0.5.0", "0.6.0", "sqlite/0004_created_time"),
|
||||
}
|
||||
|
||||
// SQLiteDatabase is implementation of Database interface
|
||||
|
@ -127,15 +128,15 @@ func (db *SQLiteDatabase) SaveBookmarks(ctx context.Context, create bool, bookma
|
|||
// Prepare statement
|
||||
|
||||
stmtInsertBook, err := tx.PreparexContext(ctx, `INSERT INTO bookmark
|
||||
(url, title, excerpt, author, public, modified, has_content)
|
||||
VALUES(?, ?, ?, ?, ?, ?, ?) RETURNING id`)
|
||||
(url, title, excerpt, author, public, modified_at, has_content, created_at)
|
||||
VALUES(?, ?, ?, ?, ?, ?, ?, ?) RETURNING id`)
|
||||
if err != nil {
|
||||
return errors.WithStack(err)
|
||||
}
|
||||
|
||||
stmtUpdateBook, err := tx.PreparexContext(ctx, `UPDATE bookmark SET
|
||||
url = ?, title = ?, excerpt = ?, author = ?,
|
||||
public = ?, modified = ?, has_content = ?
|
||||
public = ?, modified_at = ?, has_content = ?
|
||||
WHERE id = ?`)
|
||||
if err != nil {
|
||||
return errors.WithStack(err)
|
||||
|
@ -193,8 +194,8 @@ func (db *SQLiteDatabase) SaveBookmarks(ctx context.Context, create bool, bookma
|
|||
}
|
||||
|
||||
// Set modified time
|
||||
if book.Modified == "" {
|
||||
book.Modified = modifiedTime
|
||||
if book.ModifiedAt == "" {
|
||||
book.ModifiedAt = modifiedTime
|
||||
}
|
||||
|
||||
hasContent := book.Content != ""
|
||||
|
@ -202,11 +203,12 @@ func (db *SQLiteDatabase) SaveBookmarks(ctx context.Context, create bool, bookma
|
|||
// Create or update bookmark
|
||||
var err error
|
||||
if create {
|
||||
book.CreatedAt = modifiedTime
|
||||
err = stmtInsertBook.QueryRowContext(ctx,
|
||||
book.URL, book.Title, book.Excerpt, book.Author, book.Public, book.Modified, hasContent).Scan(&book.ID)
|
||||
book.URL, book.Title, book.Excerpt, book.Author, book.Public, book.ModifiedAt, hasContent, book.CreatedAt).Scan(&book.ID)
|
||||
} else {
|
||||
_, err = stmtUpdateBook.ExecContext(ctx,
|
||||
book.URL, book.Title, book.Excerpt, book.Author, book.Public, book.Modified, hasContent, book.ID)
|
||||
book.URL, book.Title, book.Excerpt, book.Author, book.Public, book.ModifiedAt, hasContent, book.ID)
|
||||
}
|
||||
if err != nil {
|
||||
return errors.WithStack(err)
|
||||
|
@ -298,7 +300,8 @@ func (db *SQLiteDatabase) GetBookmarks(ctx context.Context, opts GetBookmarksOpt
|
|||
b.excerpt,
|
||||
b.author,
|
||||
b.public,
|
||||
b.modified,
|
||||
b.created_at,
|
||||
b.modified_at,
|
||||
b.has_content
|
||||
FROM bookmark b
|
||||
WHERE 1`
|
||||
|
@ -389,7 +392,7 @@ func (db *SQLiteDatabase) GetBookmarks(ctx context.Context, opts GetBookmarksOpt
|
|||
case ByLastAdded:
|
||||
query += ` ORDER BY b.id DESC`
|
||||
case ByLastModified:
|
||||
query += ` ORDER BY b.modified DESC`
|
||||
query += ` ORDER BY b.modified_at DESC`
|
||||
default:
|
||||
query += ` ORDER BY b.id`
|
||||
}
|
||||
|
@ -669,8 +672,8 @@ func (db *SQLiteDatabase) DeleteBookmarks(ctx context.Context, ids ...int) error
|
|||
func (db *SQLiteDatabase) GetBookmark(ctx context.Context, id int, url string) (model.BookmarkDTO, bool, error) {
|
||||
args := []interface{}{id}
|
||||
query := `SELECT
|
||||
b.id, b.url, b.title, b.excerpt, b.author, b.public, b.modified,
|
||||
bc.content, bc.html, b.has_content
|
||||
b.id, b.url, b.title, b.excerpt, b.author, b.public, b.modified_at,
|
||||
bc.content, bc.html, b.has_content, b.created_at
|
||||
FROM bookmark b
|
||||
LEFT JOIN bookmark_content bc ON bc.docid = b.id
|
||||
WHERE b.id = ?`
|
||||
|
|
|
@ -14,7 +14,8 @@ type BookmarkDTO struct {
|
|||
Excerpt string `db:"excerpt" json:"excerpt"`
|
||||
Author string `db:"author" json:"author"`
|
||||
Public int `db:"public" json:"public"`
|
||||
Modified string `db:"modified" json:"modified"`
|
||||
CreatedAt string `db:"created_at" json:"createdAt"`
|
||||
ModifiedAt string `db:"modified_at" json:"modifiedAt"`
|
||||
Content string `db:"content" json:"-"`
|
||||
HTML string `db:"html" json:"html,omitempty"`
|
||||
ImageURL string `db:"image_url" json:"imageURL"`
|
||||
|
|
File diff suppressed because one or more lines are too long
1
internal/view/assets/js/dayjs.min.js
vendored
1
internal/view/assets/js/dayjs.min.js
vendored
File diff suppressed because one or more lines are too long
|
@ -5,10 +5,57 @@ select {
|
|||
color: var(--color);
|
||||
}
|
||||
|
||||
footer {
|
||||
.login-footer {
|
||||
color: var(--color);
|
||||
}
|
||||
|
||||
.content-footer {
|
||||
width: 100%;
|
||||
padding: 20px;
|
||||
max-width: 840px;
|
||||
margin-bottom: 16px;
|
||||
background-color: var(--contentBg);
|
||||
border: 1px solid var(--border);
|
||||
display: flex;
|
||||
flex-flow: column;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.metadata {
|
||||
display: flex;
|
||||
flex-flow: row wrap;
|
||||
text-align: center;
|
||||
font-size: 16px;
|
||||
color: var(--colorLink);
|
||||
&:nth-child(1) {
|
||||
justify-content: flex-start;
|
||||
}
|
||||
|
||||
&:nth-child(2) {
|
||||
justify-content: flex-end;
|
||||
}
|
||||
|
||||
&[v-cloak] {
|
||||
visibility: hidden;
|
||||
}
|
||||
}
|
||||
|
||||
.links {
|
||||
display: flex;
|
||||
flex-flow: row wrap;
|
||||
|
||||
a {
|
||||
padding: 0 4px;
|
||||
color: var(--color);
|
||||
text-decoration: underline;
|
||||
|
||||
&:hover,
|
||||
&:focus {
|
||||
color: var(--main);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
* {
|
||||
border-width: 0;
|
||||
box-sizing: border-box;
|
||||
|
@ -384,18 +431,6 @@ a {
|
|||
flex-flow: column;
|
||||
align-items: center;
|
||||
|
||||
#metadata {
|
||||
display: flex;
|
||||
flex-flow: row wrap;
|
||||
text-align: center;
|
||||
font-size: 16px;
|
||||
color: var(--colorLink);
|
||||
|
||||
&[v-cloak] {
|
||||
visibility: hidden;
|
||||
}
|
||||
}
|
||||
|
||||
#title {
|
||||
padding: 8px 0;
|
||||
grid-column-start: 1;
|
||||
|
@ -406,22 +441,6 @@ a {
|
|||
hyphens: none;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
#links {
|
||||
display: flex;
|
||||
flex-flow: row wrap;
|
||||
|
||||
a {
|
||||
padding: 0 4px;
|
||||
color: var(--color);
|
||||
text-decoration: underline;
|
||||
|
||||
&:hover,
|
||||
&:focus {
|
||||
color: var(--main);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#content {
|
||||
|
|
|
@ -16,16 +16,14 @@
|
|||
|
||||
<link href="assets/css/style.css" rel="stylesheet">
|
||||
|
||||
<script src="assets/js/dayjs.min.js"></script>
|
||||
<script src="assets/js/vue.min.js"></script>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<div id="content-scene">
|
||||
<div id="header">
|
||||
<p id="metadata" v-cloak>Added {{localtime()}}</p>
|
||||
<p id="title" dir="auto">$$.Book.Title$$</p>
|
||||
<div id="links">
|
||||
<div class="links">
|
||||
<a href="$$.Book.URL$$" target="_blank" rel="noopener noreferrer">View Original</a>
|
||||
$$if .Book.HasArchive$$
|
||||
<a href="bookmark/$$.Book.ID$$/archive">View Archive</a>
|
||||
|
@ -38,6 +36,9 @@
|
|||
<div id="content" dir="auto" v-pre>
|
||||
$$.HTML$$
|
||||
</div>
|
||||
<footer class="content-footer">
|
||||
<p class="metadata">{{ createdModifiedTime() }} </p>
|
||||
</footer>
|
||||
</div>
|
||||
|
||||
<script type="module">
|
||||
|
@ -48,15 +49,21 @@
|
|||
el: '#content-scene',
|
||||
mixins: [basePage],
|
||||
data: {
|
||||
modified: "$$.Book.Modified$$"
|
||||
created: "$$.Book.CreatedAt$$"
|
||||
},
|
||||
methods: {
|
||||
localtime() {
|
||||
var strTime = this.modified.replace(" ", "T");
|
||||
if (!strTime.endsWith("Z")) {
|
||||
strTime += "Z";
|
||||
}
|
||||
return dayjs(strTime).format("D MMMM YYYY, HH:mm:ss");
|
||||
createdModifiedTime() {
|
||||
const strCreatedTime = "$$.Book.CreatedAt$$".replace(" ", "T") + ("$$.Book.CreatedAt$$".endsWith("Z") ? "" : "Z");
|
||||
const strModifiedTime = "$$.Book.ModifiedAt$$".replace(" ", "T") + ("$$.Book.ModifiedAt$$".endsWith("Z") ? "" : "Z");
|
||||
|
||||
const createdDate = new Date(strCreatedTime);
|
||||
const modifiedDate = new Date(strModifiedTime);
|
||||
|
||||
if (createdDate.toDateString() === modifiedDate.toDateString()) {
|
||||
return `Added ${createdDate.getDate()} ${createdDate.toLocaleString('default', { month: 'long' })} ${createdDate.getFullYear()}`;
|
||||
} else {
|
||||
return `Added ${createdDate.getDate()} ${createdDate.toLocaleString('default', { month: 'long' })} ${createdDate.getFullYear()} | Last Modified ${modifiedDate.getDate()} ${modifiedDate.toLocaleString('default', {month: 'long'})} ${modifiedDate.getFullYear()}`;
|
||||
}
|
||||
},
|
||||
loadSetting() {
|
||||
var opts = JSON.parse(localStorage.getItem("shiori-account")) || {},
|
||||
|
|
|
@ -48,7 +48,7 @@
|
|||
</div>
|
||||
</form>
|
||||
</div>
|
||||
<footer>
|
||||
<footer class="login-footer">
|
||||
<p>$$.Version$$</p>
|
||||
</footer>
|
||||
</div>
|
||||
|
|
|
@ -360,6 +360,9 @@ func (h *Handler) ApiUpdateBookmark(w http.ResponseWriter, r *http.Request, ps h
|
|||
}
|
||||
}
|
||||
|
||||
// Set bookmark modified
|
||||
book.ModifiedAt = ""
|
||||
|
||||
// Update database
|
||||
res, err := h.DB.SaveBookmarks(ctx, false, book)
|
||||
checkError(err)
|
||||
|
|
Loading…
Reference in a new issue