Merge pull request #70 from bnema/refactor/dmenu-history-days
feat(dmenu): change history limit from count to days, add --most-visited flag
18 files · +1103 -20
M README.md +2 -1
| @@ -251,7 +251,8 @@ sudo apt install va-driver-all | |||
| 251 | 251 | - rofi: `dumber dmenu | rofi -dmenu -show-icons -p "🔍 " | dumber dmenu --select` | |
| 252 | 252 | - fuzzel: `dumber dmenu | fuzzel --dmenu -p "🔍 " | dumber dmenu --select` | |
| 253 | 253 | - `dumber dmenu --interactive` # built-in TUI fuzzy finder | |
| 254 | - | - `dumber dmenu --max 200` # limit output entries | |
| 254 | + | - `dumber dmenu --days 7` # show history from last 7 days (default: 30) | |
| 255 | + | - `dumber dmenu --most-visited` # sort by visit count instead of recency | |
| 255 | 256 | - Manage browsing history: | |
| 256 | 257 | - `dumber history` # interactive history browser (timeline tabs + fuzzy search) | |
| 257 | 258 | - `dumber history --json` # output recent entries as JSON | |
| @@ -251,7 +251,8 @@ sudo apt install va-driver-all | |||||
| 251 | - rofi: `dumber dmenu | rofi -dmenu -show-icons -p "🔍 " | dumber dmenu --select` | 251 | - rofi: `dumber dmenu | rofi -dmenu -show-icons -p "🔍 " | dumber dmenu --select` | ||
| 252 | - fuzzel: `dumber dmenu | fuzzel --dmenu -p "🔍 " | dumber dmenu --select` | 252 | - fuzzel: `dumber dmenu | fuzzel --dmenu -p "🔍 " | dumber dmenu --select` | ||
| 253 | - `dumber dmenu --interactive` # built-in TUI fuzzy finder | 253 | - `dumber dmenu --interactive` # built-in TUI fuzzy finder | ||
| 254 | - | - `dumber dmenu --max 200` # limit output entries | 254 | + | - `dumber dmenu --days 7` # show history from last 7 days (default: 30) |
| 255 | + | - `dumber dmenu --most-visited` # sort by visit count instead of recency | |||
| 255 | - Manage browsing history: | 256 | - Manage browsing history: | ||
| 256 | - `dumber history` # interactive history browser (timeline tabs + fuzzy search) | 257 | - `dumber history` # interactive history browser (timeline tabs + fuzzy search) | ||
| 257 | - `dumber history --json` # output recent entries as JSON | 258 | - `dumber history --json` # output recent entries as JSON | ||
M docs/CONFIG.md +10 -4
| @@ -83,14 +83,14 @@ These settings control the `dumber dmenu` CLI command for rofi/fuzzel integratio | |||
| 83 | 83 | ||
| 84 | 84 | | Key | Type | Default | Description | | |
| 85 | 85 | |-----|------|---------|-------------| | |
| 86 | - | | `dmenu.max_history_items` | int | `20` | Max items shown in launcher | | |
| 86 | + | | `dmenu.max_history_days` | int | `30` | Number of days of history to show (0 = all) | | |
| 87 | 87 | | `dmenu.show_visit_count` | bool | `true` | Show visit counts in output | | |
| 88 | 88 | | `dmenu.show_last_visited` | bool | `true` | Show last visited dates in output | | |
| 89 | 89 | | `dmenu.history_prefix` | string | `"🕒"` | Prefix for history items | | |
| 90 | 90 | | `dmenu.shortcut_prefix` | string | `"🔍"` | Prefix for shortcuts | | |
| 91 | 91 | | `dmenu.url_prefix` | string | `"🌐"` | Prefix for URLs | | |
| 92 | 92 | | `dmenu.date_format` | string | `"2006-01-02 15:04"` | Go time format string | | |
| 93 | - | | `dmenu.sort_by_visit_count` | bool | `true` | Sort by popularity | | |
| 93 | + | | `dmenu.sort_by_visit_count` | bool | `true` | Sort by visit count instead of recency | | |
| 94 | 94 | ||
| 95 | 95 | **CLI Usage:** | |
| 96 | 96 | ```bash | |
| @@ -101,8 +101,14 @@ dumber dmenu | fuzzel --dmenu -p "Browse: " | dumber dmenu --select | |||
| 101 | 101 | # Interactive TUI mode | |
| 102 | 102 | dumber dmenu --interactive | |
| 103 | 103 | ||
| 104 | - | # Override max items via CLI flag | |
| 105 | - | dumber dmenu --max 50 | |
| 104 | + | # Override history days via CLI flag (show last 7 days) | |
| 105 | + | dumber dmenu --days 7 | |
| 106 | + | ||
| 107 | + | # Sort by most visited instead of recency | |
| 108 | + | dumber dmenu --most-visited | |
| 109 | + | ||
| 110 | + | # Combined: most visited from last 14 days | |
| 111 | + | dumber dmenu --days 14 --most-visited | |
| 106 | 112 | ``` | |
| 107 | 113 | ||
| 108 | 114 | ## Omnibox | |
| @@ -83,14 +83,14 @@ These settings control the `dumber dmenu` CLI command for rofi/fuzzel integratio | |||||
| 83 | 83 | ||||
| 84 | | Key | Type | Default | Description | | 84 | | Key | Type | Default | Description | | ||
| 85 | |-----|------|---------|-------------| | 85 | |-----|------|---------|-------------| | ||
| 86 | - | | `dmenu.max_history_items` | int | `20` | Max items shown in launcher | | 86 | + | | `dmenu.max_history_days` | int | `30` | Number of days of history to show (0 = all) | |
| 87 | | `dmenu.show_visit_count` | bool | `true` | Show visit counts in output | | 87 | | `dmenu.show_visit_count` | bool | `true` | Show visit counts in output | | ||
| 88 | | `dmenu.show_last_visited` | bool | `true` | Show last visited dates in output | | 88 | | `dmenu.show_last_visited` | bool | `true` | Show last visited dates in output | | ||
| 89 | | `dmenu.history_prefix` | string | `"🕒"` | Prefix for history items | | 89 | | `dmenu.history_prefix` | string | `"🕒"` | Prefix for history items | | ||
| 90 | | `dmenu.shortcut_prefix` | string | `"🔍"` | Prefix for shortcuts | | 90 | | `dmenu.shortcut_prefix` | string | `"🔍"` | Prefix for shortcuts | | ||
| 91 | | `dmenu.url_prefix` | string | `"🌐"` | Prefix for URLs | | 91 | | `dmenu.url_prefix` | string | `"🌐"` | Prefix for URLs | | ||
| 92 | | `dmenu.date_format` | string | `"2006-01-02 15:04"` | Go time format string | | 92 | | `dmenu.date_format` | string | `"2006-01-02 15:04"` | Go time format string | | ||
| 93 | - | | `dmenu.sort_by_visit_count` | bool | `true` | Sort by popularity | | 93 | + | | `dmenu.sort_by_visit_count` | bool | `true` | Sort by visit count instead of recency | |
| 94 | 94 | ||||
| 95 | **CLI Usage:** | 95 | **CLI Usage:** | ||
| 96 | ```bash | 96 | ```bash | ||
| @@ -101,8 +101,14 @@ dumber dmenu | fuzzel --dmenu -p "Browse: " | dumber dmenu --select | |||||
| 101 | # Interactive TUI mode | 101 | # Interactive TUI mode | ||
| 102 | dumber dmenu --interactive | 102 | dumber dmenu --interactive | ||
| 103 | 103 | ||||
| 104 | - | # Override max items via CLI flag | 104 | + | # Override history days via CLI flag (show last 7 days) |
| 105 | - | dumber dmenu --max 50 | 105 | + | dumber dmenu --days 7 |
| 106 | + | ||||
| 107 | + | # Sort by most visited instead of recency | |||
| 108 | + | dumber dmenu --most-visited | |||
| 109 | + | ||||
| 110 | + | # Combined: most visited from last 14 days | |||
| 111 | + | dumber dmenu --days 14 --most-visited | |||
| 106 | ``` | 112 | ``` | ||
| 107 | 113 | ||||
| 108 | ## Omnibox | 114 | ## Omnibox | ||
M internal/application/usecase/search_history.go +50 -0
| @@ -75,6 +75,56 @@ func (uc *SearchHistoryUseCase) GetRecent(ctx context.Context, limit, offset int | |||
| 75 | 75 | return entries, nil | |
| 76 | 76 | } | |
| 77 | 77 | ||
| 78 | + | // GetRecentSince retrieves history entries visited within the last N days. | |
| 79 | + | // If days is 0, returns all history entries. | |
| 80 | + | // If days is negative, defaults to 30 days. | |
| 81 | + | func (uc *SearchHistoryUseCase) GetRecentSince(ctx context.Context, days int) ([]*entity.HistoryEntry, error) { | |
| 82 | + | if days < 0 { | |
| 83 | + | days = 30 // Default to 30 days for negative values | |
| 84 | + | } | |
| 85 | + | ||
| 86 | + | var entries []*entity.HistoryEntry | |
| 87 | + | var err error | |
| 88 | + | ||
| 89 | + | switch days { | |
| 90 | + | case 0: | |
| 91 | + | entries, err = uc.historyRepo.GetAllRecentHistory(ctx) | |
| 92 | + | default: | |
| 93 | + | entries, err = uc.historyRepo.GetRecentSince(ctx, days) | |
| 94 | + | } | |
| 95 | + | ||
| 96 | + | if err != nil { | |
| 97 | + | return nil, fmt.Errorf("failed to get recent history: %w", err) | |
| 98 | + | } | |
| 99 | + | ||
| 100 | + | return entries, nil | |
| 101 | + | } | |
| 102 | + | ||
| 103 | + | // GetMostVisited retrieves history entries sorted by visit count within the last N days. | |
| 104 | + | // If days is 0, returns all history entries sorted by visit count. | |
| 105 | + | // If days is negative, defaults to 30 days. | |
| 106 | + | func (uc *SearchHistoryUseCase) GetMostVisited(ctx context.Context, days int) ([]*entity.HistoryEntry, error) { | |
| 107 | + | if days < 0 { | |
| 108 | + | days = 30 // Default to 30 days for negative values | |
| 109 | + | } | |
| 110 | + | ||
| 111 | + | var entries []*entity.HistoryEntry | |
| 112 | + | var err error | |
| 113 | + | ||
| 114 | + | switch days { | |
| 115 | + | case 0: | |
| 116 | + | entries, err = uc.historyRepo.GetAllMostVisited(ctx) | |
| 117 | + | default: | |
| 118 | + | entries, err = uc.historyRepo.GetMostVisited(ctx, days) | |
| 119 | + | } | |
| 120 | + | ||
| 121 | + | if err != nil { | |
| 122 | + | return nil, fmt.Errorf("failed to get most visited history: %w", err) | |
| 123 | + | } | |
| 124 | + | ||
| 125 | + | return entries, nil | |
| 126 | + | } | |
| 127 | + | ||
| 78 | 128 | // FindByURL retrieves a history entry by its URL. | |
| 79 | 129 | func (uc *SearchHistoryUseCase) FindByURL(ctx context.Context, url string) (*entity.HistoryEntry, error) { | |
| 80 | 130 | log := logging.FromContext(ctx) | |
| @@ -75,6 +75,56 @@ func (uc *SearchHistoryUseCase) GetRecent(ctx context.Context, limit, offset int | |||||
| 75 | return entries, nil | 75 | return entries, nil | ||
| 76 | } | 76 | } | ||
| 77 | 77 | ||||
| 78 | + | // GetRecentSince retrieves history entries visited within the last N days. | |||
| 79 | + | // If days is 0, returns all history entries. | |||
| 80 | + | // If days is negative, defaults to 30 days. | |||
| 81 | + | func (uc *SearchHistoryUseCase) GetRecentSince(ctx context.Context, days int) ([]*entity.HistoryEntry, error) { | |||
| 82 | + | if days < 0 { | |||
| 83 | + | days = 30 // Default to 30 days for negative values | |||
| 84 | + | } | |||
| 85 | + | ||||
| 86 | + | var entries []*entity.HistoryEntry | |||
| 87 | + | var err error | |||
| 88 | + | ||||
| 89 | + | switch days { | |||
| 90 | + | case 0: | |||
| 91 | + | entries, err = uc.historyRepo.GetAllRecentHistory(ctx) | |||
| 92 | + | default: | |||
| 93 | + | entries, err = uc.historyRepo.GetRecentSince(ctx, days) | |||
| 94 | + | } | |||
| 95 | + | ||||
| 96 | + | if err != nil { | |||
| 97 | + | return nil, fmt.Errorf("failed to get recent history: %w", err) | |||
| 98 | + | } | |||
| 99 | + | ||||
| 100 | + | return entries, nil | |||
| 101 | + | } | |||
| 102 | + | ||||
| 103 | + | // GetMostVisited retrieves history entries sorted by visit count within the last N days. | |||
| 104 | + | // If days is 0, returns all history entries sorted by visit count. | |||
| 105 | + | // If days is negative, defaults to 30 days. | |||
| 106 | + | func (uc *SearchHistoryUseCase) GetMostVisited(ctx context.Context, days int) ([]*entity.HistoryEntry, error) { | |||
| 107 | + | if days < 0 { | |||
| 108 | + | days = 30 // Default to 30 days for negative values | |||
| 109 | + | } | |||
| 110 | + | ||||
| 111 | + | var entries []*entity.HistoryEntry | |||
| 112 | + | var err error | |||
| 113 | + | ||||
| 114 | + | switch days { | |||
| 115 | + | case 0: | |||
| 116 | + | entries, err = uc.historyRepo.GetAllMostVisited(ctx) | |||
| 117 | + | default: | |||
| 118 | + | entries, err = uc.historyRepo.GetMostVisited(ctx, days) | |||
| 119 | + | } | |||
| 120 | + | ||||
| 121 | + | if err != nil { | |||
| 122 | + | return nil, fmt.Errorf("failed to get most visited history: %w", err) | |||
| 123 | + | } | |||
| 124 | + | ||||
| 125 | + | return entries, nil | |||
| 126 | + | } | |||
| 127 | + | ||||
| 78 | // FindByURL retrieves a history entry by its URL. | 128 | // FindByURL retrieves a history entry by its URL. | ||
| 79 | func (uc *SearchHistoryUseCase) FindByURL(ctx context.Context, url string) (*entity.HistoryEntry, error) { | 129 | func (uc *SearchHistoryUseCase) FindByURL(ctx context.Context, url string) (*entity.HistoryEntry, error) { | ||
| 80 | log := logging.FromContext(ctx) | 130 | log := logging.FromContext(ctx) | ||
A internal/application/usecase/search_history_test.go +153 -0
| @@ -0,0 +1,153 @@ | |||
| 1 | + | package usecase_test | |
| 2 | + | ||
| 3 | + | import ( | |
| 4 | + | "errors" | |
| 5 | + | "testing" | |
| 6 | + | "time" | |
| 7 | + | ||
| 8 | + | "github.com/bnema/dumber/internal/application/usecase" | |
| 9 | + | "github.com/bnema/dumber/internal/domain/entity" | |
| 10 | + | repomocks "github.com/bnema/dumber/internal/domain/repository/mocks" | |
| 11 | + | "github.com/stretchr/testify/assert" | |
| 12 | + | "github.com/stretchr/testify/mock" | |
| 13 | + | "github.com/stretchr/testify/require" | |
| 14 | + | ) | |
| 15 | + | ||
| 16 | + | func TestSearchHistoryUseCase_GetRecentSince_ReturnsEntries(t *testing.T) { | |
| 17 | + | ctx := testContext() | |
| 18 | + | ||
| 19 | + | historyRepo := repomocks.NewMockHistoryRepository(t) | |
| 20 | + | ||
| 21 | + | entries := []*entity.HistoryEntry{ | |
| 22 | + | {ID: 1, URL: "https://example.com", Title: "Example", LastVisited: time.Now()}, | |
| 23 | + | {ID: 2, URL: "https://github.com", Title: "GitHub", LastVisited: time.Now().Add(-24 * time.Hour)}, | |
| 24 | + | } | |
| 25 | + | ||
| 26 | + | historyRepo.EXPECT().GetRecentSince(mock.Anything, 30).Return(entries, nil) | |
| 27 | + | ||
| 28 | + | uc := usecase.NewSearchHistoryUseCase(historyRepo) | |
| 29 | + | ||
| 30 | + | result, err := uc.GetRecentSince(ctx, 30) | |
| 31 | + | require.NoError(t, err) | |
| 32 | + | require.Len(t, result, 2) | |
| 33 | + | assert.Equal(t, "https://example.com", result[0].URL) | |
| 34 | + | assert.Equal(t, "https://github.com", result[1].URL) | |
| 35 | + | } | |
| 36 | + | ||
| 37 | + | func TestSearchHistoryUseCase_GetRecentSince_ZeroMeansAll(t *testing.T) { | |
| 38 | + | ctx := testContext() | |
| 39 | + | ||
| 40 | + | historyRepo := repomocks.NewMockHistoryRepository(t) | |
| 41 | + | ||
| 42 | + | entries := []*entity.HistoryEntry{ | |
| 43 | + | {ID: 1, URL: "https://example.com", Title: "Example"}, | |
| 44 | + | } | |
| 45 | + | ||
| 46 | + | // When days == 0, should call GetAllRecentHistory | |
| 47 | + | historyRepo.EXPECT().GetAllRecentHistory(mock.Anything).Return(entries, nil) | |
| 48 | + | ||
| 49 | + | uc := usecase.NewSearchHistoryUseCase(historyRepo) | |
| 50 | + | ||
| 51 | + | result, err := uc.GetRecentSince(ctx, 0) | |
| 52 | + | require.NoError(t, err) | |
| 53 | + | require.Len(t, result, 1) | |
| 54 | + | } | |
| 55 | + | ||
| 56 | + | func TestSearchHistoryUseCase_GetRecentSince_NegativeDaysDefaultsTo30(t *testing.T) { | |
| 57 | + | ctx := testContext() | |
| 58 | + | ||
| 59 | + | historyRepo := repomocks.NewMockHistoryRepository(t) | |
| 60 | + | ||
| 61 | + | entries := []*entity.HistoryEntry{} | |
| 62 | + | ||
| 63 | + | historyRepo.EXPECT().GetRecentSince(mock.Anything, 30).Return(entries, nil) | |
| 64 | + | ||
| 65 | + | uc := usecase.NewSearchHistoryUseCase(historyRepo) | |
| 66 | + | ||
| 67 | + | result, err := uc.GetRecentSince(ctx, -5) | |
| 68 | + | require.NoError(t, err) | |
| 69 | + | require.Empty(t, result) | |
| 70 | + | } | |
| 71 | + | ||
| 72 | + | func TestSearchHistoryUseCase_GetRecentSince_ReturnsErrorOnRepoFailure(t *testing.T) { | |
| 73 | + | ctx := testContext() | |
| 74 | + | ||
| 75 | + | historyRepo := repomocks.NewMockHistoryRepository(t) | |
| 76 | + | ||
| 77 | + | historyRepo.EXPECT().GetRecentSince(mock.Anything, 7).Return(nil, errors.New("db connection failed")) | |
| 78 | + | ||
| 79 | + | uc := usecase.NewSearchHistoryUseCase(historyRepo) | |
| 80 | + | ||
| 81 | + | result, err := uc.GetRecentSince(ctx, 7) | |
| 82 | + | require.Error(t, err) | |
| 83 | + | assert.Nil(t, result) | |
| 84 | + | assert.Contains(t, err.Error(), "failed to get recent history") | |
| 85 | + | } | |
| 86 | + | ||
| 87 | + | func TestSearchHistoryUseCase_GetMostVisited_ReturnsEntries(t *testing.T) { | |
| 88 | + | ctx := testContext() | |
| 89 | + | ||
| 90 | + | historyRepo := repomocks.NewMockHistoryRepository(t) | |
| 91 | + | ||
| 92 | + | entries := []*entity.HistoryEntry{ | |
| 93 | + | {ID: 1, URL: "https://github.com", Title: "GitHub", VisitCount: 100}, | |
| 94 | + | {ID: 2, URL: "https://example.com", Title: "Example", VisitCount: 50}, | |
| 95 | + | } | |
| 96 | + | ||
| 97 | + | historyRepo.EXPECT().GetMostVisited(mock.Anything, 30).Return(entries, nil) | |
| 98 | + | ||
| 99 | + | uc := usecase.NewSearchHistoryUseCase(historyRepo) | |
| 100 | + | ||
| 101 | + | result, err := uc.GetMostVisited(ctx, 30) | |
| 102 | + | require.NoError(t, err) | |
| 103 | + | require.Len(t, result, 2) | |
| 104 | + | assert.Equal(t, "https://github.com", result[0].URL) | |
| 105 | + | assert.Equal(t, int64(100), result[0].VisitCount) | |
| 106 | + | } | |
| 107 | + | ||
| 108 | + | func TestSearchHistoryUseCase_GetMostVisited_ZeroMeansAll(t *testing.T) { | |
| 109 | + | ctx := testContext() | |
| 110 | + | ||
| 111 | + | historyRepo := repomocks.NewMockHistoryRepository(t) | |
| 112 | + | ||
| 113 | + | entries := []*entity.HistoryEntry{} | |
| 114 | + | ||
| 115 | + | // When days == 0, should call GetAllMostVisited | |
| 116 | + | historyRepo.EXPECT().GetAllMostVisited(mock.Anything).Return(entries, nil) | |
| 117 | + | ||
| 118 | + | uc := usecase.NewSearchHistoryUseCase(historyRepo) | |
| 119 | + | ||
| 120 | + | result, err := uc.GetMostVisited(ctx, 0) | |
| 121 | + | require.NoError(t, err) | |
| 122 | + | require.Empty(t, result) | |
| 123 | + | } | |
| 124 | + | ||
| 125 | + | func TestSearchHistoryUseCase_GetMostVisited_ReturnsErrorOnRepoFailure(t *testing.T) { | |
| 126 | + | ctx := testContext() | |
| 127 | + | ||
| 128 | + | historyRepo := repomocks.NewMockHistoryRepository(t) | |
| 129 | + | ||
| 130 | + | historyRepo.EXPECT().GetMostVisited(mock.Anything, 14).Return(nil, errors.New("query failed")) | |
| 131 | + | ||
| 132 | + | uc := usecase.NewSearchHistoryUseCase(historyRepo) | |
| 133 | + | ||
| 134 | + | result, err := uc.GetMostVisited(ctx, 14) | |
| 135 | + | require.Error(t, err) | |
| 136 | + | assert.Nil(t, result) | |
| 137 | + | assert.Contains(t, err.Error(), "failed to get most visited history") | |
| 138 | + | } | |
| 139 | + | ||
| 140 | + | func TestSearchHistoryUseCase_GetMostVisited_ReturnsEmptyWhenNoHistory(t *testing.T) { | |
| 141 | + | ctx := testContext() | |
| 142 | + | ||
| 143 | + | historyRepo := repomocks.NewMockHistoryRepository(t) | |
| 144 | + | ||
| 145 | + | historyRepo.EXPECT().GetMostVisited(mock.Anything, 30).Return([]*entity.HistoryEntry{}, nil) | |
| 146 | + | ||
| 147 | + | uc := usecase.NewSearchHistoryUseCase(historyRepo) | |
| 148 | + | ||
| 149 | + | result, err := uc.GetMostVisited(ctx, 30) | |
| 150 | + | require.NoError(t, err) | |
| 151 | + | require.NotNil(t, result) | |
| 152 | + | assert.Empty(t, result) | |
| 153 | + | } | |
| @@ -0,0 +1,153 @@ | |||||
| 1 | + | package usecase_test | |||
| 2 | + | ||||
| 3 | + | import ( | |||
| 4 | + | "errors" | |||
| 5 | + | "testing" | |||
| 6 | + | "time" | |||
| 7 | + | ||||
| 8 | + | "github.com/bnema/dumber/internal/application/usecase" | |||
| 9 | + | "github.com/bnema/dumber/internal/domain/entity" | |||
| 10 | + | repomocks "github.com/bnema/dumber/internal/domain/repository/mocks" | |||
| 11 | + | "github.com/stretchr/testify/assert" | |||
| 12 | + | "github.com/stretchr/testify/mock" | |||
| 13 | + | "github.com/stretchr/testify/require" | |||
| 14 | + | ) | |||
| 15 | + | ||||
| 16 | + | func TestSearchHistoryUseCase_GetRecentSince_ReturnsEntries(t *testing.T) { | |||
| 17 | + | ctx := testContext() | |||
| 18 | + | ||||
| 19 | + | historyRepo := repomocks.NewMockHistoryRepository(t) | |||
| 20 | + | ||||
| 21 | + | entries := []*entity.HistoryEntry{ | |||
| 22 | + | {ID: 1, URL: "https://example.com", Title: "Example", LastVisited: time.Now()}, | |||
| 23 | + | {ID: 2, URL: "https://github.com", Title: "GitHub", LastVisited: time.Now().Add(-24 * time.Hour)}, | |||
| 24 | + | } | |||
| 25 | + | ||||
| 26 | + | historyRepo.EXPECT().GetRecentSince(mock.Anything, 30).Return(entries, nil) | |||
| 27 | + | ||||
| 28 | + | uc := usecase.NewSearchHistoryUseCase(historyRepo) | |||
| 29 | + | ||||
| 30 | + | result, err := uc.GetRecentSince(ctx, 30) | |||
| 31 | + | require.NoError(t, err) | |||
| 32 | + | require.Len(t, result, 2) | |||
| 33 | + | assert.Equal(t, "https://example.com", result[0].URL) | |||
| 34 | + | assert.Equal(t, "https://github.com", result[1].URL) | |||
| 35 | + | } | |||
| 36 | + | ||||
| 37 | + | func TestSearchHistoryUseCase_GetRecentSince_ZeroMeansAll(t *testing.T) { | |||
| 38 | + | ctx := testContext() | |||
| 39 | + | ||||
| 40 | + | historyRepo := repomocks.NewMockHistoryRepository(t) | |||
| 41 | + | ||||
| 42 | + | entries := []*entity.HistoryEntry{ | |||
| 43 | + | {ID: 1, URL: "https://example.com", Title: "Example"}, | |||
| 44 | + | } | |||
| 45 | + | ||||
| 46 | + | // When days == 0, should call GetAllRecentHistory | |||
| 47 | + | historyRepo.EXPECT().GetAllRecentHistory(mock.Anything).Return(entries, nil) | |||
| 48 | + | ||||
| 49 | + | uc := usecase.NewSearchHistoryUseCase(historyRepo) | |||
| 50 | + | ||||
| 51 | + | result, err := uc.GetRecentSince(ctx, 0) | |||
| 52 | + | require.NoError(t, err) | |||
| 53 | + | require.Len(t, result, 1) | |||
| 54 | + | } | |||
| 55 | + | ||||
| 56 | + | func TestSearchHistoryUseCase_GetRecentSince_NegativeDaysDefaultsTo30(t *testing.T) { | |||
| 57 | + | ctx := testContext() | |||
| 58 | + | ||||
| 59 | + | historyRepo := repomocks.NewMockHistoryRepository(t) | |||
| 60 | + | ||||
| 61 | + | entries := []*entity.HistoryEntry{} | |||
| 62 | + | ||||
| 63 | + | historyRepo.EXPECT().GetRecentSince(mock.Anything, 30).Return(entries, nil) | |||
| 64 | + | ||||
| 65 | + | uc := usecase.NewSearchHistoryUseCase(historyRepo) | |||
| 66 | + | ||||
| 67 | + | result, err := uc.GetRecentSince(ctx, -5) | |||
| 68 | + | require.NoError(t, err) | |||
| 69 | + | require.Empty(t, result) | |||
| 70 | + | } | |||
| 71 | + | ||||
| 72 | + | func TestSearchHistoryUseCase_GetRecentSince_ReturnsErrorOnRepoFailure(t *testing.T) { | |||
| 73 | + | ctx := testContext() | |||
| 74 | + | ||||
| 75 | + | historyRepo := repomocks.NewMockHistoryRepository(t) | |||
| 76 | + | ||||
| 77 | + | historyRepo.EXPECT().GetRecentSince(mock.Anything, 7).Return(nil, errors.New("db connection failed")) | |||
| 78 | + | ||||
| 79 | + | uc := usecase.NewSearchHistoryUseCase(historyRepo) | |||
| 80 | + | ||||
| 81 | + | result, err := uc.GetRecentSince(ctx, 7) | |||
| 82 | + | require.Error(t, err) | |||
| 83 | + | assert.Nil(t, result) | |||
| 84 | + | assert.Contains(t, err.Error(), "failed to get recent history") | |||
| 85 | + | } | |||
| 86 | + | ||||
| 87 | + | func TestSearchHistoryUseCase_GetMostVisited_ReturnsEntries(t *testing.T) { | |||
| 88 | + | ctx := testContext() | |||
| 89 | + | ||||
| 90 | + | historyRepo := repomocks.NewMockHistoryRepository(t) | |||
| 91 | + | ||||
| 92 | + | entries := []*entity.HistoryEntry{ | |||
| 93 | + | {ID: 1, URL: "https://github.com", Title: "GitHub", VisitCount: 100}, | |||
| 94 | + | {ID: 2, URL: "https://example.com", Title: "Example", VisitCount: 50}, | |||
| 95 | + | } | |||
| 96 | + | ||||
| 97 | + | historyRepo.EXPECT().GetMostVisited(mock.Anything, 30).Return(entries, nil) | |||
| 98 | + | ||||
| 99 | + | uc := usecase.NewSearchHistoryUseCase(historyRepo) | |||
| 100 | + | ||||
| 101 | + | result, err := uc.GetMostVisited(ctx, 30) | |||
| 102 | + | require.NoError(t, err) | |||
| 103 | + | require.Len(t, result, 2) | |||
| 104 | + | assert.Equal(t, "https://github.com", result[0].URL) | |||
| 105 | + | assert.Equal(t, int64(100), result[0].VisitCount) | |||
| 106 | + | } | |||
| 107 | + | ||||
| 108 | + | func TestSearchHistoryUseCase_GetMostVisited_ZeroMeansAll(t *testing.T) { | |||
| 109 | + | ctx := testContext() | |||
| 110 | + | ||||
| 111 | + | historyRepo := repomocks.NewMockHistoryRepository(t) | |||
| 112 | + | ||||
| 113 | + | entries := []*entity.HistoryEntry{} | |||
| 114 | + | ||||
| 115 | + | // When days == 0, should call GetAllMostVisited | |||
| 116 | + | historyRepo.EXPECT().GetAllMostVisited(mock.Anything).Return(entries, nil) | |||
| 117 | + | ||||
| 118 | + | uc := usecase.NewSearchHistoryUseCase(historyRepo) | |||
| 119 | + | ||||
| 120 | + | result, err := uc.GetMostVisited(ctx, 0) | |||
| 121 | + | require.NoError(t, err) | |||
| 122 | + | require.Empty(t, result) | |||
| 123 | + | } | |||
| 124 | + | ||||
| 125 | + | func TestSearchHistoryUseCase_GetMostVisited_ReturnsErrorOnRepoFailure(t *testing.T) { | |||
| 126 | + | ctx := testContext() | |||
| 127 | + | ||||
| 128 | + | historyRepo := repomocks.NewMockHistoryRepository(t) | |||
| 129 | + | ||||
| 130 | + | historyRepo.EXPECT().GetMostVisited(mock.Anything, 14).Return(nil, errors.New("query failed")) | |||
| 131 | + | ||||
| 132 | + | uc := usecase.NewSearchHistoryUseCase(historyRepo) | |||
| 133 | + | ||||
| 134 | + | result, err := uc.GetMostVisited(ctx, 14) | |||
| 135 | + | require.Error(t, err) | |||
| 136 | + | assert.Nil(t, result) | |||
| 137 | + | assert.Contains(t, err.Error(), "failed to get most visited history") | |||
| 138 | + | } | |||
| 139 | + | ||||
| 140 | + | func TestSearchHistoryUseCase_GetMostVisited_ReturnsEmptyWhenNoHistory(t *testing.T) { | |||
| 141 | + | ctx := testContext() | |||
| 142 | + | ||||
| 143 | + | historyRepo := repomocks.NewMockHistoryRepository(t) | |||
| 144 | + | ||||
| 145 | + | historyRepo.EXPECT().GetMostVisited(mock.Anything, 30).Return([]*entity.HistoryEntry{}, nil) | |||
| 146 | + | ||||
| 147 | + | uc := usecase.NewSearchHistoryUseCase(historyRepo) | |||
| 148 | + | ||||
| 149 | + | result, err := uc.GetMostVisited(ctx, 30) | |||
| 150 | + | require.NoError(t, err) | |||
| 151 | + | require.NotNil(t, result) | |||
| 152 | + | assert.Empty(t, result) | |||
| 153 | + | } | |||
M internal/cli/cmd/dmenu.go +21 -6
| @@ -13,6 +13,7 @@ import ( | |||
| 13 | 13 | ||
| 14 | 14 | "github.com/bnema/dumber/assets" | |
| 15 | 15 | "github.com/bnema/dumber/internal/cli/model" | |
| 16 | + | "github.com/bnema/dumber/internal/domain/entity" | |
| 16 | 17 | domainurl "github.com/bnema/dumber/internal/domain/url" | |
| 17 | 18 | "github.com/bnema/dumber/internal/infrastructure/config" | |
| 18 | 19 | "github.com/bnema/dumber/internal/infrastructure/favicon" | |
| @@ -22,7 +23,8 @@ import ( | |||
| 22 | 23 | var ( | |
| 23 | 24 | dmenuInteractive bool | |
| 24 | 25 | dmenuSelect bool | |
| 25 | - | dmenuMax int | |
| 26 | + | dmenuDays int | |
| 27 | + | dmenuMostVisited bool | |
| 26 | 28 | ) | |
| 27 | 29 | ||
| 28 | 30 | var dmenuCmd = &cobra.Command{ | |
| @@ -43,7 +45,8 @@ func init() { | |||
| 43 | 45 | ||
| 44 | 46 | dmenuCmd.Flags().BoolVarP(&dmenuInteractive, "interactive", "i", false, "use interactive TUI mode") | |
| 45 | 47 | dmenuCmd.Flags().BoolVar(&dmenuSelect, "select", false, "process selection from stdin") | |
| 46 | - | dmenuCmd.Flags().IntVar(&dmenuMax, "max", 0, "maximum entries to output (default from config)") | |
| 48 | + | dmenuCmd.Flags().IntVar(&dmenuDays, "days", 0, "number of days of history to show (default from config)") | |
| 49 | + | dmenuCmd.Flags().BoolVarP(&dmenuMostVisited, "most-visited", "m", false, "sort by visit count instead of recency") | |
| 47 | 50 | } | |
| 48 | 51 | ||
| 49 | 52 | func runDmenu(_ *cobra.Command, _ []string) error { | |
| @@ -71,9 +74,14 @@ func getDmenuConfig() config.DmenuConfig { | |||
| 71 | 74 | app := GetApp() | |
| 72 | 75 | cfg := app.Config.Dmenu | |
| 73 | 76 | ||
| 74 | - | // Override max if explicitly set via CLI flag | |
| 75 | - | if dmenuMax > 0 { | |
| 76 | - | cfg.MaxHistoryItems = dmenuMax | |
| 77 | + | // Override days if explicitly set via CLI flag | |
| 78 | + | if dmenuDays > 0 { | |
| 79 | + | cfg.MaxHistoryDays = dmenuDays | |
| 80 | + | } | |
| 81 | + | ||
| 82 | + | // Override sort_by_visit_count if --most-visited flag is set | |
| 83 | + | if dmenuMostVisited { | |
| 84 | + | cfg.SortByVisitCount = true | |
| 77 | 85 | } | |
| 78 | 86 | ||
| 79 | 87 | return cfg | |
| @@ -131,7 +139,14 @@ func runDmenuPipe() error { | |||
| 131 | 139 | ctx := context.Background() | |
| 132 | 140 | cfg := getDmenuConfig() | |
| 133 | 141 | ||
| 134 | - | entries, err := app.SearchHistoryUC.GetRecent(ctx, cfg.MaxHistoryItems, 0) | |
| 142 | + | var entries []*entity.HistoryEntry | |
| 143 | + | var err error | |
| 144 | + | ||
| 145 | + | if cfg.SortByVisitCount { | |
| 146 | + | entries, err = app.SearchHistoryUC.GetMostVisited(ctx, cfg.MaxHistoryDays) | |
| 147 | + | } else { | |
| 148 | + | entries, err = app.SearchHistoryUC.GetRecentSince(ctx, cfg.MaxHistoryDays) | |
| 149 | + | } | |
| 135 | 150 | if err != nil { | |
| 136 | 151 | return fmt.Errorf("get history: %w", err) | |
| 137 | 152 | } | |
| @@ -13,6 +13,7 @@ import ( | |||||
| 13 | 13 | ||||
| 14 | "github.com/bnema/dumber/assets" | 14 | "github.com/bnema/dumber/assets" | ||
| 15 | "github.com/bnema/dumber/internal/cli/model" | 15 | "github.com/bnema/dumber/internal/cli/model" | ||
| 16 | + | "github.com/bnema/dumber/internal/domain/entity" | |||
| 16 | domainurl "github.com/bnema/dumber/internal/domain/url" | 17 | domainurl "github.com/bnema/dumber/internal/domain/url" | ||
| 17 | "github.com/bnema/dumber/internal/infrastructure/config" | 18 | "github.com/bnema/dumber/internal/infrastructure/config" | ||
| 18 | "github.com/bnema/dumber/internal/infrastructure/favicon" | 19 | "github.com/bnema/dumber/internal/infrastructure/favicon" | ||
| @@ -22,7 +23,8 @@ import ( | |||||
| 22 | var ( | 23 | var ( | ||
| 23 | dmenuInteractive bool | 24 | dmenuInteractive bool | ||
| 24 | dmenuSelect bool | 25 | dmenuSelect bool | ||
| 25 | - | dmenuMax int | 26 | + | dmenuDays int |
| 27 | + | dmenuMostVisited bool | |||
| 26 | ) | 28 | ) | ||
| 27 | 29 | ||||
| 28 | var dmenuCmd = &cobra.Command{ | 30 | var dmenuCmd = &cobra.Command{ | ||
| @@ -43,7 +45,8 @@ func init() { | |||||
| 43 | 45 | ||||
| 44 | dmenuCmd.Flags().BoolVarP(&dmenuInteractive, "interactive", "i", false, "use interactive TUI mode") | 46 | dmenuCmd.Flags().BoolVarP(&dmenuInteractive, "interactive", "i", false, "use interactive TUI mode") | ||
| 45 | dmenuCmd.Flags().BoolVar(&dmenuSelect, "select", false, "process selection from stdin") | 47 | dmenuCmd.Flags().BoolVar(&dmenuSelect, "select", false, "process selection from stdin") | ||
| 46 | - | dmenuCmd.Flags().IntVar(&dmenuMax, "max", 0, "maximum entries to output (default from config)") | 48 | + | dmenuCmd.Flags().IntVar(&dmenuDays, "days", 0, "number of days of history to show (default from config)") |
| 49 | + | dmenuCmd.Flags().BoolVarP(&dmenuMostVisited, "most-visited", "m", false, "sort by visit count instead of recency") | |||
| 47 | } | 50 | } | ||
| 48 | 51 | ||||
| 49 | func runDmenu(_ *cobra.Command, _ []string) error { | 52 | func runDmenu(_ *cobra.Command, _ []string) error { | ||
| @@ -71,9 +74,14 @@ func getDmenuConfig() config.DmenuConfig { | |||||
| 71 | app := GetApp() | 74 | app := GetApp() | ||
| 72 | cfg := app.Config.Dmenu | 75 | cfg := app.Config.Dmenu | ||
| 73 | 76 | ||||
| 74 | - | // Override max if explicitly set via CLI flag | 77 | + | // Override days if explicitly set via CLI flag |
| 75 | - | if dmenuMax > 0 { | 78 | + | if dmenuDays > 0 { |
| 76 | - | cfg.MaxHistoryItems = dmenuMax | 79 | + | cfg.MaxHistoryDays = dmenuDays |
| 80 | + | } | |||
| 81 | + | ||||
| 82 | + | // Override sort_by_visit_count if --most-visited flag is set | |||
| 83 | + | if dmenuMostVisited { | |||
| 84 | + | cfg.SortByVisitCount = true | |||
| 77 | } | 85 | } | ||
| 78 | 86 | ||||
| 79 | return cfg | 87 | return cfg | ||
| @@ -131,7 +139,14 @@ func runDmenuPipe() error { | |||||
| 131 | ctx := context.Background() | 139 | ctx := context.Background() | ||
| 132 | cfg := getDmenuConfig() | 140 | cfg := getDmenuConfig() | ||
| 133 | 141 | ||||
| 134 | - | entries, err := app.SearchHistoryUC.GetRecent(ctx, cfg.MaxHistoryItems, 0) | 142 | + | var entries []*entity.HistoryEntry |
| 143 | + | var err error | |||
| 144 | + | ||||
| 145 | + | if cfg.SortByVisitCount { | |||
| 146 | + | entries, err = app.SearchHistoryUC.GetMostVisited(ctx, cfg.MaxHistoryDays) | |||
| 147 | + | } else { | |||
| 148 | + | entries, err = app.SearchHistoryUC.GetRecentSince(ctx, cfg.MaxHistoryDays) | |||
| 149 | + | } | |||
| 135 | if err != nil { | 150 | if err != nil { | ||
| 136 | return fmt.Errorf("get history: %w", err) | 151 | return fmt.Errorf("get history: %w", err) | ||
| 137 | } | 152 | } | ||
M internal/domain/repository/history.go +14 -0
| @@ -21,6 +21,20 @@ type HistoryRepository interface { | |||
| 21 | 21 | // GetRecent retrieves recent history entries with pagination. | |
| 22 | 22 | GetRecent(ctx context.Context, limit, offset int) ([]*entity.HistoryEntry, error) | |
| 23 | 23 | ||
| 24 | + | // GetRecentSince retrieves history entries visited within the last N days. | |
| 25 | + | // days must be > 0. | |
| 26 | + | GetRecentSince(ctx context.Context, days int) ([]*entity.HistoryEntry, error) | |
| 27 | + | ||
| 28 | + | // GetMostVisited retrieves history entries sorted by visit count within the last N days. | |
| 29 | + | // days must be > 0. | |
| 30 | + | GetMostVisited(ctx context.Context, days int) ([]*entity.HistoryEntry, error) | |
| 31 | + | ||
| 32 | + | // GetAllRecentHistory retrieves all history entries sorted by recency. | |
| 33 | + | GetAllRecentHistory(ctx context.Context) ([]*entity.HistoryEntry, error) | |
| 34 | + | ||
| 35 | + | // GetAllMostVisited retrieves all history entries sorted by visit count. | |
| 36 | + | GetAllMostVisited(ctx context.Context) ([]*entity.HistoryEntry, error) | |
| 37 | + | ||
| 24 | 38 | // IncrementVisitCount increments the visit count for a URL. | |
| 25 | 39 | IncrementVisitCount(ctx context.Context, url string) error | |
| 26 | 40 | ||
| @@ -21,6 +21,20 @@ type HistoryRepository interface { | |||||
| 21 | // GetRecent retrieves recent history entries with pagination. | 21 | // GetRecent retrieves recent history entries with pagination. | ||
| 22 | GetRecent(ctx context.Context, limit, offset int) ([]*entity.HistoryEntry, error) | 22 | GetRecent(ctx context.Context, limit, offset int) ([]*entity.HistoryEntry, error) | ||
| 23 | 23 | ||||
| 24 | + | // GetRecentSince retrieves history entries visited within the last N days. | |||
| 25 | + | // days must be > 0. | |||
| 26 | + | GetRecentSince(ctx context.Context, days int) ([]*entity.HistoryEntry, error) | |||
| 27 | + | ||||
| 28 | + | // GetMostVisited retrieves history entries sorted by visit count within the last N days. | |||
| 29 | + | // days must be > 0. | |||
| 30 | + | GetMostVisited(ctx context.Context, days int) ([]*entity.HistoryEntry, error) | |||
| 31 | + | ||||
| 32 | + | // GetAllRecentHistory retrieves all history entries sorted by recency. | |||
| 33 | + | GetAllRecentHistory(ctx context.Context) ([]*entity.HistoryEntry, error) | |||
| 34 | + | ||||
| 35 | + | // GetAllMostVisited retrieves all history entries sorted by visit count. | |||
| 36 | + | GetAllMostVisited(ctx context.Context) ([]*entity.HistoryEntry, error) | |||
| 37 | + | ||||
| 24 | // IncrementVisitCount increments the visit count for a URL. | 38 | // IncrementVisitCount increments the visit count for a URL. | ||
| 25 | IncrementVisitCount(ctx context.Context, url string) error | 39 | IncrementVisitCount(ctx context.Context, url string) error | ||
| 26 | 40 | ||||
M internal/domain/repository/mocks/mocks.go +260 -0
| @@ -2197,6 +2197,130 @@ func (_c *MockHistoryRepository_FindByURL_Call) RunAndReturn(run func(ctx contex | |||
| 2197 | 2197 | return _c | |
| 2198 | 2198 | } | |
| 2199 | 2199 | ||
| 2200 | + | // GetAllMostVisited provides a mock function for the type MockHistoryRepository | |
| 2201 | + | func (_mock *MockHistoryRepository) GetAllMostVisited(ctx context.Context) ([]*entity.HistoryEntry, error) { | |
| 2202 | + | ret := _mock.Called(ctx) | |
| 2203 | + | ||
| 2204 | + | if len(ret) == 0 { | |
| 2205 | + | panic("no return value specified for GetAllMostVisited") | |
| 2206 | + | } | |
| 2207 | + | ||
| 2208 | + | var r0 []*entity.HistoryEntry | |
| 2209 | + | var r1 error | |
| 2210 | + | if returnFunc, ok := ret.Get(0).(func(context.Context) ([]*entity.HistoryEntry, error)); ok { | |
| 2211 | + | return returnFunc(ctx) | |
| 2212 | + | } | |
| 2213 | + | if returnFunc, ok := ret.Get(0).(func(context.Context) []*entity.HistoryEntry); ok { | |
| 2214 | + | r0 = returnFunc(ctx) | |
| 2215 | + | } else { | |
| 2216 | + | if ret.Get(0) != nil { | |
| 2217 | + | r0 = ret.Get(0).([]*entity.HistoryEntry) | |
| 2218 | + | } | |
| 2219 | + | } | |
| 2220 | + | if returnFunc, ok := ret.Get(1).(func(context.Context) error); ok { | |
| 2221 | + | r1 = returnFunc(ctx) | |
| 2222 | + | } else { | |
| 2223 | + | r1 = ret.Error(1) | |
| 2224 | + | } | |
| 2225 | + | return r0, r1 | |
| 2226 | + | } | |
| 2227 | + | ||
| 2228 | + | // MockHistoryRepository_GetAllMostVisited_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'GetAllMostVisited' | |
| 2229 | + | type MockHistoryRepository_GetAllMostVisited_Call struct { | |
| 2230 | + | *mock.Call | |
| 2231 | + | } | |
| 2232 | + | ||
| 2233 | + | // GetAllMostVisited is a helper method to define mock.On call | |
| 2234 | + | // - ctx context.Context | |
| 2235 | + | func (_e *MockHistoryRepository_Expecter) GetAllMostVisited(ctx interface{}) *MockHistoryRepository_GetAllMostVisited_Call { | |
| 2236 | + | return &MockHistoryRepository_GetAllMostVisited_Call{Call: _e.mock.On("GetAllMostVisited", ctx)} | |
| 2237 | + | } | |
| 2238 | + | ||
| 2239 | + | func (_c *MockHistoryRepository_GetAllMostVisited_Call) Run(run func(ctx context.Context)) *MockHistoryRepository_GetAllMostVisited_Call { | |
| 2240 | + | _c.Call.Run(func(args mock.Arguments) { | |
| 2241 | + | var arg0 context.Context | |
| 2242 | + | if args[0] != nil { | |
| 2243 | + | arg0 = args[0].(context.Context) | |
| 2244 | + | } | |
| 2245 | + | run( | |
| 2246 | + | arg0, | |
| 2247 | + | ) | |
| 2248 | + | }) | |
| 2249 | + | return _c | |
| 2250 | + | } | |
| 2251 | + | ||
| 2252 | + | func (_c *MockHistoryRepository_GetAllMostVisited_Call) Return(historyEntrys []*entity.HistoryEntry, err error) *MockHistoryRepository_GetAllMostVisited_Call { | |
| 2253 | + | _c.Call.Return(historyEntrys, err) | |
| 2254 | + | return _c | |
| 2255 | + | } | |
| 2256 | + | ||
| 2257 | + | func (_c *MockHistoryRepository_GetAllMostVisited_Call) RunAndReturn(run func(ctx context.Context) ([]*entity.HistoryEntry, error)) *MockHistoryRepository_GetAllMostVisited_Call { | |
| 2258 | + | _c.Call.Return(run) | |
| 2259 | + | return _c | |
| 2260 | + | } | |
| 2261 | + | ||
| 2262 | + | // GetAllRecentHistory provides a mock function for the type MockHistoryRepository | |
| 2263 | + | func (_mock *MockHistoryRepository) GetAllRecentHistory(ctx context.Context) ([]*entity.HistoryEntry, error) { | |
| 2264 | + | ret := _mock.Called(ctx) | |
| 2265 | + | ||
| 2266 | + | if len(ret) == 0 { | |
| 2267 | + | panic("no return value specified for GetAllRecentHistory") | |
| 2268 | + | } | |
| 2269 | + | ||
| 2270 | + | var r0 []*entity.HistoryEntry | |
| 2271 | + | var r1 error | |
| 2272 | + | if returnFunc, ok := ret.Get(0).(func(context.Context) ([]*entity.HistoryEntry, error)); ok { | |
| 2273 | + | return returnFunc(ctx) | |
| 2274 | + | } | |
| 2275 | + | if returnFunc, ok := ret.Get(0).(func(context.Context) []*entity.HistoryEntry); ok { | |
| 2276 | + | r0 = returnFunc(ctx) | |
| 2277 | + | } else { | |
| 2278 | + | if ret.Get(0) != nil { | |
| 2279 | + | r0 = ret.Get(0).([]*entity.HistoryEntry) | |
| 2280 | + | } | |
| 2281 | + | } | |
| 2282 | + | if returnFunc, ok := ret.Get(1).(func(context.Context) error); ok { | |
| 2283 | + | r1 = returnFunc(ctx) | |
| 2284 | + | } else { | |
| 2285 | + | r1 = ret.Error(1) | |
| 2286 | + | } | |
| 2287 | + | return r0, r1 | |
| 2288 | + | } | |
| 2289 | + | ||
| 2290 | + | // MockHistoryRepository_GetAllRecentHistory_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'GetAllRecentHistory' | |
| 2291 | + | type MockHistoryRepository_GetAllRecentHistory_Call struct { | |
| 2292 | + | *mock.Call | |
| 2293 | + | } | |
| 2294 | + | ||
| 2295 | + | // GetAllRecentHistory is a helper method to define mock.On call | |
| 2296 | + | // - ctx context.Context | |
| 2297 | + | func (_e *MockHistoryRepository_Expecter) GetAllRecentHistory(ctx interface{}) *MockHistoryRepository_GetAllRecentHistory_Call { | |
| 2298 | + | return &MockHistoryRepository_GetAllRecentHistory_Call{Call: _e.mock.On("GetAllRecentHistory", ctx)} | |
| 2299 | + | } | |
| 2300 | + | ||
| 2301 | + | func (_c *MockHistoryRepository_GetAllRecentHistory_Call) Run(run func(ctx context.Context)) *MockHistoryRepository_GetAllRecentHistory_Call { | |
| 2302 | + | _c.Call.Run(func(args mock.Arguments) { | |
| 2303 | + | var arg0 context.Context | |
| 2304 | + | if args[0] != nil { | |
| 2305 | + | arg0 = args[0].(context.Context) | |
| 2306 | + | } | |
| 2307 | + | run( | |
| 2308 | + | arg0, | |
| 2309 | + | ) | |
| 2310 | + | }) | |
| 2311 | + | return _c | |
| 2312 | + | } | |
| 2313 | + | ||
| 2314 | + | func (_c *MockHistoryRepository_GetAllRecentHistory_Call) Return(historyEntrys []*entity.HistoryEntry, err error) *MockHistoryRepository_GetAllRecentHistory_Call { | |
| 2315 | + | _c.Call.Return(historyEntrys, err) | |
| 2316 | + | return _c | |
| 2317 | + | } | |
| 2318 | + | ||
| 2319 | + | func (_c *MockHistoryRepository_GetAllRecentHistory_Call) RunAndReturn(run func(ctx context.Context) ([]*entity.HistoryEntry, error)) *MockHistoryRepository_GetAllRecentHistory_Call { | |
| 2320 | + | _c.Call.Return(run) | |
| 2321 | + | return _c | |
| 2322 | + | } | |
| 2323 | + | ||
| 2200 | 2324 | // GetDailyVisitCount provides a mock function for the type MockHistoryRepository | |
| 2201 | 2325 | func (_mock *MockHistoryRepository) GetDailyVisitCount(ctx context.Context, daysAgo string) ([]*entity.DailyVisitCount, error) { | |
| 2202 | 2326 | ret := _mock.Called(ctx, daysAgo) | |
| @@ -2395,6 +2519,74 @@ func (_c *MockHistoryRepository_GetHourlyDistribution_Call) RunAndReturn(run fun | |||
| 2395 | 2519 | return _c | |
| 2396 | 2520 | } | |
| 2397 | 2521 | ||
| 2522 | + | // GetMostVisited provides a mock function for the type MockHistoryRepository | |
| 2523 | + | func (_mock *MockHistoryRepository) GetMostVisited(ctx context.Context, days int) ([]*entity.HistoryEntry, error) { | |
| 2524 | + | ret := _mock.Called(ctx, days) | |
| 2525 | + | ||
| 2526 | + | if len(ret) == 0 { | |
| 2527 | + | panic("no return value specified for GetMostVisited") | |
| 2528 | + | } | |
| 2529 | + | ||
| 2530 | + | var r0 []*entity.HistoryEntry | |
| 2531 | + | var r1 error | |
| 2532 | + | if returnFunc, ok := ret.Get(0).(func(context.Context, int) ([]*entity.HistoryEntry, error)); ok { | |
| 2533 | + | return returnFunc(ctx, days) | |
| 2534 | + | } | |
| 2535 | + | if returnFunc, ok := ret.Get(0).(func(context.Context, int) []*entity.HistoryEntry); ok { | |
| 2536 | + | r0 = returnFunc(ctx, days) | |
| 2537 | + | } else { | |
| 2538 | + | if ret.Get(0) != nil { | |
| 2539 | + | r0 = ret.Get(0).([]*entity.HistoryEntry) | |
| 2540 | + | } | |
| 2541 | + | } | |
| 2542 | + | if returnFunc, ok := ret.Get(1).(func(context.Context, int) error); ok { | |
| 2543 | + | r1 = returnFunc(ctx, days) | |
| 2544 | + | } else { | |
| 2545 | + | r1 = ret.Error(1) | |
| 2546 | + | } | |
| 2547 | + | return r0, r1 | |
| 2548 | + | } | |
| 2549 | + | ||
| 2550 | + | // MockHistoryRepository_GetMostVisited_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'GetMostVisited' | |
| 2551 | + | type MockHistoryRepository_GetMostVisited_Call struct { | |
| 2552 | + | *mock.Call | |
| 2553 | + | } | |
| 2554 | + | ||
| 2555 | + | // GetMostVisited is a helper method to define mock.On call | |
| 2556 | + | // - ctx context.Context | |
| 2557 | + | // - days int | |
| 2558 | + | func (_e *MockHistoryRepository_Expecter) GetMostVisited(ctx interface{}, days interface{}) *MockHistoryRepository_GetMostVisited_Call { | |
| 2559 | + | return &MockHistoryRepository_GetMostVisited_Call{Call: _e.mock.On("GetMostVisited", ctx, days)} | |
| 2560 | + | } | |
| 2561 | + | ||
| 2562 | + | func (_c *MockHistoryRepository_GetMostVisited_Call) Run(run func(ctx context.Context, days int)) *MockHistoryRepository_GetMostVisited_Call { | |
| 2563 | + | _c.Call.Run(func(args mock.Arguments) { | |
| 2564 | + | var arg0 context.Context | |
| 2565 | + | if args[0] != nil { | |
| 2566 | + | arg0 = args[0].(context.Context) | |
| 2567 | + | } | |
| 2568 | + | var arg1 int | |
| 2569 | + | if args[1] != nil { | |
| 2570 | + | arg1 = args[1].(int) | |
| 2571 | + | } | |
| 2572 | + | run( | |
| 2573 | + | arg0, | |
| 2574 | + | arg1, | |
| 2575 | + | ) | |
| 2576 | + | }) | |
| 2577 | + | return _c | |
| 2578 | + | } | |
| 2579 | + | ||
| 2580 | + | func (_c *MockHistoryRepository_GetMostVisited_Call) Return(historyEntrys []*entity.HistoryEntry, err error) *MockHistoryRepository_GetMostVisited_Call { | |
| 2581 | + | _c.Call.Return(historyEntrys, err) | |
| 2582 | + | return _c | |
| 2583 | + | } | |
| 2584 | + | ||
| 2585 | + | func (_c *MockHistoryRepository_GetMostVisited_Call) RunAndReturn(run func(ctx context.Context, days int) ([]*entity.HistoryEntry, error)) *MockHistoryRepository_GetMostVisited_Call { | |
| 2586 | + | _c.Call.Return(run) | |
| 2587 | + | return _c | |
| 2588 | + | } | |
| 2589 | + | ||
| 2398 | 2590 | // GetRecent provides a mock function for the type MockHistoryRepository | |
| 2399 | 2591 | func (_mock *MockHistoryRepository) GetRecent(ctx context.Context, limit int, offset int) ([]*entity.HistoryEntry, error) { | |
| 2400 | 2592 | ret := _mock.Called(ctx, limit, offset) | |
| @@ -2469,6 +2661,74 @@ func (_c *MockHistoryRepository_GetRecent_Call) RunAndReturn(run func(ctx contex | |||
| 2469 | 2661 | return _c | |
| 2470 | 2662 | } | |
| 2471 | 2663 | ||
| 2664 | + | // GetRecentSince provides a mock function for the type MockHistoryRepository | |
| 2665 | + | func (_mock *MockHistoryRepository) GetRecentSince(ctx context.Context, days int) ([]*entity.HistoryEntry, error) { | |
| 2666 | + | ret := _mock.Called(ctx, days) | |
| 2667 | + | ||
| 2668 | + | if len(ret) == 0 { | |
| 2669 | + | panic("no return value specified for GetRecentSince") | |
| 2670 | + | } | |
| 2671 | + | ||
| 2672 | + | var r0 []*entity.HistoryEntry | |
| 2673 | + | var r1 error | |
| 2674 | + | if returnFunc, ok := ret.Get(0).(func(context.Context, int) ([]*entity.HistoryEntry, error)); ok { | |
| 2675 | + | return returnFunc(ctx, days) | |
| 2676 | + | } | |
| 2677 | + | if returnFunc, ok := ret.Get(0).(func(context.Context, int) []*entity.HistoryEntry); ok { | |
| 2678 | + | r0 = returnFunc(ctx, days) | |
| 2679 | + | } else { | |
| 2680 | + | if ret.Get(0) != nil { | |
| 2681 | + | r0 = ret.Get(0).([]*entity.HistoryEntry) | |
| 2682 | + | } | |
| 2683 | + | } | |
| 2684 | + | if returnFunc, ok := ret.Get(1).(func(context.Context, int) error); ok { | |
| 2685 | + | r1 = returnFunc(ctx, days) | |
| 2686 | + | } else { | |
| 2687 | + | r1 = ret.Error(1) | |
| 2688 | + | } | |
| 2689 | + | return r0, r1 | |
| 2690 | + | } | |
| 2691 | + | ||
| 2692 | + | // MockHistoryRepository_GetRecentSince_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'GetRecentSince' | |
| 2693 | + | type MockHistoryRepository_GetRecentSince_Call struct { | |
| 2694 | + | *mock.Call | |
| 2695 | + | } | |
| 2696 | + | ||
| 2697 | + | // GetRecentSince is a helper method to define mock.On call | |
| 2698 | + | // - ctx context.Context | |
| 2699 | + | // - days int | |
| 2700 | + | func (_e *MockHistoryRepository_Expecter) GetRecentSince(ctx interface{}, days interface{}) *MockHistoryRepository_GetRecentSince_Call { | |
| 2701 | + | return &MockHistoryRepository_GetRecentSince_Call{Call: _e.mock.On("GetRecentSince", ctx, days)} | |
| 2702 | + | } | |
| 2703 | + | ||
| 2704 | + | func (_c *MockHistoryRepository_GetRecentSince_Call) Run(run func(ctx context.Context, days int)) *MockHistoryRepository_GetRecentSince_Call { | |
| 2705 | + | _c.Call.Run(func(args mock.Arguments) { | |
| 2706 | + | var arg0 context.Context | |
| 2707 | + | if args[0] != nil { | |
| 2708 | + | arg0 = args[0].(context.Context) | |
| 2709 | + | } | |
| 2710 | + | var arg1 int | |
| 2711 | + | if args[1] != nil { | |
| 2712 | + | arg1 = args[1].(int) | |
| 2713 | + | } | |
| 2714 | + | run( | |
| 2715 | + | arg0, | |
| 2716 | + | arg1, | |
| 2717 | + | ) | |
| 2718 | + | }) | |
| 2719 | + | return _c | |
| 2720 | + | } | |
| 2721 | + | ||
| 2722 | + | func (_c *MockHistoryRepository_GetRecentSince_Call) Return(historyEntrys []*entity.HistoryEntry, err error) *MockHistoryRepository_GetRecentSince_Call { | |
| 2723 | + | _c.Call.Return(historyEntrys, err) | |
| 2724 | + | return _c | |
| 2725 | + | } | |
| 2726 | + | ||
| 2727 | + | func (_c *MockHistoryRepository_GetRecentSince_Call) RunAndReturn(run func(ctx context.Context, days int) ([]*entity.HistoryEntry, error)) *MockHistoryRepository_GetRecentSince_Call { | |
| 2728 | + | _c.Call.Return(run) | |
| 2729 | + | return _c | |
| 2730 | + | } | |
| 2731 | + | ||
| 2472 | 2732 | // GetStats provides a mock function for the type MockHistoryRepository | |
| 2473 | 2733 | func (_mock *MockHistoryRepository) GetStats(ctx context.Context) (*entity.HistoryStats, error) { | |
| 2474 | 2734 | ret := _mock.Called(ctx) | |
| @@ -2197,6 +2197,130 @@ func (_c *MockHistoryRepository_FindByURL_Call) RunAndReturn(run func(ctx contex | |||||
| 2197 | return _c | 2197 | return _c | ||
| 2198 | } | 2198 | } | ||
| 2199 | 2199 | ||||
| 2200 | + | // GetAllMostVisited provides a mock function for the type MockHistoryRepository | |||
| 2201 | + | func (_mock *MockHistoryRepository) GetAllMostVisited(ctx context.Context) ([]*entity.HistoryEntry, error) { | |||
| 2202 | + | ret := _mock.Called(ctx) | |||
| 2203 | + | ||||
| 2204 | + | if len(ret) == 0 { | |||
| 2205 | + | panic("no return value specified for GetAllMostVisited") | |||
| 2206 | + | } | |||
| 2207 | + | ||||
| 2208 | + | var r0 []*entity.HistoryEntry | |||
| 2209 | + | var r1 error | |||
| 2210 | + | if returnFunc, ok := ret.Get(0).(func(context.Context) ([]*entity.HistoryEntry, error)); ok { | |||
| 2211 | + | return returnFunc(ctx) | |||
| 2212 | + | } | |||
| 2213 | + | if returnFunc, ok := ret.Get(0).(func(context.Context) []*entity.HistoryEntry); ok { | |||
| 2214 | + | r0 = returnFunc(ctx) | |||
| 2215 | + | } else { | |||
| 2216 | + | if ret.Get(0) != nil { | |||
| 2217 | + | r0 = ret.Get(0).([]*entity.HistoryEntry) | |||
| 2218 | + | } | |||
| 2219 | + | } | |||
| 2220 | + | if returnFunc, ok := ret.Get(1).(func(context.Context) error); ok { | |||
| 2221 | + | r1 = returnFunc(ctx) | |||
| 2222 | + | } else { | |||
| 2223 | + | r1 = ret.Error(1) | |||
| 2224 | + | } | |||
| 2225 | + | return r0, r1 | |||
| 2226 | + | } | |||
| 2227 | + | ||||
| 2228 | + | // MockHistoryRepository_GetAllMostVisited_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'GetAllMostVisited' | |||
| 2229 | + | type MockHistoryRepository_GetAllMostVisited_Call struct { | |||
| 2230 | + | *mock.Call | |||
| 2231 | + | } | |||
| 2232 | + | ||||
| 2233 | + | // GetAllMostVisited is a helper method to define mock.On call | |||
| 2234 | + | // - ctx context.Context | |||
| 2235 | + | func (_e *MockHistoryRepository_Expecter) GetAllMostVisited(ctx interface{}) *MockHistoryRepository_GetAllMostVisited_Call { | |||
| 2236 | + | return &MockHistoryRepository_GetAllMostVisited_Call{Call: _e.mock.On("GetAllMostVisited", ctx)} | |||
| 2237 | + | } | |||
| 2238 | + | ||||
| 2239 | + | func (_c *MockHistoryRepository_GetAllMostVisited_Call) Run(run func(ctx context.Context)) *MockHistoryRepository_GetAllMostVisited_Call { | |||
| 2240 | + | _c.Call.Run(func(args mock.Arguments) { | |||
| 2241 | + | var arg0 context.Context | |||
| 2242 | + | if args[0] != nil { | |||
| 2243 | + | arg0 = args[0].(context.Context) | |||
| 2244 | + | } | |||
| 2245 | + | run( | |||
| 2246 | + | arg0, | |||
| 2247 | + | ) | |||
| 2248 | + | }) | |||
| 2249 | + | return _c | |||
| 2250 | + | } | |||
| 2251 | + | ||||
| 2252 | + | func (_c *MockHistoryRepository_GetAllMostVisited_Call) Return(historyEntrys []*entity.HistoryEntry, err error) *MockHistoryRepository_GetAllMostVisited_Call { | |||
| 2253 | + | _c.Call.Return(historyEntrys, err) | |||
| 2254 | + | return _c | |||
| 2255 | + | } | |||
| 2256 | + | ||||
| 2257 | + | func (_c *MockHistoryRepository_GetAllMostVisited_Call) RunAndReturn(run func(ctx context.Context) ([]*entity.HistoryEntry, error)) *MockHistoryRepository_GetAllMostVisited_Call { | |||
| 2258 | + | _c.Call.Return(run) | |||
| 2259 | + | return _c | |||
| 2260 | + | } | |||
| 2261 | + | ||||
| 2262 | + | // GetAllRecentHistory provides a mock function for the type MockHistoryRepository | |||
| 2263 | + | func (_mock *MockHistoryRepository) GetAllRecentHistory(ctx context.Context) ([]*entity.HistoryEntry, error) { | |||
| 2264 | + | ret := _mock.Called(ctx) | |||
| 2265 | + | ||||
| 2266 | + | if len(ret) == 0 { | |||
| 2267 | + | panic("no return value specified for GetAllRecentHistory") | |||
| 2268 | + | } | |||
| 2269 | + | ||||
| 2270 | + | var r0 []*entity.HistoryEntry | |||
| 2271 | + | var r1 error | |||
| 2272 | + | if returnFunc, ok := ret.Get(0).(func(context.Context) ([]*entity.HistoryEntry, error)); ok { | |||
| 2273 | + | return returnFunc(ctx) | |||
| 2274 | + | } | |||
| 2275 | + | if returnFunc, ok := ret.Get(0).(func(context.Context) []*entity.HistoryEntry); ok { | |||
| 2276 | + | r0 = returnFunc(ctx) | |||
| 2277 | + | } else { | |||
| 2278 | + | if ret.Get(0) != nil { | |||
| 2279 | + | r0 = ret.Get(0).([]*entity.HistoryEntry) | |||
| 2280 | + | } | |||
| 2281 | + | } | |||
| 2282 | + | if returnFunc, ok := ret.Get(1).(func(context.Context) error); ok { | |||
| 2283 | + | r1 = returnFunc(ctx) | |||
| 2284 | + | } else { | |||
| 2285 | + | r1 = ret.Error(1) | |||
| 2286 | + | } | |||
| 2287 | + | return r0, r1 | |||
| 2288 | + | } | |||
| 2289 | + | ||||
| 2290 | + | // MockHistoryRepository_GetAllRecentHistory_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'GetAllRecentHistory' | |||
| 2291 | + | type MockHistoryRepository_GetAllRecentHistory_Call struct { | |||
| 2292 | + | *mock.Call | |||
| 2293 | + | } | |||
| 2294 | + | ||||
| 2295 | + | // GetAllRecentHistory is a helper method to define mock.On call | |||
| 2296 | + | // - ctx context.Context | |||
| 2297 | + | func (_e *MockHistoryRepository_Expecter) GetAllRecentHistory(ctx interface{}) *MockHistoryRepository_GetAllRecentHistory_Call { | |||
| 2298 | + | return &MockHistoryRepository_GetAllRecentHistory_Call{Call: _e.mock.On("GetAllRecentHistory", ctx)} | |||
| 2299 | + | } | |||
| 2300 | + | ||||
| 2301 | + | func (_c *MockHistoryRepository_GetAllRecentHistory_Call) Run(run func(ctx context.Context)) *MockHistoryRepository_GetAllRecentHistory_Call { | |||
| 2302 | + | _c.Call.Run(func(args mock.Arguments) { | |||
| 2303 | + | var arg0 context.Context | |||
| 2304 | + | if args[0] != nil { | |||
| 2305 | + | arg0 = args[0].(context.Context) | |||
| 2306 | + | } | |||
| 2307 | + | run( | |||
| 2308 | + | arg0, | |||
| 2309 | + | ) | |||
| 2310 | + | }) | |||
| 2311 | + | return _c | |||
| 2312 | + | } | |||
| 2313 | + | ||||
| 2314 | + | func (_c *MockHistoryRepository_GetAllRecentHistory_Call) Return(historyEntrys []*entity.HistoryEntry, err error) *MockHistoryRepository_GetAllRecentHistory_Call { | |||
| 2315 | + | _c.Call.Return(historyEntrys, err) | |||
| 2316 | + | return _c | |||
| 2317 | + | } | |||
| 2318 | + | ||||
| 2319 | + | func (_c *MockHistoryRepository_GetAllRecentHistory_Call) RunAndReturn(run func(ctx context.Context) ([]*entity.HistoryEntry, error)) *MockHistoryRepository_GetAllRecentHistory_Call { | |||
| 2320 | + | _c.Call.Return(run) | |||
| 2321 | + | return _c | |||
| 2322 | + | } | |||
| 2323 | + | ||||
| 2200 | // GetDailyVisitCount provides a mock function for the type MockHistoryRepository | 2324 | // GetDailyVisitCount provides a mock function for the type MockHistoryRepository | ||
| 2201 | func (_mock *MockHistoryRepository) GetDailyVisitCount(ctx context.Context, daysAgo string) ([]*entity.DailyVisitCount, error) { | 2325 | func (_mock *MockHistoryRepository) GetDailyVisitCount(ctx context.Context, daysAgo string) ([]*entity.DailyVisitCount, error) { | ||
| 2202 | ret := _mock.Called(ctx, daysAgo) | 2326 | ret := _mock.Called(ctx, daysAgo) | ||
| @@ -2395,6 +2519,74 @@ func (_c *MockHistoryRepository_GetHourlyDistribution_Call) RunAndReturn(run fun | |||||
| 2395 | return _c | 2519 | return _c | ||
| 2396 | } | 2520 | } | ||
| 2397 | 2521 | ||||
| 2522 | + | // GetMostVisited provides a mock function for the type MockHistoryRepository | |||
| 2523 | + | func (_mock *MockHistoryRepository) GetMostVisited(ctx context.Context, days int) ([]*entity.HistoryEntry, error) { | |||
| 2524 | + | ret := _mock.Called(ctx, days) | |||
| 2525 | + | ||||
| 2526 | + | if len(ret) == 0 { | |||
| 2527 | + | panic("no return value specified for GetMostVisited") | |||
| 2528 | + | } | |||
| 2529 | + | ||||
| 2530 | + | var r0 []*entity.HistoryEntry | |||
| 2531 | + | var r1 error | |||
| 2532 | + | if returnFunc, ok := ret.Get(0).(func(context.Context, int) ([]*entity.HistoryEntry, error)); ok { | |||
| 2533 | + | return returnFunc(ctx, days) | |||
| 2534 | + | } | |||
| 2535 | + | if returnFunc, ok := ret.Get(0).(func(context.Context, int) []*entity.HistoryEntry); ok { | |||
| 2536 | + | r0 = returnFunc(ctx, days) | |||
| 2537 | + | } else { | |||
| 2538 | + | if ret.Get(0) != nil { | |||
| 2539 | + | r0 = ret.Get(0).([]*entity.HistoryEntry) | |||
| 2540 | + | } | |||
| 2541 | + | } | |||
| 2542 | + | if returnFunc, ok := ret.Get(1).(func(context.Context, int) error); ok { | |||
| 2543 | + | r1 = returnFunc(ctx, days) | |||
| 2544 | + | } else { | |||
| 2545 | + | r1 = ret.Error(1) | |||
| 2546 | + | } | |||
| 2547 | + | return r0, r1 | |||
| 2548 | + | } | |||
| 2549 | + | ||||
| 2550 | + | // MockHistoryRepository_GetMostVisited_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'GetMostVisited' | |||
| 2551 | + | type MockHistoryRepository_GetMostVisited_Call struct { | |||
| 2552 | + | *mock.Call | |||
| 2553 | + | } | |||
| 2554 | + | ||||
| 2555 | + | // GetMostVisited is a helper method to define mock.On call | |||
| 2556 | + | // - ctx context.Context | |||
| 2557 | + | // - days int | |||
| 2558 | + | func (_e *MockHistoryRepository_Expecter) GetMostVisited(ctx interface{}, days interface{}) *MockHistoryRepository_GetMostVisited_Call { | |||
| 2559 | + | return &MockHistoryRepository_GetMostVisited_Call{Call: _e.mock.On("GetMostVisited", ctx, days)} | |||
| 2560 | + | } | |||
| 2561 | + | ||||
| 2562 | + | func (_c *MockHistoryRepository_GetMostVisited_Call) Run(run func(ctx context.Context, days int)) *MockHistoryRepository_GetMostVisited_Call { | |||
| 2563 | + | _c.Call.Run(func(args mock.Arguments) { | |||
| 2564 | + | var arg0 context.Context | |||
| 2565 | + | if args[0] != nil { | |||
| 2566 | + | arg0 = args[0].(context.Context) | |||
| 2567 | + | } | |||
| 2568 | + | var arg1 int | |||
| 2569 | + | if args[1] != nil { | |||
| 2570 | + | arg1 = args[1].(int) | |||
| 2571 | + | } | |||
| 2572 | + | run( | |||
| 2573 | + | arg0, | |||
| 2574 | + | arg1, | |||
| 2575 | + | ) | |||
| 2576 | + | }) | |||
| 2577 | + | return _c | |||
| 2578 | + | } | |||
| 2579 | + | ||||
| 2580 | + | func (_c *MockHistoryRepository_GetMostVisited_Call) Return(historyEntrys []*entity.HistoryEntry, err error) *MockHistoryRepository_GetMostVisited_Call { | |||
| 2581 | + | _c.Call.Return(historyEntrys, err) | |||
| 2582 | + | return _c | |||
| 2583 | + | } | |||
| 2584 | + | ||||
| 2585 | + | func (_c *MockHistoryRepository_GetMostVisited_Call) RunAndReturn(run func(ctx context.Context, days int) ([]*entity.HistoryEntry, error)) *MockHistoryRepository_GetMostVisited_Call { | |||
| 2586 | + | _c.Call.Return(run) | |||
| 2587 | + | return _c | |||
| 2588 | + | } | |||
| 2589 | + | ||||
| 2398 | // GetRecent provides a mock function for the type MockHistoryRepository | 2590 | // GetRecent provides a mock function for the type MockHistoryRepository | ||
| 2399 | func (_mock *MockHistoryRepository) GetRecent(ctx context.Context, limit int, offset int) ([]*entity.HistoryEntry, error) { | 2591 | func (_mock *MockHistoryRepository) GetRecent(ctx context.Context, limit int, offset int) ([]*entity.HistoryEntry, error) { | ||
| 2400 | ret := _mock.Called(ctx, limit, offset) | 2592 | ret := _mock.Called(ctx, limit, offset) | ||
| @@ -2469,6 +2661,74 @@ func (_c *MockHistoryRepository_GetRecent_Call) RunAndReturn(run func(ctx contex | |||||
| 2469 | return _c | 2661 | return _c | ||
| 2470 | } | 2662 | } | ||
| 2471 | 2663 | ||||
| 2664 | + | // GetRecentSince provides a mock function for the type MockHistoryRepository | |||
| 2665 | + | func (_mock *MockHistoryRepository) GetRecentSince(ctx context.Context, days int) ([]*entity.HistoryEntry, error) { | |||
| 2666 | + | ret := _mock.Called(ctx, days) | |||
| 2667 | + | ||||
| 2668 | + | if len(ret) == 0 { | |||
| 2669 | + | panic("no return value specified for GetRecentSince") | |||
| 2670 | + | } | |||
| 2671 | + | ||||
| 2672 | + | var r0 []*entity.HistoryEntry | |||
| 2673 | + | var r1 error | |||
| 2674 | + | if returnFunc, ok := ret.Get(0).(func(context.Context, int) ([]*entity.HistoryEntry, error)); ok { | |||
| 2675 | + | return returnFunc(ctx, days) | |||
| 2676 | + | } | |||
| 2677 | + | if returnFunc, ok := ret.Get(0).(func(context.Context, int) []*entity.HistoryEntry); ok { | |||
| 2678 | + | r0 = returnFunc(ctx, days) | |||
| 2679 | + | } else { | |||
| 2680 | + | if ret.Get(0) != nil { | |||
| 2681 | + | r0 = ret.Get(0).([]*entity.HistoryEntry) | |||
| 2682 | + | } | |||
| 2683 | + | } | |||
| 2684 | + | if returnFunc, ok := ret.Get(1).(func(context.Context, int) error); ok { | |||
| 2685 | + | r1 = returnFunc(ctx, days) | |||
| 2686 | + | } else { | |||
| 2687 | + | r1 = ret.Error(1) | |||
| 2688 | + | } | |||
| 2689 | + | return r0, r1 | |||
| 2690 | + | } | |||
| 2691 | + | ||||
| 2692 | + | // MockHistoryRepository_GetRecentSince_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'GetRecentSince' | |||
| 2693 | + | type MockHistoryRepository_GetRecentSince_Call struct { | |||
| 2694 | + | *mock.Call | |||
| 2695 | + | } | |||
| 2696 | + | ||||
| 2697 | + | // GetRecentSince is a helper method to define mock.On call | |||
| 2698 | + | // - ctx context.Context | |||
| 2699 | + | // - days int | |||
| 2700 | + | func (_e *MockHistoryRepository_Expecter) GetRecentSince(ctx interface{}, days interface{}) *MockHistoryRepository_GetRecentSince_Call { | |||
| 2701 | + | return &MockHistoryRepository_GetRecentSince_Call{Call: _e.mock.On("GetRecentSince", ctx, days)} | |||
| 2702 | + | } | |||
| 2703 | + | ||||
| 2704 | + | func (_c *MockHistoryRepository_GetRecentSince_Call) Run(run func(ctx context.Context, days int)) *MockHistoryRepository_GetRecentSince_Call { | |||
| 2705 | + | _c.Call.Run(func(args mock.Arguments) { | |||
| 2706 | + | var arg0 context.Context | |||
| 2707 | + | if args[0] != nil { | |||
| 2708 | + | arg0 = args[0].(context.Context) | |||
| 2709 | + | } | |||
| 2710 | + | var arg1 int | |||
| 2711 | + | if args[1] != nil { | |||
| 2712 | + | arg1 = args[1].(int) | |||
| 2713 | + | } | |||
| 2714 | + | run( | |||
| 2715 | + | arg0, | |||
| 2716 | + | arg1, | |||
| 2717 | + | ) | |||
| 2718 | + | }) | |||
| 2719 | + | return _c | |||
| 2720 | + | } | |||
| 2721 | + | ||||
| 2722 | + | func (_c *MockHistoryRepository_GetRecentSince_Call) Return(historyEntrys []*entity.HistoryEntry, err error) *MockHistoryRepository_GetRecentSince_Call { | |||
| 2723 | + | _c.Call.Return(historyEntrys, err) | |||
| 2724 | + | return _c | |||
| 2725 | + | } | |||
| 2726 | + | ||||
| 2727 | + | func (_c *MockHistoryRepository_GetRecentSince_Call) RunAndReturn(run func(ctx context.Context, days int) ([]*entity.HistoryEntry, error)) *MockHistoryRepository_GetRecentSince_Call { | |||
| 2728 | + | _c.Call.Return(run) | |||
| 2729 | + | return _c | |||
| 2730 | + | } | |||
| 2731 | + | ||||
| 2472 | // GetStats provides a mock function for the type MockHistoryRepository | 2732 | // GetStats provides a mock function for the type MockHistoryRepository | ||
| 2473 | func (_mock *MockHistoryRepository) GetStats(ctx context.Context) (*entity.HistoryStats, error) { | 2733 | func (_mock *MockHistoryRepository) GetStats(ctx context.Context) (*entity.HistoryStats, error) { | ||
| 2474 | ret := _mock.Called(ctx) | 2734 | ret := _mock.Called(ctx) | ||
M internal/infrastructure/config/defaults.go +2 -2
| @@ -7,7 +7,7 @@ const ( | |||
| 7 | 7 | defaultRetentionDays = 365 // 1 year | |
| 8 | 8 | ||
| 9 | 9 | // Dmenu defaults | |
| 10 | - | defaultMaxHistoryItems = 20 // items | |
| 10 | + | defaultMaxHistoryDays = 30 // days | |
| 11 | 11 | ||
| 12 | 12 | // Logging defaults | |
| 13 | 13 | defaultMaxLogAgeDays = 7 // days | |
| @@ -97,7 +97,7 @@ func DefaultConfig() *Config { | |||
| 97 | 97 | SearchShortcuts: GetDefaultSearchShortcuts(), | |
| 98 | 98 | DefaultSearchEngine: "https://duckduckgo.com/?q=%s", | |
| 99 | 99 | Dmenu: DmenuConfig{ | |
| 100 | - | MaxHistoryItems: defaultMaxHistoryItems, | |
| 100 | + | MaxHistoryDays: defaultMaxHistoryDays, | |
| 101 | 101 | ShowVisitCount: true, | |
| 102 | 102 | ShowLastVisited: true, | |
| 103 | 103 | HistoryPrefix: "🕒", | |
| @@ -7,7 +7,7 @@ const ( | |||||
| 7 | defaultRetentionDays = 365 // 1 year | 7 | defaultRetentionDays = 365 // 1 year | ||
| 8 | 8 | ||||
| 9 | // Dmenu defaults | 9 | // Dmenu defaults | ||
| 10 | - | defaultMaxHistoryItems = 20 // items | 10 | + | defaultMaxHistoryDays = 30 // days |
| 11 | 11 | ||||
| 12 | // Logging defaults | 12 | // Logging defaults | ||
| 13 | defaultMaxLogAgeDays = 7 // days | 13 | defaultMaxLogAgeDays = 7 // days | ||
| @@ -97,7 +97,7 @@ func DefaultConfig() *Config { | |||||
| 97 | SearchShortcuts: GetDefaultSearchShortcuts(), | 97 | SearchShortcuts: GetDefaultSearchShortcuts(), | ||
| 98 | DefaultSearchEngine: "https://duckduckgo.com/?q=%s", | 98 | DefaultSearchEngine: "https://duckduckgo.com/?q=%s", | ||
| 99 | Dmenu: DmenuConfig{ | 99 | Dmenu: DmenuConfig{ | ||
| 100 | - | MaxHistoryItems: defaultMaxHistoryItems, | 100 | + | MaxHistoryDays: defaultMaxHistoryDays, |
| 101 | ShowVisitCount: true, | 101 | ShowVisitCount: true, | ||
| 102 | ShowLastVisited: true, | 102 | ShowLastVisited: true, | ||
| 103 | HistoryPrefix: "🕒", | 103 | HistoryPrefix: "🕒", | ||
M internal/infrastructure/config/loader.go +1 -1
| @@ -348,7 +348,7 @@ func (m *Manager) setSearchDefaults(defaults *Config) { | |||
| 348 | 348 | } | |
| 349 | 349 | ||
| 350 | 350 | func (m *Manager) setDmenuDefaults(defaults *Config) { | |
| 351 | - | m.viper.SetDefault("dmenu.max_history_items", defaults.Dmenu.MaxHistoryItems) | |
| 351 | + | m.viper.SetDefault("dmenu.max_history_days", defaults.Dmenu.MaxHistoryDays) | |
| 352 | 352 | m.viper.SetDefault("dmenu.show_visit_count", defaults.Dmenu.ShowVisitCount) | |
| 353 | 353 | m.viper.SetDefault("dmenu.show_last_visited", defaults.Dmenu.ShowLastVisited) | |
| 354 | 354 | m.viper.SetDefault("dmenu.history_prefix", defaults.Dmenu.HistoryPrefix) | |
| @@ -348,7 +348,7 @@ func (m *Manager) setSearchDefaults(defaults *Config) { | |||||
| 348 | } | 348 | } | ||
| 349 | 349 | ||||
| 350 | func (m *Manager) setDmenuDefaults(defaults *Config) { | 350 | func (m *Manager) setDmenuDefaults(defaults *Config) { | ||
| 351 | - | m.viper.SetDefault("dmenu.max_history_items", defaults.Dmenu.MaxHistoryItems) | 351 | + | m.viper.SetDefault("dmenu.max_history_days", defaults.Dmenu.MaxHistoryDays) |
| 352 | m.viper.SetDefault("dmenu.show_visit_count", defaults.Dmenu.ShowVisitCount) | 352 | m.viper.SetDefault("dmenu.show_visit_count", defaults.Dmenu.ShowVisitCount) | ||
| 353 | m.viper.SetDefault("dmenu.show_last_visited", defaults.Dmenu.ShowLastVisited) | 353 | m.viper.SetDefault("dmenu.show_last_visited", defaults.Dmenu.ShowLastVisited) | ||
| 354 | m.viper.SetDefault("dmenu.history_prefix", defaults.Dmenu.HistoryPrefix) | 354 | m.viper.SetDefault("dmenu.history_prefix", defaults.Dmenu.HistoryPrefix) | ||
M internal/infrastructure/config/schema.go +1 -1
| @@ -286,7 +286,7 @@ func (c *Config) ShortcutURLs() map[string]string { | |||
| 286 | 286 | ||
| 287 | 287 | // DmenuConfig holds dmenu/rofi integration configuration. | |
| 288 | 288 | type DmenuConfig struct { | |
| 289 | - | MaxHistoryItems int `mapstructure:"max_history_items" yaml:"max_history_items" toml:"max_history_items"` | |
| 289 | + | MaxHistoryDays int `mapstructure:"max_history_days" yaml:"max_history_days" toml:"max_history_days"` | |
| 290 | 290 | ShowVisitCount bool `mapstructure:"show_visit_count" yaml:"show_visit_count" toml:"show_visit_count"` | |
| 291 | 291 | ShowLastVisited bool `mapstructure:"show_last_visited" yaml:"show_last_visited" toml:"show_last_visited"` | |
| 292 | 292 | HistoryPrefix string `mapstructure:"history_prefix" yaml:"history_prefix" toml:"history_prefix"` | |
| @@ -286,7 +286,7 @@ func (c *Config) ShortcutURLs() map[string]string { | |||||
| 286 | 286 | ||||
| 287 | // DmenuConfig holds dmenu/rofi integration configuration. | 287 | // DmenuConfig holds dmenu/rofi integration configuration. | ||
| 288 | type DmenuConfig struct { | 288 | type DmenuConfig struct { | ||
| 289 | - | MaxHistoryItems int `mapstructure:"max_history_items" yaml:"max_history_items" toml:"max_history_items"` | 289 | + | MaxHistoryDays int `mapstructure:"max_history_days" yaml:"max_history_days" toml:"max_history_days"` |
| 290 | ShowVisitCount bool `mapstructure:"show_visit_count" yaml:"show_visit_count" toml:"show_visit_count"` | 290 | ShowVisitCount bool `mapstructure:"show_visit_count" yaml:"show_visit_count" toml:"show_visit_count"` | ||
| 291 | ShowLastVisited bool `mapstructure:"show_last_visited" yaml:"show_last_visited" toml:"show_last_visited"` | 291 | ShowLastVisited bool `mapstructure:"show_last_visited" yaml:"show_last_visited" toml:"show_last_visited"` | ||
| 292 | HistoryPrefix string `mapstructure:"history_prefix" yaml:"history_prefix" toml:"history_prefix"` | 292 | HistoryPrefix string `mapstructure:"history_prefix" yaml:"history_prefix" toml:"history_prefix"` | ||
M internal/infrastructure/config/schema_provider.go +3 -3
| @@ -264,10 +264,10 @@ func (*SchemaProvider) getSearchKeys(defaults *Config) []entity.ConfigKeyInfo { | |||
| 264 | 264 | func (*SchemaProvider) getDmenuKeys(defaults *Config) []entity.ConfigKeyInfo { | |
| 265 | 265 | return []entity.ConfigKeyInfo{ | |
| 266 | 266 | { | |
| 267 | - | Key: "dmenu.max_history_items", | |
| 267 | + | Key: "dmenu.max_history_days", | |
| 268 | 268 | Type: "int", | |
| 269 | - | Default: fmt.Sprintf("%d", defaults.Dmenu.MaxHistoryItems), | |
| 270 | - | Description: "Maximum history items shown in dmenu", | |
| 269 | + | Default: fmt.Sprintf("%d", defaults.Dmenu.MaxHistoryDays), | |
| 270 | + | Description: "Number of days of history to show in dmenu (0 = all)", | |
| 271 | 271 | Range: ">=0", | |
| 272 | 272 | Section: SectionDmenu, | |
| 273 | 273 | }, | |
| @@ -264,10 +264,10 @@ func (*SchemaProvider) getSearchKeys(defaults *Config) []entity.ConfigKeyInfo { | |||||
| 264 | func (*SchemaProvider) getDmenuKeys(defaults *Config) []entity.ConfigKeyInfo { | 264 | func (*SchemaProvider) getDmenuKeys(defaults *Config) []entity.ConfigKeyInfo { | ||
| 265 | return []entity.ConfigKeyInfo{ | 265 | return []entity.ConfigKeyInfo{ | ||
| 266 | { | 266 | { | ||
| 267 | - | Key: "dmenu.max_history_items", | 267 | + | Key: "dmenu.max_history_days", |
| 268 | Type: "int", | 268 | Type: "int", | ||
| 269 | - | Default: fmt.Sprintf("%d", defaults.Dmenu.MaxHistoryItems), | 269 | + | Default: fmt.Sprintf("%d", defaults.Dmenu.MaxHistoryDays), |
| 270 | - | Description: "Maximum history items shown in dmenu", | 270 | + | Description: "Number of days of history to show in dmenu (0 = all)", |
| 271 | Range: ">=0", | 271 | Range: ">=0", | ||
| 272 | Section: SectionDmenu, | 272 | Section: SectionDmenu, | ||
| 273 | }, | 273 | }, | ||
M internal/infrastructure/config/validation.go +2 -2
| @@ -53,8 +53,8 @@ func validateHistory(config *Config) []string { | |||
| 53 | 53 | } | |
| 54 | 54 | ||
| 55 | 55 | func validateDmenu(config *Config) []string { | |
| 56 | - | if config.Dmenu.MaxHistoryItems < 0 { | |
| 57 | - | return []string{"dmenu.max_history_items must be non-negative"} | |
| 56 | + | if config.Dmenu.MaxHistoryDays < 0 { | |
| 57 | + | return []string{"dmenu.max_history_days must be non-negative"} | |
| 58 | 58 | } | |
| 59 | 59 | return nil | |
| 60 | 60 | } | |
| @@ -53,8 +53,8 @@ func validateHistory(config *Config) []string { | |||||
| 53 | } | 53 | } | ||
| 54 | 54 | ||||
| 55 | func validateDmenu(config *Config) []string { | 55 | func validateDmenu(config *Config) []string { | ||
| 56 | - | if config.Dmenu.MaxHistoryItems < 0 { | 56 | + | if config.Dmenu.MaxHistoryDays < 0 { |
| 57 | - | return []string{"dmenu.max_history_items must be non-negative"} | 57 | + | return []string{"dmenu.max_history_days must be non-negative"} |
| 58 | } | 58 | } | ||
| 59 | return nil | 59 | return nil | ||
| 60 | } | 60 | } | ||
M internal/infrastructure/persistence/sqlite/history_repo.go +63 -0
| @@ -4,6 +4,7 @@ import ( | |||
| 4 | 4 | "context" | |
| 5 | 5 | "database/sql" | |
| 6 | 6 | "errors" | |
| 7 | + | "fmt" | |
| 7 | 8 | "time" | |
| 8 | 9 | ||
| 9 | 10 | "github.com/bnema/dumber/internal/domain/entity" | |
| @@ -87,6 +88,68 @@ func (r *historyRepo) GetRecent(ctx context.Context, limit, offset int) ([]*enti | |||
| 87 | 88 | return entries, nil | |
| 88 | 89 | } | |
| 89 | 90 | ||
| 91 | + | func (r *historyRepo) GetRecentSince(ctx context.Context, days int) ([]*entity.HistoryEntry, error) { | |
| 92 | + | if days <= 0 { | |
| 93 | + | return nil, fmt.Errorf("days must be positive, got %d", days) | |
| 94 | + | } | |
| 95 | + | // Format: "-N days" for SQLite datetime modifier | |
| 96 | + | daysModifier := fmt.Sprintf("-%d days", days) | |
| 97 | + | rows, err := r.queries.GetRecentHistorySince(ctx, daysModifier) | |
| 98 | + | if err != nil { | |
| 99 | + | return nil, err | |
| 100 | + | } | |
| 101 | + | ||
| 102 | + | entries := make([]*entity.HistoryEntry, len(rows)) | |
| 103 | + | for i := range rows { | |
| 104 | + | entries[i] = historyFromRow(rows[i]) | |
| 105 | + | } | |
| 106 | + | return entries, nil | |
| 107 | + | } | |
| 108 | + | ||
| 109 | + | func (r *historyRepo) GetMostVisited(ctx context.Context, days int) ([]*entity.HistoryEntry, error) { | |
| 110 | + | if days <= 0 { | |
| 111 | + | return nil, fmt.Errorf("days must be positive, got %d", days) | |
| 112 | + | } | |
| 113 | + | // Format: "-N days" for SQLite datetime modifier | |
| 114 | + | daysModifier := fmt.Sprintf("-%d days", days) | |
| 115 | + | rows, err := r.queries.GetMostVisited(ctx, daysModifier) | |
| 116 | + | if err != nil { | |
| 117 | + | return nil, err | |
| 118 | + | } | |
| 119 | + | ||
| 120 | + | entries := make([]*entity.HistoryEntry, len(rows)) | |
| 121 | + | for i := range rows { | |
| 122 | + | entries[i] = historyFromRow(rows[i]) | |
| 123 | + | } | |
| 124 | + | return entries, nil | |
| 125 | + | } | |
| 126 | + | ||
| 127 | + | func (r *historyRepo) GetAllRecentHistory(ctx context.Context) ([]*entity.HistoryEntry, error) { | |
| 128 | + | rows, err := r.queries.GetAllRecentHistory(ctx) | |
| 129 | + | if err != nil { | |
| 130 | + | return nil, err | |
| 131 | + | } | |
| 132 | + | ||
| 133 | + | entries := make([]*entity.HistoryEntry, len(rows)) | |
| 134 | + | for i := range rows { | |
| 135 | + | entries[i] = historyFromRow(rows[i]) | |
| 136 | + | } | |
| 137 | + | return entries, nil | |
| 138 | + | } | |
| 139 | + | ||
| 140 | + | func (r *historyRepo) GetAllMostVisited(ctx context.Context) ([]*entity.HistoryEntry, error) { | |
| 141 | + | rows, err := r.queries.GetAllMostVisited(ctx) | |
| 142 | + | if err != nil { | |
| 143 | + | return nil, err | |
| 144 | + | } | |
| 145 | + | ||
| 146 | + | entries := make([]*entity.HistoryEntry, len(rows)) | |
| 147 | + | for i := range rows { | |
| 148 | + | entries[i] = historyFromRow(rows[i]) | |
| 149 | + | } | |
| 150 | + | return entries, nil | |
| 151 | + | } | |
| 152 | + | ||
| 90 | 153 | func (r *historyRepo) IncrementVisitCount(ctx context.Context, url string) error { | |
| 91 | 154 | return r.queries.IncrementVisitCount(ctx, url) | |
| 92 | 155 | } | |
| @@ -4,6 +4,7 @@ import ( | |||||
| 4 | "context" | 4 | "context" | ||
| 5 | "database/sql" | 5 | "database/sql" | ||
| 6 | "errors" | 6 | "errors" | ||
| 7 | + | "fmt" | |||
| 7 | "time" | 8 | "time" | ||
| 8 | 9 | ||||
| 9 | "github.com/bnema/dumber/internal/domain/entity" | 10 | "github.com/bnema/dumber/internal/domain/entity" | ||
| @@ -87,6 +88,68 @@ func (r *historyRepo) GetRecent(ctx context.Context, limit, offset int) ([]*enti | |||||
| 87 | return entries, nil | 88 | return entries, nil | ||
| 88 | } | 89 | } | ||
| 89 | 90 | ||||
| 91 | + | func (r *historyRepo) GetRecentSince(ctx context.Context, days int) ([]*entity.HistoryEntry, error) { | |||
| 92 | + | if days <= 0 { | |||
| 93 | + | return nil, fmt.Errorf("days must be positive, got %d", days) | |||
| 94 | + | } | |||
| 95 | + | // Format: "-N days" for SQLite datetime modifier | |||
| 96 | + | daysModifier := fmt.Sprintf("-%d days", days) | |||
| 97 | + | rows, err := r.queries.GetRecentHistorySince(ctx, daysModifier) | |||
| 98 | + | if err != nil { | |||
| 99 | + | return nil, err | |||
| 100 | + | } | |||
| 101 | + | ||||
| 102 | + | entries := make([]*entity.HistoryEntry, len(rows)) | |||
| 103 | + | for i := range rows { | |||
| 104 | + | entries[i] = historyFromRow(rows[i]) | |||
| 105 | + | } | |||
| 106 | + | return entries, nil | |||
| 107 | + | } | |||
| 108 | + | ||||
| 109 | + | func (r *historyRepo) GetMostVisited(ctx context.Context, days int) ([]*entity.HistoryEntry, error) { | |||
| 110 | + | if days <= 0 { | |||
| 111 | + | return nil, fmt.Errorf("days must be positive, got %d", days) | |||
| 112 | + | } | |||
| 113 | + | // Format: "-N days" for SQLite datetime modifier | |||
| 114 | + | daysModifier := fmt.Sprintf("-%d days", days) | |||
| 115 | + | rows, err := r.queries.GetMostVisited(ctx, daysModifier) | |||
| 116 | + | if err != nil { | |||
| 117 | + | return nil, err | |||
| 118 | + | } | |||
| 119 | + | ||||
| 120 | + | entries := make([]*entity.HistoryEntry, len(rows)) | |||
| 121 | + | for i := range rows { | |||
| 122 | + | entries[i] = historyFromRow(rows[i]) | |||
| 123 | + | } | |||
| 124 | + | return entries, nil | |||
| 125 | + | } | |||
| 126 | + | ||||
| 127 | + | func (r *historyRepo) GetAllRecentHistory(ctx context.Context) ([]*entity.HistoryEntry, error) { | |||
| 128 | + | rows, err := r.queries.GetAllRecentHistory(ctx) | |||
| 129 | + | if err != nil { | |||
| 130 | + | return nil, err | |||
| 131 | + | } | |||
| 132 | + | ||||
| 133 | + | entries := make([]*entity.HistoryEntry, len(rows)) | |||
| 134 | + | for i := range rows { | |||
| 135 | + | entries[i] = historyFromRow(rows[i]) | |||
| 136 | + | } | |||
| 137 | + | return entries, nil | |||
| 138 | + | } | |||
| 139 | + | ||||
| 140 | + | func (r *historyRepo) GetAllMostVisited(ctx context.Context) ([]*entity.HistoryEntry, error) { | |||
| 141 | + | rows, err := r.queries.GetAllMostVisited(ctx) | |||
| 142 | + | if err != nil { | |||
| 143 | + | return nil, err | |||
| 144 | + | } | |||
| 145 | + | ||||
| 146 | + | entries := make([]*entity.HistoryEntry, len(rows)) | |||
| 147 | + | for i := range rows { | |||
| 148 | + | entries[i] = historyFromRow(rows[i]) | |||
| 149 | + | } | |||
| 150 | + | return entries, nil | |||
| 151 | + | } | |||
| 152 | + | ||||
| 90 | func (r *historyRepo) IncrementVisitCount(ctx context.Context, url string) error { | 153 | func (r *historyRepo) IncrementVisitCount(ctx context.Context, url string) error { | ||
| 91 | return r.queries.IncrementVisitCount(ctx, url) | 154 | return r.queries.IncrementVisitCount(ctx, url) | ||
| 92 | } | 155 | } | ||
A internal/infrastructure/persistence/sqlite/history_repo_test.go +325 -0
| @@ -0,0 +1,325 @@ | |||
| 1 | + | package sqlite_test | |
| 2 | + | ||
| 3 | + | import ( | |
| 4 | + | "context" | |
| 5 | + | "path/filepath" | |
| 6 | + | "testing" | |
| 7 | + | "time" | |
| 8 | + | ||
| 9 | + | "github.com/bnema/dumber/internal/domain/entity" | |
| 10 | + | "github.com/bnema/dumber/internal/infrastructure/persistence/sqlite" | |
| 11 | + | "github.com/bnema/dumber/internal/logging" | |
| 12 | + | "github.com/stretchr/testify/assert" | |
| 13 | + | "github.com/stretchr/testify/require" | |
| 14 | + | ) | |
| 15 | + | ||
| 16 | + | func historyTestCtx() context.Context { | |
| 17 | + | logger := logging.NewFromConfigValues("debug", "console") | |
| 18 | + | return logging.WithContext(context.Background(), logger) | |
| 19 | + | } | |
| 20 | + | ||
| 21 | + | func TestHistoryRepository_GetRecentSince(t *testing.T) { | |
| 22 | + | ctx := historyTestCtx() | |
| 23 | + | dbPath := filepath.Join(t.TempDir(), "dumber.db") | |
| 24 | + | ||
| 25 | + | db, err := sqlite.NewConnection(ctx, dbPath) | |
| 26 | + | require.NoError(t, err) | |
| 27 | + | t.Cleanup(func() { _ = db.Close() }) | |
| 28 | + | ||
| 29 | + | repo := sqlite.NewHistoryRepository(db) | |
| 30 | + | ||
| 31 | + | // Save some history entries | |
| 32 | + | entries := []*entity.HistoryEntry{ | |
| 33 | + | {URL: "https://example.com", Title: "Example"}, | |
| 34 | + | {URL: "https://github.com", Title: "GitHub"}, | |
| 35 | + | {URL: "https://google.com", Title: "Google"}, | |
| 36 | + | } | |
| 37 | + | ||
| 38 | + | for _, e := range entries { | |
| 39 | + | require.NoError(t, repo.Save(ctx, e)) | |
| 40 | + | } | |
| 41 | + | ||
| 42 | + | // Get recent since 30 days (all should be included) | |
| 43 | + | results, err := repo.GetRecentSince(ctx, 30) | |
| 44 | + | require.NoError(t, err) | |
| 45 | + | require.Len(t, results, 3) | |
| 46 | + | ||
| 47 | + | // Results should be sorted by last_visited DESC | |
| 48 | + | // Since they were all just inserted, order may vary | |
| 49 | + | urls := make(map[string]bool) | |
| 50 | + | for _, r := range results { | |
| 51 | + | urls[r.URL] = true | |
| 52 | + | } | |
| 53 | + | assert.True(t, urls["https://example.com"]) | |
| 54 | + | assert.True(t, urls["https://github.com"]) | |
| 55 | + | assert.True(t, urls["https://google.com"]) | |
| 56 | + | } | |
| 57 | + | ||
| 58 | + | func TestHistoryRepository_GetRecentSince_EmptyResult(t *testing.T) { | |
| 59 | + | ctx := historyTestCtx() | |
| 60 | + | dbPath := filepath.Join(t.TempDir(), "dumber.db") | |
| 61 | + | ||
| 62 | + | db, err := sqlite.NewConnection(ctx, dbPath) | |
| 63 | + | require.NoError(t, err) | |
| 64 | + | t.Cleanup(func() { _ = db.Close() }) | |
| 65 | + | ||
| 66 | + | repo := sqlite.NewHistoryRepository(db) | |
| 67 | + | ||
| 68 | + | // Get recent with no data | |
| 69 | + | results, err := repo.GetRecentSince(ctx, 30) | |
| 70 | + | require.NoError(t, err) | |
| 71 | + | assert.Empty(t, results) | |
| 72 | + | } | |
| 73 | + | ||
| 74 | + | func TestHistoryRepository_GetMostVisited(t *testing.T) { | |
| 75 | + | ctx := historyTestCtx() | |
| 76 | + | dbPath := filepath.Join(t.TempDir(), "dumber.db") | |
| 77 | + | ||
| 78 | + | db, err := sqlite.NewConnection(ctx, dbPath) | |
| 79 | + | require.NoError(t, err) | |
| 80 | + | t.Cleanup(func() { _ = db.Close() }) | |
| 81 | + | ||
| 82 | + | repo := sqlite.NewHistoryRepository(db) | |
| 83 | + | ||
| 84 | + | // Save entries with different visit counts | |
| 85 | + | entry1 := &entity.HistoryEntry{URL: "https://example.com", Title: "Example"} | |
| 86 | + | entry2 := &entity.HistoryEntry{URL: "https://github.com", Title: "GitHub"} | |
| 87 | + | entry3 := &entity.HistoryEntry{URL: "https://google.com", Title: "Google"} | |
| 88 | + | ||
| 89 | + | require.NoError(t, repo.Save(ctx, entry1)) | |
| 90 | + | require.NoError(t, repo.Save(ctx, entry2)) | |
| 91 | + | require.NoError(t, repo.Save(ctx, entry3)) | |
| 92 | + | ||
| 93 | + | // Increment visit counts to create different popularities | |
| 94 | + | // github: 5 visits, google: 3 visits, example: 1 visit | |
| 95 | + | for i := 0; i < 4; i++ { | |
| 96 | + | require.NoError(t, repo.IncrementVisitCount(ctx, "https://github.com")) | |
| 97 | + | } | |
| 98 | + | for i := 0; i < 2; i++ { | |
| 99 | + | require.NoError(t, repo.IncrementVisitCount(ctx, "https://google.com")) | |
| 100 | + | } | |
| 101 | + | ||
| 102 | + | // Get most visited | |
| 103 | + | results, err := repo.GetMostVisited(ctx, 30) | |
| 104 | + | require.NoError(t, err) | |
| 105 | + | require.Len(t, results, 3) | |
| 106 | + | ||
| 107 | + | // First result should be github (5 visits) | |
| 108 | + | assert.Equal(t, "https://github.com", results[0].URL) | |
| 109 | + | assert.Equal(t, int64(5), results[0].VisitCount) | |
| 110 | + | ||
| 111 | + | // Second should be google (3 visits) | |
| 112 | + | assert.Equal(t, "https://google.com", results[1].URL) | |
| 113 | + | assert.Equal(t, int64(3), results[1].VisitCount) | |
| 114 | + | ||
| 115 | + | // Third should be example (1 visit) | |
| 116 | + | assert.Equal(t, "https://example.com", results[2].URL) | |
| 117 | + | assert.Equal(t, int64(1), results[2].VisitCount) | |
| 118 | + | } | |
| 119 | + | ||
| 120 | + | func TestHistoryRepository_GetMostVisited_EmptyResult(t *testing.T) { | |
| 121 | + | ctx := historyTestCtx() | |
| 122 | + | dbPath := filepath.Join(t.TempDir(), "dumber.db") | |
| 123 | + | ||
| 124 | + | db, err := sqlite.NewConnection(ctx, dbPath) | |
| 125 | + | require.NoError(t, err) | |
| 126 | + | t.Cleanup(func() { _ = db.Close() }) | |
| 127 | + | ||
| 128 | + | repo := sqlite.NewHistoryRepository(db) | |
| 129 | + | ||
| 130 | + | // Get most visited with no data | |
| 131 | + | results, err := repo.GetMostVisited(ctx, 30) | |
| 132 | + | require.NoError(t, err) | |
| 133 | + | assert.Empty(t, results) | |
| 134 | + | } | |
| 135 | + | ||
| 136 | + | func TestHistoryRepository_GetRecentSince_RespectsTimeFilter(t *testing.T) { | |
| 137 | + | ctx := historyTestCtx() | |
| 138 | + | dbPath := filepath.Join(t.TempDir(), "dumber.db") | |
| 139 | + | ||
| 140 | + | db, err := sqlite.NewConnection(ctx, dbPath) | |
| 141 | + | require.NoError(t, err) | |
| 142 | + | t.Cleanup(func() { _ = db.Close() }) | |
| 143 | + | ||
| 144 | + | repo := sqlite.NewHistoryRepository(db) | |
| 145 | + | ||
| 146 | + | // Save an entry | |
| 147 | + | entry := &entity.HistoryEntry{URL: "https://example.com", Title: "Example"} | |
| 148 | + | require.NoError(t, repo.Save(ctx, entry)) | |
| 149 | + | ||
| 150 | + | // Query with 1 day filter - should include recently added entry | |
| 151 | + | results, err := repo.GetRecentSince(ctx, 1) | |
| 152 | + | require.NoError(t, err) | |
| 153 | + | require.Len(t, results, 1) | |
| 154 | + | ||
| 155 | + | // Query with very large days filter - should still work | |
| 156 | + | results, err = repo.GetRecentSince(ctx, 365) | |
| 157 | + | require.NoError(t, err) | |
| 158 | + | require.Len(t, results, 1) | |
| 159 | + | } | |
| 160 | + | ||
| 161 | + | func TestHistoryRepository_CRUD(t *testing.T) { | |
| 162 | + | ctx := historyTestCtx() | |
| 163 | + | dbPath := filepath.Join(t.TempDir(), "dumber.db") | |
| 164 | + | ||
| 165 | + | db, err := sqlite.NewConnection(ctx, dbPath) | |
| 166 | + | require.NoError(t, err) | |
| 167 | + | t.Cleanup(func() { _ = db.Close() }) | |
| 168 | + | ||
| 169 | + | repo := sqlite.NewHistoryRepository(db) | |
| 170 | + | ||
| 171 | + | // Create | |
| 172 | + | entry := &entity.HistoryEntry{URL: "https://example.com", Title: "Example"} | |
| 173 | + | require.NoError(t, repo.Save(ctx, entry)) | |
| 174 | + | ||
| 175 | + | // Read | |
| 176 | + | found, err := repo.FindByURL(ctx, "https://example.com") | |
| 177 | + | require.NoError(t, err) | |
| 178 | + | require.NotNil(t, found) | |
| 179 | + | assert.Equal(t, "https://example.com", found.URL) | |
| 180 | + | assert.Equal(t, "Example", found.Title) | |
| 181 | + | assert.Equal(t, int64(1), found.VisitCount) | |
| 182 | + | ||
| 183 | + | // Update (save again increments visit count) | |
| 184 | + | require.NoError(t, repo.Save(ctx, entry)) | |
| 185 | + | found2, err := repo.FindByURL(ctx, "https://example.com") | |
| 186 | + | require.NoError(t, err) | |
| 187 | + | assert.Equal(t, int64(2), found2.VisitCount) | |
| 188 | + | ||
| 189 | + | // Get recent | |
| 190 | + | recent, err := repo.GetRecent(ctx, 10, 0) | |
| 191 | + | require.NoError(t, err) | |
| 192 | + | require.Len(t, recent, 1) | |
| 193 | + | ||
| 194 | + | // Delete | |
| 195 | + | require.NoError(t, repo.Delete(ctx, found.ID)) | |
| 196 | + | ||
| 197 | + | // Verify deleted | |
| 198 | + | deleted, err := repo.FindByURL(ctx, "https://example.com") | |
| 199 | + | require.NoError(t, err) | |
| 200 | + | assert.Nil(t, deleted) | |
| 201 | + | } | |
| 202 | + | ||
| 203 | + | // Ensure the test uses an isolated timestamp for time-sensitive queries | |
| 204 | + | func TestHistoryRepository_TimeFilterIntegrity(t *testing.T) { | |
| 205 | + | ctx := historyTestCtx() | |
| 206 | + | dbPath := filepath.Join(t.TempDir(), "dumber.db") | |
| 207 | + | ||
| 208 | + | db, err := sqlite.NewConnection(ctx, dbPath) | |
| 209 | + | require.NoError(t, err) | |
| 210 | + | t.Cleanup(func() { _ = db.Close() }) | |
| 211 | + | ||
| 212 | + | repo := sqlite.NewHistoryRepository(db) | |
| 213 | + | ||
| 214 | + | // Save entry | |
| 215 | + | entry := &entity.HistoryEntry{URL: "https://test.com", Title: "Test"} | |
| 216 | + | require.NoError(t, repo.Save(ctx, entry)) | |
| 217 | + | ||
| 218 | + | // Verify entry was saved with recent timestamp | |
| 219 | + | found, err := repo.FindByURL(ctx, "https://test.com") | |
| 220 | + | require.NoError(t, err) | |
| 221 | + | require.NotNil(t, found) | |
| 222 | + | ||
| 223 | + | // LastVisited should be within the last minute | |
| 224 | + | timeDiff := time.Since(found.LastVisited) | |
| 225 | + | assert.Less(t, timeDiff, time.Minute, "LastVisited should be recent, got %v ago", timeDiff) | |
| 226 | + | } | |
| 227 | + | ||
| 228 | + | func TestHistoryRepository_GetRecentSince_RejectsNonPositiveDays(t *testing.T) { | |
| 229 | + | ctx := historyTestCtx() | |
| 230 | + | dbPath := filepath.Join(t.TempDir(), "dumber.db") | |
| 231 | + | ||
| 232 | + | db, err := sqlite.NewConnection(ctx, dbPath) | |
| 233 | + | require.NoError(t, err) | |
| 234 | + | t.Cleanup(func() { _ = db.Close() }) | |
| 235 | + | ||
| 236 | + | repo := sqlite.NewHistoryRepository(db) | |
| 237 | + | ||
| 238 | + | // days = 0 should return error | |
| 239 | + | _, err = repo.GetRecentSince(ctx, 0) | |
| 240 | + | require.Error(t, err) | |
| 241 | + | assert.Contains(t, err.Error(), "days must be positive") | |
| 242 | + | ||
| 243 | + | // days < 0 should return error | |
| 244 | + | _, err = repo.GetRecentSince(ctx, -5) | |
| 245 | + | require.Error(t, err) | |
| 246 | + | assert.Contains(t, err.Error(), "days must be positive") | |
| 247 | + | } | |
| 248 | + | ||
| 249 | + | func TestHistoryRepository_GetMostVisited_RejectsNonPositiveDays(t *testing.T) { | |
| 250 | + | ctx := historyTestCtx() | |
| 251 | + | dbPath := filepath.Join(t.TempDir(), "dumber.db") | |
| 252 | + | ||
| 253 | + | db, err := sqlite.NewConnection(ctx, dbPath) | |
| 254 | + | require.NoError(t, err) | |
| 255 | + | t.Cleanup(func() { _ = db.Close() }) | |
| 256 | + | ||
| 257 | + | repo := sqlite.NewHistoryRepository(db) | |
| 258 | + | ||
| 259 | + | // days = 0 should return error | |
| 260 | + | _, err = repo.GetMostVisited(ctx, 0) | |
| 261 | + | require.Error(t, err) | |
| 262 | + | assert.Contains(t, err.Error(), "days must be positive") | |
| 263 | + | ||
| 264 | + | // days < 0 should return error | |
| 265 | + | _, err = repo.GetMostVisited(ctx, -5) | |
| 266 | + | require.Error(t, err) | |
| 267 | + | assert.Contains(t, err.Error(), "days must be positive") | |
| 268 | + | } | |
| 269 | + | ||
| 270 | + | func TestHistoryRepository_GetAllRecentHistory(t *testing.T) { | |
| 271 | + | ctx := historyTestCtx() | |
| 272 | + | dbPath := filepath.Join(t.TempDir(), "dumber.db") | |
| 273 | + | ||
| 274 | + | db, err := sqlite.NewConnection(ctx, dbPath) | |
| 275 | + | require.NoError(t, err) | |
| 276 | + | t.Cleanup(func() { _ = db.Close() }) | |
| 277 | + | ||
| 278 | + | repo := sqlite.NewHistoryRepository(db) | |
| 279 | + | ||
| 280 | + | // Save some entries | |
| 281 | + | entries := []*entity.HistoryEntry{ | |
| 282 | + | {URL: "https://example.com", Title: "Example"}, | |
| 283 | + | {URL: "https://github.com", Title: "GitHub"}, | |
| 284 | + | } | |
| 285 | + | for _, e := range entries { | |
| 286 | + | require.NoError(t, repo.Save(ctx, e)) | |
| 287 | + | } | |
| 288 | + | ||
| 289 | + | // Get all recent history | |
| 290 | + | results, err := repo.GetAllRecentHistory(ctx) | |
| 291 | + | require.NoError(t, err) | |
| 292 | + | require.Len(t, results, 2) | |
| 293 | + | } | |
| 294 | + | ||
| 295 | + | func TestHistoryRepository_GetAllMostVisited(t *testing.T) { | |
| 296 | + | ctx := historyTestCtx() | |
| 297 | + | dbPath := filepath.Join(t.TempDir(), "dumber.db") | |
| 298 | + | ||
| 299 | + | db, err := sqlite.NewConnection(ctx, dbPath) | |
| 300 | + | require.NoError(t, err) | |
| 301 | + | t.Cleanup(func() { _ = db.Close() }) | |
| 302 | + | ||
| 303 | + | repo := sqlite.NewHistoryRepository(db) | |
| 304 | + | ||
| 305 | + | // Save entries with different visit counts | |
| 306 | + | entry1 := &entity.HistoryEntry{URL: "https://example.com", Title: "Example"} | |
| 307 | + | entry2 := &entity.HistoryEntry{URL: "https://github.com", Title: "GitHub"} | |
| 308 | + | ||
| 309 | + | require.NoError(t, repo.Save(ctx, entry1)) | |
| 310 | + | require.NoError(t, repo.Save(ctx, entry2)) | |
| 311 | + | ||
| 312 | + | // Increment github visits | |
| 313 | + | for i := 0; i < 3; i++ { | |
| 314 | + | require.NoError(t, repo.IncrementVisitCount(ctx, "https://github.com")) | |
| 315 | + | } | |
| 316 | + | ||
| 317 | + | // Get all most visited | |
| 318 | + | results, err := repo.GetAllMostVisited(ctx) | |
| 319 | + | require.NoError(t, err) | |
| 320 | + | require.Len(t, results, 2) | |
| 321 | + | ||
| 322 | + | // First should be github (4 visits) | |
| 323 | + | assert.Equal(t, "https://github.com", results[0].URL) | |
| 324 | + | assert.Equal(t, int64(4), results[0].VisitCount) | |
| 325 | + | } | |
| @@ -0,0 +1,325 @@ | |||||
| 1 | + | package sqlite_test | |||
| 2 | + | ||||
| 3 | + | import ( | |||
| 4 | + | "context" | |||
| 5 | + | "path/filepath" | |||
| 6 | + | "testing" | |||
| 7 | + | "time" | |||
| 8 | + | ||||
| 9 | + | "github.com/bnema/dumber/internal/domain/entity" | |||
| 10 | + | "github.com/bnema/dumber/internal/infrastructure/persistence/sqlite" | |||
| 11 | + | "github.com/bnema/dumber/internal/logging" | |||
| 12 | + | "github.com/stretchr/testify/assert" | |||
| 13 | + | "github.com/stretchr/testify/require" | |||
| 14 | + | ) | |||
| 15 | + | ||||
| 16 | + | func historyTestCtx() context.Context { | |||
| 17 | + | logger := logging.NewFromConfigValues("debug", "console") | |||
| 18 | + | return logging.WithContext(context.Background(), logger) | |||
| 19 | + | } | |||
| 20 | + | ||||
| 21 | + | func TestHistoryRepository_GetRecentSince(t *testing.T) { | |||
| 22 | + | ctx := historyTestCtx() | |||
| 23 | + | dbPath := filepath.Join(t.TempDir(), "dumber.db") | |||
| 24 | + | ||||
| 25 | + | db, err := sqlite.NewConnection(ctx, dbPath) | |||
| 26 | + | require.NoError(t, err) | |||
| 27 | + | t.Cleanup(func() { _ = db.Close() }) | |||
| 28 | + | ||||
| 29 | + | repo := sqlite.NewHistoryRepository(db) | |||
| 30 | + | ||||
| 31 | + | // Save some history entries | |||
| 32 | + | entries := []*entity.HistoryEntry{ | |||
| 33 | + | {URL: "https://example.com", Title: "Example"}, | |||
| 34 | + | {URL: "https://github.com", Title: "GitHub"}, | |||
| 35 | + | {URL: "https://google.com", Title: "Google"}, | |||
| 36 | + | } | |||
| 37 | + | ||||
| 38 | + | for _, e := range entries { | |||
| 39 | + | require.NoError(t, repo.Save(ctx, e)) | |||
| 40 | + | } | |||
| 41 | + | ||||
| 42 | + | // Get recent since 30 days (all should be included) | |||
| 43 | + | results, err := repo.GetRecentSince(ctx, 30) | |||
| 44 | + | require.NoError(t, err) | |||
| 45 | + | require.Len(t, results, 3) | |||
| 46 | + | ||||
| 47 | + | // Results should be sorted by last_visited DESC | |||
| 48 | + | // Since they were all just inserted, order may vary | |||
| 49 | + | urls := make(map[string]bool) | |||
| 50 | + | for _, r := range results { | |||
| 51 | + | urls[r.URL] = true | |||
| 52 | + | } | |||
| 53 | + | assert.True(t, urls["https://example.com"]) | |||
| 54 | + | assert.True(t, urls["https://github.com"]) | |||
| 55 | + | assert.True(t, urls["https://google.com"]) | |||
| 56 | + | } | |||
| 57 | + | ||||
| 58 | + | func TestHistoryRepository_GetRecentSince_EmptyResult(t *testing.T) { | |||
| 59 | + | ctx := historyTestCtx() | |||
| 60 | + | dbPath := filepath.Join(t.TempDir(), "dumber.db") | |||
| 61 | + | ||||
| 62 | + | db, err := sqlite.NewConnection(ctx, dbPath) | |||
| 63 | + | require.NoError(t, err) | |||
| 64 | + | t.Cleanup(func() { _ = db.Close() }) | |||
| 65 | + | ||||
| 66 | + | repo := sqlite.NewHistoryRepository(db) | |||
| 67 | + | ||||
| 68 | + | // Get recent with no data | |||
| 69 | + | results, err := repo.GetRecentSince(ctx, 30) | |||
| 70 | + | require.NoError(t, err) | |||
| 71 | + | assert.Empty(t, results) | |||
| 72 | + | } | |||
| 73 | + | ||||
| 74 | + | func TestHistoryRepository_GetMostVisited(t *testing.T) { | |||
| 75 | + | ctx := historyTestCtx() | |||
| 76 | + | dbPath := filepath.Join(t.TempDir(), "dumber.db") | |||
| 77 | + | ||||
| 78 | + | db, err := sqlite.NewConnection(ctx, dbPath) | |||
| 79 | + | require.NoError(t, err) | |||
| 80 | + | t.Cleanup(func() { _ = db.Close() }) | |||
| 81 | + | ||||
| 82 | + | repo := sqlite.NewHistoryRepository(db) | |||
| 83 | + | ||||
| 84 | + | // Save entries with different visit counts | |||
| 85 | + | entry1 := &entity.HistoryEntry{URL: "https://example.com", Title: "Example"} | |||
| 86 | + | entry2 := &entity.HistoryEntry{URL: "https://github.com", Title: "GitHub"} | |||
| 87 | + | entry3 := &entity.HistoryEntry{URL: "https://google.com", Title: "Google"} | |||
| 88 | + | ||||
| 89 | + | require.NoError(t, repo.Save(ctx, entry1)) | |||
| 90 | + | require.NoError(t, repo.Save(ctx, entry2)) | |||
| 91 | + | require.NoError(t, repo.Save(ctx, entry3)) | |||
| 92 | + | ||||
| 93 | + | // Increment visit counts to create different popularities | |||
| 94 | + | // github: 5 visits, google: 3 visits, example: 1 visit | |||
| 95 | + | for i := 0; i < 4; i++ { | |||
| 96 | + | require.NoError(t, repo.IncrementVisitCount(ctx, "https://github.com")) | |||
| 97 | + | } | |||
| 98 | + | for i := 0; i < 2; i++ { | |||
| 99 | + | require.NoError(t, repo.IncrementVisitCount(ctx, "https://google.com")) | |||
| 100 | + | } | |||
| 101 | + | ||||
| 102 | + | // Get most visited | |||
| 103 | + | results, err := repo.GetMostVisited(ctx, 30) | |||
| 104 | + | require.NoError(t, err) | |||
| 105 | + | require.Len(t, results, 3) | |||
| 106 | + | ||||
| 107 | + | // First result should be github (5 visits) | |||
| 108 | + | assert.Equal(t, "https://github.com", results[0].URL) | |||
| 109 | + | assert.Equal(t, int64(5), results[0].VisitCount) | |||
| 110 | + | ||||
| 111 | + | // Second should be google (3 visits) | |||
| 112 | + | assert.Equal(t, "https://google.com", results[1].URL) | |||
| 113 | + | assert.Equal(t, int64(3), results[1].VisitCount) | |||
| 114 | + | ||||
| 115 | + | // Third should be example (1 visit) | |||
| 116 | + | assert.Equal(t, "https://example.com", results[2].URL) | |||
| 117 | + | assert.Equal(t, int64(1), results[2].VisitCount) | |||
| 118 | + | } | |||
| 119 | + | ||||
| 120 | + | func TestHistoryRepository_GetMostVisited_EmptyResult(t *testing.T) { | |||
| 121 | + | ctx := historyTestCtx() | |||
| 122 | + | dbPath := filepath.Join(t.TempDir(), "dumber.db") | |||
| 123 | + | ||||
| 124 | + | db, err := sqlite.NewConnection(ctx, dbPath) | |||
| 125 | + | require.NoError(t, err) | |||
| 126 | + | t.Cleanup(func() { _ = db.Close() }) | |||
| 127 | + | ||||
| 128 | + | repo := sqlite.NewHistoryRepository(db) | |||
| 129 | + | ||||
| 130 | + | // Get most visited with no data | |||
| 131 | + | results, err := repo.GetMostVisited(ctx, 30) | |||
| 132 | + | require.NoError(t, err) | |||
| 133 | + | assert.Empty(t, results) | |||
| 134 | + | } | |||
| 135 | + | ||||
| 136 | + | func TestHistoryRepository_GetRecentSince_RespectsTimeFilter(t *testing.T) { | |||
| 137 | + | ctx := historyTestCtx() | |||
| 138 | + | dbPath := filepath.Join(t.TempDir(), "dumber.db") | |||
| 139 | + | ||||
| 140 | + | db, err := sqlite.NewConnection(ctx, dbPath) | |||
| 141 | + | require.NoError(t, err) | |||
| 142 | + | t.Cleanup(func() { _ = db.Close() }) | |||
| 143 | + | ||||
| 144 | + | repo := sqlite.NewHistoryRepository(db) | |||
| 145 | + | ||||
| 146 | + | // Save an entry | |||
| 147 | + | entry := &entity.HistoryEntry{URL: "https://example.com", Title: "Example"} | |||
| 148 | + | require.NoError(t, repo.Save(ctx, entry)) | |||
| 149 | + | ||||
| 150 | + | // Query with 1 day filter - should include recently added entry | |||
| 151 | + | results, err := repo.GetRecentSince(ctx, 1) | |||
| 152 | + | require.NoError(t, err) | |||
| 153 | + | require.Len(t, results, 1) | |||
| 154 | + | ||||
| 155 | + | // Query with very large days filter - should still work | |||
| 156 | + | results, err = repo.GetRecentSince(ctx, 365) | |||
| 157 | + | require.NoError(t, err) | |||
| 158 | + | require.Len(t, results, 1) | |||
| 159 | + | } | |||
| 160 | + | ||||
| 161 | + | func TestHistoryRepository_CRUD(t *testing.T) { | |||
| 162 | + | ctx := historyTestCtx() | |||
| 163 | + | dbPath := filepath.Join(t.TempDir(), "dumber.db") | |||
| 164 | + | ||||
| 165 | + | db, err := sqlite.NewConnection(ctx, dbPath) | |||
| 166 | + | require.NoError(t, err) | |||
| 167 | + | t.Cleanup(func() { _ = db.Close() }) | |||
| 168 | + | ||||
| 169 | + | repo := sqlite.NewHistoryRepository(db) | |||
| 170 | + | ||||
| 171 | + | // Create | |||
| 172 | + | entry := &entity.HistoryEntry{URL: "https://example.com", Title: "Example"} | |||
| 173 | + | require.NoError(t, repo.Save(ctx, entry)) | |||
| 174 | + | ||||
| 175 | + | // Read | |||
| 176 | + | found, err := repo.FindByURL(ctx, "https://example.com") | |||
| 177 | + | require.NoError(t, err) | |||
| 178 | + | require.NotNil(t, found) | |||
| 179 | + | assert.Equal(t, "https://example.com", found.URL) | |||
| 180 | + | assert.Equal(t, "Example", found.Title) | |||
| 181 | + | assert.Equal(t, int64(1), found.VisitCount) | |||
| 182 | + | ||||
| 183 | + | // Update (save again increments visit count) | |||
| 184 | + | require.NoError(t, repo.Save(ctx, entry)) | |||
| 185 | + | found2, err := repo.FindByURL(ctx, "https://example.com") | |||
| 186 | + | require.NoError(t, err) | |||
| 187 | + | assert.Equal(t, int64(2), found2.VisitCount) | |||
| 188 | + | ||||
| 189 | + | // Get recent | |||
| 190 | + | recent, err := repo.GetRecent(ctx, 10, 0) | |||
| 191 | + | require.NoError(t, err) | |||
| 192 | + | require.Len(t, recent, 1) | |||
| 193 | + | ||||
| 194 | + | // Delete | |||
| 195 | + | require.NoError(t, repo.Delete(ctx, found.ID)) | |||
| 196 | + | ||||
| 197 | + | // Verify deleted | |||
| 198 | + | deleted, err := repo.FindByURL(ctx, "https://example.com") | |||
| 199 | + | require.NoError(t, err) | |||
| 200 | + | assert.Nil(t, deleted) | |||
| 201 | + | } | |||
| 202 | + | ||||
| 203 | + | // Ensure the test uses an isolated timestamp for time-sensitive queries | |||
| 204 | + | func TestHistoryRepository_TimeFilterIntegrity(t *testing.T) { | |||
| 205 | + | ctx := historyTestCtx() | |||
| 206 | + | dbPath := filepath.Join(t.TempDir(), "dumber.db") | |||
| 207 | + | ||||
| 208 | + | db, err := sqlite.NewConnection(ctx, dbPath) | |||
| 209 | + | require.NoError(t, err) | |||
| 210 | + | t.Cleanup(func() { _ = db.Close() }) | |||
| 211 | + | ||||
| 212 | + | repo := sqlite.NewHistoryRepository(db) | |||
| 213 | + | ||||
| 214 | + | // Save entry | |||
| 215 | + | entry := &entity.HistoryEntry{URL: "https://test.com", Title: "Test"} | |||
| 216 | + | require.NoError(t, repo.Save(ctx, entry)) | |||
| 217 | + | ||||
| 218 | + | // Verify entry was saved with recent timestamp | |||
| 219 | + | found, err := repo.FindByURL(ctx, "https://test.com") | |||
| 220 | + | require.NoError(t, err) | |||
| 221 | + | require.NotNil(t, found) | |||
| 222 | + | ||||
| 223 | + | // LastVisited should be within the last minute | |||
| 224 | + | timeDiff := time.Since(found.LastVisited) | |||
| 225 | + | assert.Less(t, timeDiff, time.Minute, "LastVisited should be recent, got %v ago", timeDiff) | |||
| 226 | + | } | |||
| 227 | + | ||||
| 228 | + | func TestHistoryRepository_GetRecentSince_RejectsNonPositiveDays(t *testing.T) { | |||
| 229 | + | ctx := historyTestCtx() | |||
| 230 | + | dbPath := filepath.Join(t.TempDir(), "dumber.db") | |||
| 231 | + | ||||
| 232 | + | db, err := sqlite.NewConnection(ctx, dbPath) | |||
| 233 | + | require.NoError(t, err) | |||
| 234 | + | t.Cleanup(func() { _ = db.Close() }) | |||
| 235 | + | ||||
| 236 | + | repo := sqlite.NewHistoryRepository(db) | |||
| 237 | + | ||||
| 238 | + | // days = 0 should return error | |||
| 239 | + | _, err = repo.GetRecentSince(ctx, 0) | |||
| 240 | + | require.Error(t, err) | |||
| 241 | + | assert.Contains(t, err.Error(), "days must be positive") | |||
| 242 | + | ||||
| 243 | + | // days < 0 should return error | |||
| 244 | + | _, err = repo.GetRecentSince(ctx, -5) | |||
| 245 | + | require.Error(t, err) | |||
| 246 | + | assert.Contains(t, err.Error(), "days must be positive") | |||
| 247 | + | } | |||
| 248 | + | ||||
| 249 | + | func TestHistoryRepository_GetMostVisited_RejectsNonPositiveDays(t *testing.T) { | |||
| 250 | + | ctx := historyTestCtx() | |||
| 251 | + | dbPath := filepath.Join(t.TempDir(), "dumber.db") | |||
| 252 | + | ||||
| 253 | + | db, err := sqlite.NewConnection(ctx, dbPath) | |||
| 254 | + | require.NoError(t, err) | |||
| 255 | + | t.Cleanup(func() { _ = db.Close() }) | |||
| 256 | + | ||||
| 257 | + | repo := sqlite.NewHistoryRepository(db) | |||
| 258 | + | ||||
| 259 | + | // days = 0 should return error | |||
| 260 | + | _, err = repo.GetMostVisited(ctx, 0) | |||
| 261 | + | require.Error(t, err) | |||
| 262 | + | assert.Contains(t, err.Error(), "days must be positive") | |||
| 263 | + | ||||
| 264 | + | // days < 0 should return error | |||
| 265 | + | _, err = repo.GetMostVisited(ctx, -5) | |||
| 266 | + | require.Error(t, err) | |||
| 267 | + | assert.Contains(t, err.Error(), "days must be positive") | |||
| 268 | + | } | |||
| 269 | + | ||||
| 270 | + | func TestHistoryRepository_GetAllRecentHistory(t *testing.T) { | |||
| 271 | + | ctx := historyTestCtx() | |||
| 272 | + | dbPath := filepath.Join(t.TempDir(), "dumber.db") | |||
| 273 | + | ||||
| 274 | + | db, err := sqlite.NewConnection(ctx, dbPath) | |||
| 275 | + | require.NoError(t, err) | |||
| 276 | + | t.Cleanup(func() { _ = db.Close() }) | |||
| 277 | + | ||||
| 278 | + | repo := sqlite.NewHistoryRepository(db) | |||
| 279 | + | ||||
| 280 | + | // Save some entries | |||
| 281 | + | entries := []*entity.HistoryEntry{ | |||
| 282 | + | {URL: "https://example.com", Title: "Example"}, | |||
| 283 | + | {URL: "https://github.com", Title: "GitHub"}, | |||
| 284 | + | } | |||
| 285 | + | for _, e := range entries { | |||
| 286 | + | require.NoError(t, repo.Save(ctx, e)) | |||
| 287 | + | } | |||
| 288 | + | ||||
| 289 | + | // Get all recent history | |||
| 290 | + | results, err := repo.GetAllRecentHistory(ctx) | |||
| 291 | + | require.NoError(t, err) | |||
| 292 | + | require.Len(t, results, 2) | |||
| 293 | + | } | |||
| 294 | + | ||||
| 295 | + | func TestHistoryRepository_GetAllMostVisited(t *testing.T) { | |||
| 296 | + | ctx := historyTestCtx() | |||
| 297 | + | dbPath := filepath.Join(t.TempDir(), "dumber.db") | |||
| 298 | + | ||||
| 299 | + | db, err := sqlite.NewConnection(ctx, dbPath) | |||
| 300 | + | require.NoError(t, err) | |||
| 301 | + | t.Cleanup(func() { _ = db.Close() }) | |||
| 302 | + | ||||
| 303 | + | repo := sqlite.NewHistoryRepository(db) | |||
| 304 | + | ||||
| 305 | + | // Save entries with different visit counts | |||
| 306 | + | entry1 := &entity.HistoryEntry{URL: "https://example.com", Title: "Example"} | |||
| 307 | + | entry2 := &entity.HistoryEntry{URL: "https://github.com", Title: "GitHub"} | |||
| 308 | + | ||||
| 309 | + | require.NoError(t, repo.Save(ctx, entry1)) | |||
| 310 | + | require.NoError(t, repo.Save(ctx, entry2)) | |||
| 311 | + | ||||
| 312 | + | // Increment github visits | |||
| 313 | + | for i := 0; i < 3; i++ { | |||
| 314 | + | require.NoError(t, repo.IncrementVisitCount(ctx, "https://github.com")) | |||
| 315 | + | } | |||
| 316 | + | ||||
| 317 | + | // Get all most visited | |||
| 318 | + | results, err := repo.GetAllMostVisited(ctx) | |||
| 319 | + | require.NoError(t, err) | |||
| 320 | + | require.Len(t, results, 2) | |||
| 321 | + | ||||
| 322 | + | // First should be github (4 visits) | |||
| 323 | + | assert.Equal(t, "https://github.com", results[0].URL) | |||
| 324 | + | assert.Equal(t, int64(4), results[0].VisitCount) | |||
| 325 | + | } | |||
M internal/infrastructure/persistence/sqlite/lazy_repos.go +28 -0
| @@ -79,6 +79,34 @@ func (r *LazyHistoryRepository) GetRecent(ctx context.Context, limit, offset int | |||
| 79 | 79 | return r.repo.GetRecent(ctx, limit, offset) | |
| 80 | 80 | } | |
| 81 | 81 | ||
| 82 | + | func (r *LazyHistoryRepository) GetRecentSince(ctx context.Context, days int) ([]*entity.HistoryEntry, error) { | |
| 83 | + | if err := r.init(ctx); err != nil { | |
| 84 | + | return nil, err | |
| 85 | + | } | |
| 86 | + | return r.repo.GetRecentSince(ctx, days) | |
| 87 | + | } | |
| 88 | + | ||
| 89 | + | func (r *LazyHistoryRepository) GetMostVisited(ctx context.Context, days int) ([]*entity.HistoryEntry, error) { | |
| 90 | + | if err := r.init(ctx); err != nil { | |
| 91 | + | return nil, err | |
| 92 | + | } | |
| 93 | + | return r.repo.GetMostVisited(ctx, days) | |
| 94 | + | } | |
| 95 | + | ||
| 96 | + | func (r *LazyHistoryRepository) GetAllRecentHistory(ctx context.Context) ([]*entity.HistoryEntry, error) { | |
| 97 | + | if err := r.init(ctx); err != nil { | |
| 98 | + | return nil, err | |
| 99 | + | } | |
| 100 | + | return r.repo.GetAllRecentHistory(ctx) | |
| 101 | + | } | |
| 102 | + | ||
| 103 | + | func (r *LazyHistoryRepository) GetAllMostVisited(ctx context.Context) ([]*entity.HistoryEntry, error) { | |
| 104 | + | if err := r.init(ctx); err != nil { | |
| 105 | + | return nil, err | |
| 106 | + | } | |
| 107 | + | return r.repo.GetAllMostVisited(ctx) | |
| 108 | + | } | |
| 109 | + | ||
| 82 | 110 | func (r *LazyHistoryRepository) IncrementVisitCount(ctx context.Context, url string) error { | |
| 83 | 111 | if err := r.init(ctx); err != nil { | |
| 84 | 112 | return err | |
| @@ -79,6 +79,34 @@ func (r *LazyHistoryRepository) GetRecent(ctx context.Context, limit, offset int | |||||
| 79 | return r.repo.GetRecent(ctx, limit, offset) | 79 | return r.repo.GetRecent(ctx, limit, offset) | ||
| 80 | } | 80 | } | ||
| 81 | 81 | ||||
| 82 | + | func (r *LazyHistoryRepository) GetRecentSince(ctx context.Context, days int) ([]*entity.HistoryEntry, error) { | |||
| 83 | + | if err := r.init(ctx); err != nil { | |||
| 84 | + | return nil, err | |||
| 85 | + | } | |||
| 86 | + | return r.repo.GetRecentSince(ctx, days) | |||
| 87 | + | } | |||
| 88 | + | ||||
| 89 | + | func (r *LazyHistoryRepository) GetMostVisited(ctx context.Context, days int) ([]*entity.HistoryEntry, error) { | |||
| 90 | + | if err := r.init(ctx); err != nil { | |||
| 91 | + | return nil, err | |||
| 92 | + | } | |||
| 93 | + | return r.repo.GetMostVisited(ctx, days) | |||
| 94 | + | } | |||
| 95 | + | ||||
| 96 | + | func (r *LazyHistoryRepository) GetAllRecentHistory(ctx context.Context) ([]*entity.HistoryEntry, error) { | |||
| 97 | + | if err := r.init(ctx); err != nil { | |||
| 98 | + | return nil, err | |||
| 99 | + | } | |||
| 100 | + | return r.repo.GetAllRecentHistory(ctx) | |||
| 101 | + | } | |||
| 102 | + | ||||
| 103 | + | func (r *LazyHistoryRepository) GetAllMostVisited(ctx context.Context) ([]*entity.HistoryEntry, error) { | |||
| 104 | + | if err := r.init(ctx); err != nil { | |||
| 105 | + | return nil, err | |||
| 106 | + | } | |||
| 107 | + | return r.repo.GetAllMostVisited(ctx) | |||
| 108 | + | } | |||
| 109 | + | ||||
| 82 | func (r *LazyHistoryRepository) IncrementVisitCount(ctx context.Context, url string) error { | 110 | func (r *LazyHistoryRepository) IncrementVisitCount(ctx context.Context, url string) error { | ||
| 83 | if err := r.init(ctx); err != nil { | 111 | if err := r.init(ctx); err != nil { | ||
| 84 | return err | 112 | return err | ||
M internal/infrastructure/persistence/sqlite/queries/history.sql +18 -0
| @@ -56,3 +56,21 @@ JOIN history h ON fts.rowid = h.id | |||
| 56 | 56 | WHERE fts.url MATCH ? | |
| 57 | 57 | ORDER BY bm25(history_fts) | |
| 58 | 58 | LIMIT ?; | |
| 59 | + | ||
| 60 | + | -- name: GetRecentHistorySince :many | |
| 61 | + | SELECT * FROM history | |
| 62 | + | WHERE last_visited >= datetime('now', ?) | |
| 63 | + | ORDER BY last_visited DESC; | |
| 64 | + | ||
| 65 | + | -- name: GetMostVisited :many | |
| 66 | + | SELECT * FROM history | |
| 67 | + | WHERE last_visited >= datetime('now', ?) | |
| 68 | + | ORDER BY visit_count DESC, last_visited DESC; | |
| 69 | + | ||
| 70 | + | -- name: GetAllRecentHistory :many | |
| 71 | + | SELECT * FROM history | |
| 72 | + | ORDER BY last_visited DESC; | |
| 73 | + | ||
| 74 | + | -- name: GetAllMostVisited :many | |
| 75 | + | SELECT * FROM history | |
| 76 | + | ORDER BY visit_count DESC, last_visited DESC; | |
| @@ -56,3 +56,21 @@ JOIN history h ON fts.rowid = h.id | |||||
| 56 | WHERE fts.url MATCH ? | 56 | WHERE fts.url MATCH ? | ||
| 57 | ORDER BY bm25(history_fts) | 57 | ORDER BY bm25(history_fts) | ||
| 58 | LIMIT ?; | 58 | LIMIT ?; | ||
| 59 | + | ||||
| 60 | + | -- name: GetRecentHistorySince :many | |||
| 61 | + | SELECT * FROM history | |||
| 62 | + | WHERE last_visited >= datetime('now', ?) | |||
| 63 | + | ORDER BY last_visited DESC; | |||
| 64 | + | ||||
| 65 | + | -- name: GetMostVisited :many | |||
| 66 | + | SELECT * FROM history | |||
| 67 | + | WHERE last_visited >= datetime('now', ?) | |||
| 68 | + | ORDER BY visit_count DESC, last_visited DESC; | |||
| 69 | + | ||||
| 70 | + | -- name: GetAllRecentHistory :many | |||
| 71 | + | SELECT * FROM history | |||
| 72 | + | ORDER BY last_visited DESC; | |||
| 73 | + | ||||
| 74 | + | -- name: GetAllMostVisited :many | |||
| 75 | + | SELECT * FROM history | |||
| 76 | + | ORDER BY visit_count DESC, last_visited DESC; | |||
M internal/infrastructure/persistence/sqlite/sqlc/history.sql.go +146 -0
| @@ -58,6 +58,78 @@ func (q *Queries) DeleteHistoryOlderThan(ctx context.Context, lastVisited sql.Nu | |||
| 58 | 58 | return err | |
| 59 | 59 | } | |
| 60 | 60 | ||
| 61 | + | const GetAllMostVisited = `-- name: GetAllMostVisited :many | |
| 62 | + | SELECT id, url, title, favicon_url, visit_count, last_visited, created_at FROM history | |
| 63 | + | ORDER BY visit_count DESC, last_visited DESC | |
| 64 | + | ` | |
| 65 | + | ||
| 66 | + | func (q *Queries) GetAllMostVisited(ctx context.Context) ([]History, error) { | |
| 67 | + | rows, err := q.db.QueryContext(ctx, GetAllMostVisited) | |
| 68 | + | if err != nil { | |
| 69 | + | return nil, err | |
| 70 | + | } | |
| 71 | + | defer rows.Close() | |
| 72 | + | items := []History{} | |
| 73 | + | for rows.Next() { | |
| 74 | + | var i History | |
| 75 | + | if err := rows.Scan( | |
| 76 | + | &i.ID, | |
| 77 | + | &i.Url, | |
| 78 | + | &i.Title, | |
| 79 | + | &i.FaviconUrl, | |
| 80 | + | &i.VisitCount, | |
| 81 | + | &i.LastVisited, | |
| 82 | + | &i.CreatedAt, | |
| 83 | + | ); err != nil { | |
| 84 | + | return nil, err | |
| 85 | + | } | |
| 86 | + | items = append(items, i) | |
| 87 | + | } | |
| 88 | + | if err := rows.Close(); err != nil { | |
| 89 | + | return nil, err | |
| 90 | + | } | |
| 91 | + | if err := rows.Err(); err != nil { | |
| 92 | + | return nil, err | |
| 93 | + | } | |
| 94 | + | return items, nil | |
| 95 | + | } | |
| 96 | + | ||
| 97 | + | const GetAllRecentHistory = `-- name: GetAllRecentHistory :many | |
| 98 | + | SELECT id, url, title, favicon_url, visit_count, last_visited, created_at FROM history | |
| 99 | + | ORDER BY last_visited DESC | |
| 100 | + | ` | |
| 101 | + | ||
| 102 | + | func (q *Queries) GetAllRecentHistory(ctx context.Context) ([]History, error) { | |
| 103 | + | rows, err := q.db.QueryContext(ctx, GetAllRecentHistory) | |
| 104 | + | if err != nil { | |
| 105 | + | return nil, err | |
| 106 | + | } | |
| 107 | + | defer rows.Close() | |
| 108 | + | items := []History{} | |
| 109 | + | for rows.Next() { | |
| 110 | + | var i History | |
| 111 | + | if err := rows.Scan( | |
| 112 | + | &i.ID, | |
| 113 | + | &i.Url, | |
| 114 | + | &i.Title, | |
| 115 | + | &i.FaviconUrl, | |
| 116 | + | &i.VisitCount, | |
| 117 | + | &i.LastVisited, | |
| 118 | + | &i.CreatedAt, | |
| 119 | + | ); err != nil { | |
| 120 | + | return nil, err | |
| 121 | + | } | |
| 122 | + | items = append(items, i) | |
| 123 | + | } | |
| 124 | + | if err := rows.Close(); err != nil { | |
| 125 | + | return nil, err | |
| 126 | + | } | |
| 127 | + | if err := rows.Err(); err != nil { | |
| 128 | + | return nil, err | |
| 129 | + | } | |
| 130 | + | return items, nil | |
| 131 | + | } | |
| 132 | + | ||
| 61 | 133 | const GetDailyVisitCount = `-- name: GetDailyVisitCount :many | |
| 62 | 134 | SELECT date(last_visited) as day, COUNT(*) as entries, SUM(visit_count) as visits FROM history WHERE last_visited >= date('now', ?) GROUP BY day ORDER BY day ASC | |
| 63 | 135 | ` | |
| @@ -198,6 +270,43 @@ func (q *Queries) GetHourlyDistribution(ctx context.Context) ([]GetHourlyDistrib | |||
| 198 | 270 | return items, nil | |
| 199 | 271 | } | |
| 200 | 272 | ||
| 273 | + | const GetMostVisited = `-- name: GetMostVisited :many | |
| 274 | + | SELECT id, url, title, favicon_url, visit_count, last_visited, created_at FROM history | |
| 275 | + | WHERE last_visited >= datetime('now', ?) | |
| 276 | + | ORDER BY visit_count DESC, last_visited DESC | |
| 277 | + | ` | |
| 278 | + | ||
| 279 | + | func (q *Queries) GetMostVisited(ctx context.Context, datetime interface{}) ([]History, error) { | |
| 280 | + | rows, err := q.db.QueryContext(ctx, GetMostVisited, datetime) | |
| 281 | + | if err != nil { | |
| 282 | + | return nil, err | |
| 283 | + | } | |
| 284 | + | defer rows.Close() | |
| 285 | + | items := []History{} | |
| 286 | + | for rows.Next() { | |
| 287 | + | var i History | |
| 288 | + | if err := rows.Scan( | |
| 289 | + | &i.ID, | |
| 290 | + | &i.Url, | |
| 291 | + | &i.Title, | |
| 292 | + | &i.FaviconUrl, | |
| 293 | + | &i.VisitCount, | |
| 294 | + | &i.LastVisited, | |
| 295 | + | &i.CreatedAt, | |
| 296 | + | ); err != nil { | |
| 297 | + | return nil, err | |
| 298 | + | } | |
| 299 | + | items = append(items, i) | |
| 300 | + | } | |
| 301 | + | if err := rows.Close(); err != nil { | |
| 302 | + | return nil, err | |
| 303 | + | } | |
| 304 | + | if err := rows.Err(); err != nil { | |
| 305 | + | return nil, err | |
| 306 | + | } | |
| 307 | + | return items, nil | |
| 308 | + | } | |
| 309 | + | ||
| 201 | 310 | const GetRecentHistory = `-- name: GetRecentHistory :many | |
| 202 | 311 | SELECT id, url, title, favicon_url, visit_count, last_visited, created_at FROM history ORDER BY last_visited DESC LIMIT ? OFFSET ? | |
| 203 | 312 | ` | |
| @@ -238,6 +347,43 @@ func (q *Queries) GetRecentHistory(ctx context.Context, arg GetRecentHistoryPara | |||
| 238 | 347 | return items, nil | |
| 239 | 348 | } | |
| 240 | 349 | ||
| 350 | + | const GetRecentHistorySince = `-- name: GetRecentHistorySince :many | |
| 351 | + | SELECT id, url, title, favicon_url, visit_count, last_visited, created_at FROM history | |
| 352 | + | WHERE last_visited >= datetime('now', ?) | |
| 353 | + | ORDER BY last_visited DESC | |
| 354 | + | ` | |
| 355 | + | ||
| 356 | + | func (q *Queries) GetRecentHistorySince(ctx context.Context, datetime interface{}) ([]History, error) { | |
| 357 | + | rows, err := q.db.QueryContext(ctx, GetRecentHistorySince, datetime) | |
| 358 | + | if err != nil { | |
| 359 | + | return nil, err | |
| 360 | + | } | |
| 361 | + | defer rows.Close() | |
| 362 | + | items := []History{} | |
| 363 | + | for rows.Next() { | |
| 364 | + | var i History | |
| 365 | + | if err := rows.Scan( | |
| 366 | + | &i.ID, | |
| 367 | + | &i.Url, | |
| 368 | + | &i.Title, | |
| 369 | + | &i.FaviconUrl, | |
| 370 | + | &i.VisitCount, | |
| 371 | + | &i.LastVisited, | |
| 372 | + | &i.CreatedAt, | |
| 373 | + | ); err != nil { | |
| 374 | + | return nil, err | |
| 375 | + | } | |
| 376 | + | items = append(items, i) | |
| 377 | + | } | |
| 378 | + | if err := rows.Close(); err != nil { | |
| 379 | + | return nil, err | |
| 380 | + | } | |
| 381 | + | if err := rows.Err(); err != nil { | |
| 382 | + | return nil, err | |
| 383 | + | } | |
| 384 | + | return items, nil | |
| 385 | + | } | |
| 386 | + | ||
| 241 | 387 | const IncrementVisitCount = `-- name: IncrementVisitCount :exec | |
| 242 | 388 | UPDATE history | |
| 243 | 389 | SET visit_count = visit_count + 1, last_visited = CURRENT_TIMESTAMP | |
| @@ -58,6 +58,78 @@ func (q *Queries) DeleteHistoryOlderThan(ctx context.Context, lastVisited sql.Nu | |||||
| 58 | return err | 58 | return err | ||
| 59 | } | 59 | } | ||
| 60 | 60 | ||||
| 61 | + | const GetAllMostVisited = `-- name: GetAllMostVisited :many | |||
| 62 | + | SELECT id, url, title, favicon_url, visit_count, last_visited, created_at FROM history | |||
| 63 | + | ORDER BY visit_count DESC, last_visited DESC | |||
| 64 | + | ` | |||
| 65 | + | ||||
| 66 | + | func (q *Queries) GetAllMostVisited(ctx context.Context) ([]History, error) { | |||
| 67 | + | rows, err := q.db.QueryContext(ctx, GetAllMostVisited) | |||
| 68 | + | if err != nil { | |||
| 69 | + | return nil, err | |||
| 70 | + | } | |||
| 71 | + | defer rows.Close() | |||
| 72 | + | items := []History{} | |||
| 73 | + | for rows.Next() { | |||
| 74 | + | var i History | |||
| 75 | + | if err := rows.Scan( | |||
| 76 | + | &i.ID, | |||
| 77 | + | &i.Url, | |||
| 78 | + | &i.Title, | |||
| 79 | + | &i.FaviconUrl, | |||
| 80 | + | &i.VisitCount, | |||
| 81 | + | &i.LastVisited, | |||
| 82 | + | &i.CreatedAt, | |||
| 83 | + | ); err != nil { | |||
| 84 | + | return nil, err | |||
| 85 | + | } | |||
| 86 | + | items = append(items, i) | |||
| 87 | + | } | |||
| 88 | + | if err := rows.Close(); err != nil { | |||
| 89 | + | return nil, err | |||
| 90 | + | } | |||
| 91 | + | if err := rows.Err(); err != nil { | |||
| 92 | + | return nil, err | |||
| 93 | + | } | |||
| 94 | + | return items, nil | |||
| 95 | + | } | |||
| 96 | + | ||||
| 97 | + | const GetAllRecentHistory = `-- name: GetAllRecentHistory :many | |||
| 98 | + | SELECT id, url, title, favicon_url, visit_count, last_visited, created_at FROM history | |||
| 99 | + | ORDER BY last_visited DESC | |||
| 100 | + | ` | |||
| 101 | + | ||||
| 102 | + | func (q *Queries) GetAllRecentHistory(ctx context.Context) ([]History, error) { | |||
| 103 | + | rows, err := q.db.QueryContext(ctx, GetAllRecentHistory) | |||
| 104 | + | if err != nil { | |||
| 105 | + | return nil, err | |||
| 106 | + | } | |||
| 107 | + | defer rows.Close() | |||
| 108 | + | items := []History{} | |||
| 109 | + | for rows.Next() { | |||
| 110 | + | var i History | |||
| 111 | + | if err := rows.Scan( | |||
| 112 | + | &i.ID, | |||
| 113 | + | &i.Url, | |||
| 114 | + | &i.Title, | |||
| 115 | + | &i.FaviconUrl, | |||
| 116 | + | &i.VisitCount, | |||
| 117 | + | &i.LastVisited, | |||
| 118 | + | &i.CreatedAt, | |||
| 119 | + | ); err != nil { | |||
| 120 | + | return nil, err | |||
| 121 | + | } | |||
| 122 | + | items = append(items, i) | |||
| 123 | + | } | |||
| 124 | + | if err := rows.Close(); err != nil { | |||
| 125 | + | return nil, err | |||
| 126 | + | } | |||
| 127 | + | if err := rows.Err(); err != nil { | |||
| 128 | + | return nil, err | |||
| 129 | + | } | |||
| 130 | + | return items, nil | |||
| 131 | + | } | |||
| 132 | + | ||||
| 61 | const GetDailyVisitCount = `-- name: GetDailyVisitCount :many | 133 | const GetDailyVisitCount = `-- name: GetDailyVisitCount :many | ||
| 62 | SELECT date(last_visited) as day, COUNT(*) as entries, SUM(visit_count) as visits FROM history WHERE last_visited >= date('now', ?) GROUP BY day ORDER BY day ASC | 134 | SELECT date(last_visited) as day, COUNT(*) as entries, SUM(visit_count) as visits FROM history WHERE last_visited >= date('now', ?) GROUP BY day ORDER BY day ASC | ||
| 63 | ` | 135 | ` | ||
| @@ -198,6 +270,43 @@ func (q *Queries) GetHourlyDistribution(ctx context.Context) ([]GetHourlyDistrib | |||||
| 198 | return items, nil | 270 | return items, nil | ||
| 199 | } | 271 | } | ||
| 200 | 272 | ||||
| 273 | + | const GetMostVisited = `-- name: GetMostVisited :many | |||
| 274 | + | SELECT id, url, title, favicon_url, visit_count, last_visited, created_at FROM history | |||
| 275 | + | WHERE last_visited >= datetime('now', ?) | |||
| 276 | + | ORDER BY visit_count DESC, last_visited DESC | |||
| 277 | + | ` | |||
| 278 | + | ||||
| 279 | + | func (q *Queries) GetMostVisited(ctx context.Context, datetime interface{}) ([]History, error) { | |||
| 280 | + | rows, err := q.db.QueryContext(ctx, GetMostVisited, datetime) | |||
| 281 | + | if err != nil { | |||
| 282 | + | return nil, err | |||
| 283 | + | } | |||
| 284 | + | defer rows.Close() | |||
| 285 | + | items := []History{} | |||
| 286 | + | for rows.Next() { | |||
| 287 | + | var i History | |||
| 288 | + | if err := rows.Scan( | |||
| 289 | + | &i.ID, | |||
| 290 | + | &i.Url, | |||
| 291 | + | &i.Title, | |||
| 292 | + | &i.FaviconUrl, | |||
| 293 | + | &i.VisitCount, | |||
| 294 | + | &i.LastVisited, | |||
| 295 | + | &i.CreatedAt, | |||
| 296 | + | ); err != nil { | |||
| 297 | + | return nil, err | |||
| 298 | + | } | |||
| 299 | + | items = append(items, i) | |||
| 300 | + | } | |||
| 301 | + | if err := rows.Close(); err != nil { | |||
| 302 | + | return nil, err | |||
| 303 | + | } | |||
| 304 | + | if err := rows.Err(); err != nil { | |||
| 305 | + | return nil, err | |||
| 306 | + | } | |||
| 307 | + | return items, nil | |||
| 308 | + | } | |||
| 309 | + | ||||
| 201 | const GetRecentHistory = `-- name: GetRecentHistory :many | 310 | const GetRecentHistory = `-- name: GetRecentHistory :many | ||
| 202 | SELECT id, url, title, favicon_url, visit_count, last_visited, created_at FROM history ORDER BY last_visited DESC LIMIT ? OFFSET ? | 311 | SELECT id, url, title, favicon_url, visit_count, last_visited, created_at FROM history ORDER BY last_visited DESC LIMIT ? OFFSET ? | ||
| 203 | ` | 312 | ` | ||
| @@ -238,6 +347,43 @@ func (q *Queries) GetRecentHistory(ctx context.Context, arg GetRecentHistoryPara | |||||
| 238 | return items, nil | 347 | return items, nil | ||
| 239 | } | 348 | } | ||
| 240 | 349 | ||||
| 350 | + | const GetRecentHistorySince = `-- name: GetRecentHistorySince :many | |||
| 351 | + | SELECT id, url, title, favicon_url, visit_count, last_visited, created_at FROM history | |||
| 352 | + | WHERE last_visited >= datetime('now', ?) | |||
| 353 | + | ORDER BY last_visited DESC | |||
| 354 | + | ` | |||
| 355 | + | ||||
| 356 | + | func (q *Queries) GetRecentHistorySince(ctx context.Context, datetime interface{}) ([]History, error) { | |||
| 357 | + | rows, err := q.db.QueryContext(ctx, GetRecentHistorySince, datetime) | |||
| 358 | + | if err != nil { | |||
| 359 | + | return nil, err | |||
| 360 | + | } | |||
| 361 | + | defer rows.Close() | |||
| 362 | + | items := []History{} | |||
| 363 | + | for rows.Next() { | |||
| 364 | + | var i History | |||
| 365 | + | if err := rows.Scan( | |||
| 366 | + | &i.ID, | |||
| 367 | + | &i.Url, | |||
| 368 | + | &i.Title, | |||
| 369 | + | &i.FaviconUrl, | |||
| 370 | + | &i.VisitCount, | |||
| 371 | + | &i.LastVisited, | |||
| 372 | + | &i.CreatedAt, | |||
| 373 | + | ); err != nil { | |||
| 374 | + | return nil, err | |||
| 375 | + | } | |||
| 376 | + | items = append(items, i) | |||
| 377 | + | } | |||
| 378 | + | if err := rows.Close(); err != nil { | |||
| 379 | + | return nil, err | |||
| 380 | + | } | |||
| 381 | + | if err := rows.Err(); err != nil { | |||
| 382 | + | return nil, err | |||
| 383 | + | } | |||
| 384 | + | return items, nil | |||
| 385 | + | } | |||
| 386 | + | ||||
| 241 | const IncrementVisitCount = `-- name: IncrementVisitCount :exec | 387 | const IncrementVisitCount = `-- name: IncrementVisitCount :exec | ||
| 242 | UPDATE history | 388 | UPDATE history | ||
| 243 | SET visit_count = visit_count + 1, last_visited = CURRENT_TIMESTAMP | 389 | SET visit_count = visit_count + 1, last_visited = CURRENT_TIMESTAMP | ||
M internal/infrastructure/persistence/sqlite/sqlc/querier.go +4 -0
| @@ -32,6 +32,8 @@ type Querier interface { | |||
| 32 | 32 | GetActiveBrowserSession(ctx context.Context) (Session, error) | |
| 33 | 33 | GetAllFavorites(ctx context.Context) ([]Favorite, error) | |
| 34 | 34 | GetAllFolders(ctx context.Context) ([]FavoriteFolder, error) | |
| 35 | + | GetAllMostVisited(ctx context.Context) ([]History, error) | |
| 36 | + | GetAllRecentHistory(ctx context.Context) ([]History, error) | |
| 35 | 37 | GetAllSessionStates(ctx context.Context) ([]SessionState, error) | |
| 36 | 38 | GetAllTags(ctx context.Context) ([]FavoriteTag, error) | |
| 37 | 39 | GetAllWhitelistedDomains(ctx context.Context) ([]string, error) | |
| @@ -47,7 +49,9 @@ type Querier interface { | |||
| 47 | 49 | GetHistoryByURL(ctx context.Context, url string) (History, error) | |
| 48 | 50 | GetHistoryStats(ctx context.Context) (GetHistoryStatsRow, error) | |
| 49 | 51 | GetHourlyDistribution(ctx context.Context) ([]GetHourlyDistributionRow, error) | |
| 52 | + | GetMostVisited(ctx context.Context, datetime interface{}) ([]History, error) | |
| 50 | 53 | GetRecentHistory(ctx context.Context, arg GetRecentHistoryParams) ([]History, error) | |
| 54 | + | GetRecentHistorySince(ctx context.Context, datetime interface{}) ([]History, error) | |
| 51 | 55 | GetRecentSessions(ctx context.Context, limit int64) ([]Session, error) | |
| 52 | 56 | GetRootFolders(ctx context.Context) ([]FavoriteFolder, error) | |
| 53 | 57 | GetSessionByID(ctx context.Context, id string) (Session, error) | |
| @@ -32,6 +32,8 @@ type Querier interface { | |||||
| 32 | GetActiveBrowserSession(ctx context.Context) (Session, error) | 32 | GetActiveBrowserSession(ctx context.Context) (Session, error) | ||
| 33 | GetAllFavorites(ctx context.Context) ([]Favorite, error) | 33 | GetAllFavorites(ctx context.Context) ([]Favorite, error) | ||
| 34 | GetAllFolders(ctx context.Context) ([]FavoriteFolder, error) | 34 | GetAllFolders(ctx context.Context) ([]FavoriteFolder, error) | ||
| 35 | + | GetAllMostVisited(ctx context.Context) ([]History, error) | |||
| 36 | + | GetAllRecentHistory(ctx context.Context) ([]History, error) | |||
| 35 | GetAllSessionStates(ctx context.Context) ([]SessionState, error) | 37 | GetAllSessionStates(ctx context.Context) ([]SessionState, error) | ||
| 36 | GetAllTags(ctx context.Context) ([]FavoriteTag, error) | 38 | GetAllTags(ctx context.Context) ([]FavoriteTag, error) | ||
| 37 | GetAllWhitelistedDomains(ctx context.Context) ([]string, error) | 39 | GetAllWhitelistedDomains(ctx context.Context) ([]string, error) | ||
| @@ -47,7 +49,9 @@ type Querier interface { | |||||
| 47 | GetHistoryByURL(ctx context.Context, url string) (History, error) | 49 | GetHistoryByURL(ctx context.Context, url string) (History, error) | ||
| 48 | GetHistoryStats(ctx context.Context) (GetHistoryStatsRow, error) | 50 | GetHistoryStats(ctx context.Context) (GetHistoryStatsRow, error) | ||
| 49 | GetHourlyDistribution(ctx context.Context) ([]GetHourlyDistributionRow, error) | 51 | GetHourlyDistribution(ctx context.Context) ([]GetHourlyDistributionRow, error) | ||
| 52 | + | GetMostVisited(ctx context.Context, datetime interface{}) ([]History, error) | |||
| 50 | GetRecentHistory(ctx context.Context, arg GetRecentHistoryParams) ([]History, error) | 53 | GetRecentHistory(ctx context.Context, arg GetRecentHistoryParams) ([]History, error) | ||
| 54 | + | GetRecentHistorySince(ctx context.Context, datetime interface{}) ([]History, error) | |||
| 51 | GetRecentSessions(ctx context.Context, limit int64) ([]Session, error) | 55 | GetRecentSessions(ctx context.Context, limit int64) ([]Session, error) | ||
| 52 | GetRootFolders(ctx context.Context) ([]FavoriteFolder, error) | 56 | GetRootFolders(ctx context.Context) ([]FavoriteFolder, error) | ||
| 53 | GetSessionByID(ctx context.Context, id string) (Session, error) | 57 | GetSessionByID(ctx context.Context, id string) (Session, error) | ||