diff --git a/backend/lynx/lynx.go b/backend/lynx/lynx.go index b6ac17a..a9f6b55 100644 --- a/backend/lynx/lynx.go +++ b/backend/lynx/lynx.go @@ -1,7 +1,9 @@ package lynx import ( + "log" "net/http" + "time" "github.com/labstack/echo/v5" "github.com/pocketbase/pocketbase/apis" @@ -25,4 +27,24 @@ func InitializePocketbase(app core.App) { return nil }) + + // Automatically update last_viewed_at when links are loaded + // individually. However, let the client control this behavior + // with a header. + app.OnRecordViewRequest("links").Add(func(e *core.RecordViewEvent) error { + updateHeader := e.HttpContext.Request().Header.Get("X-Lynx-Update-Last-Viewed") + if updateHeader != "true" { + return nil + } + + e.Record.Set("last_viewed_at", time.Now().UTC().Format(time.RFC3339)) + + err := app.Dao().SaveRecord(e.Record) + if err != nil { + log.Printf("Failed to update last_viewed_at: %v", err) + return err + } + + return nil + }) } diff --git a/backend/lynx/lynx_test.go b/backend/lynx/lynx_test.go index b3fa337..83b1516 100644 --- a/backend/lynx/lynx_test.go +++ b/backend/lynx/lynx_test.go @@ -4,6 +4,7 @@ import ( "net/http" "strings" "testing" + "time" "github.com/labstack/echo/v5" "github.com/pocketbase/pocketbase/core" @@ -68,6 +69,70 @@ func TestHandleParseURL(t *testing.T) { } } +func TestOnRecordViewRequest(t *testing.T) { + setupTestApp := func(t *testing.T) *tests.TestApp { + testApp, err := tests.NewTestApp(testDataDir) + if err != nil { + t.Fatal(err) + } + + InitializePocketbase(testApp) + + return testApp + } + + scenarios := []tests.ApiScenario{ + { + Name: "View link without update header", + Method: http.MethodGet, + Url: "/api/collections/links/records/8n3iq8dt6vwi4ph", + RequestHeaders: map[string]string{ + "Authorization": generateRecordToken("users", "test2@example.com"), + }, + ExpectedStatus: 200, + ExpectedContent: []string{"8n3iq8dt6vwi4ph"}, + ExpectedEvents: map[string]int{ + "OnRecordViewRequest": 1, + }, + TestAppFactory: setupTestApp, + }, + { + Name: "View link with update header", + Method: http.MethodGet, + Url: "/api/collections/links/records/8n3iq8dt6vwi4ph", + RequestHeaders: map[string]string{ + "Authorization": generateRecordToken("users", "test2@example.com"), + "X-Lynx-Update-Last-Viewed": "true", + }, + ExpectedStatus: 200, + ExpectedContent: []string{"8n3iq8dt6vwi4ph"}, + ExpectedEvents: map[string]int{ + "OnRecordViewRequest": 1, + "OnModelBeforeUpdate": 1, + "OnModelAfterUpdate": 1, + }, + AfterTestFunc: func(t *testing.T, app *tests.TestApp, res *http.Response) { + record, err := app.Dao().FindRecordById("links", "8n3iq8dt6vwi4ph") + if err != nil { + t.Fatal(err) + } + lastViewedAt := record.GetDateTime("last_viewed_at") + if lastViewedAt.IsZero() { + t.Fatal("last_viewed_at was not updated") + } + if time.Since(lastViewedAt.Time()) > time.Minute { + t.Fatal("last_viewed_at was not updated recently") + } + }, + TestAppFactory: setupTestApp, + }, + } + + for _, scenario := range scenarios { + scenario.Test(t) + } +} + func generateRecordToken(collectionNameOrId string, email string) string { app, err := tests.NewTestApp(testDataDir) if err != nil { diff --git a/backend/test_pb_data/README.md b/backend/test_pb_data/README.md index a7052d0..3316627 100644 --- a/backend/test_pb_data/README.md +++ b/backend/test_pb_data/README.md @@ -1,3 +1,4 @@ # Test data users: -- `test@example.com`: Basic user with no related models. \ No newline at end of file +- `test@example.com`: Basic user with no related models. +- `test2@example.com`: Basic user with a single link, ID `8n3iq8dt6vwi4ph` \ No newline at end of file diff --git a/backend/test_pb_data/data.db b/backend/test_pb_data/data.db index 1e79a1b..48fbcf1 100644 Binary files a/backend/test_pb_data/data.db and b/backend/test_pb_data/data.db differ diff --git a/backend/test_pb_data/logs.db b/backend/test_pb_data/logs.db index da31d4c..a2f8c7f 100644 Binary files a/backend/test_pb_data/logs.db and b/backend/test_pb_data/logs.db differ