feat(infrastructure): add SQLite persistence layer with migrations and generated queries
- commit
- d088d278d9ba6a9033ae57f9c13fcbff640aee4a
- author
- bnema · · 2025-12-09 21:32 UTC
- parent
- 8523928
- signature
- unsigned
25 files · +2155 -0
A internal/infrastructure/persistence/sqlite/connection.go +84 -0
| @@ -0,0 +1,84 @@ | |||
| 1 | + | package sqlite | |
| 2 | + | ||
| 3 | + | import ( | |
| 4 | + | "context" | |
| 5 | + | "database/sql" | |
| 6 | + | "fmt" | |
| 7 | + | "os" | |
| 8 | + | "path/filepath" | |
| 9 | + | ||
| 10 | + | "github.com/bnema/dumber/internal/logging" | |
| 11 | + | _ "github.com/ncruces/go-sqlite3/driver" // SQLite driver (pure Go) | |
| 12 | + | _ "github.com/ncruces/go-sqlite3/embed" // Embed SQLite WASM binary | |
| 13 | + | ) | |
| 14 | + | ||
| 15 | + | // NewConnection creates a new SQLite database connection with optimized settings. | |
| 16 | + | // It creates the database directory if it doesn't exist and applies performance pragmas. | |
| 17 | + | func NewConnection(ctx context.Context, dbPath string) (*sql.DB, error) { | |
| 18 | + | log := logging.FromContext(ctx) | |
| 19 | + | ||
| 20 | + | if dbPath == "" { | |
| 21 | + | return nil, fmt.Errorf("database path cannot be empty") | |
| 22 | + | } | |
| 23 | + | ||
| 24 | + | // Ensure database directory exists | |
| 25 | + | if err := os.MkdirAll(filepath.Dir(dbPath), 0750); err != nil { | |
| 26 | + | return nil, fmt.Errorf("failed to create database directory: %w", err) | |
| 27 | + | } | |
| 28 | + | ||
| 29 | + | // Open database connection | |
| 30 | + | db, err := sql.Open("sqlite3", dbPath) | |
| 31 | + | if err != nil { | |
| 32 | + | return nil, fmt.Errorf("failed to open database: %w", err) | |
| 33 | + | } | |
| 34 | + | ||
| 35 | + | // Test the connection | |
| 36 | + | if err := db.Ping(); err != nil { | |
| 37 | + | _ = db.Close() | |
| 38 | + | return nil, fmt.Errorf("failed to connect to database: %w", err) | |
| 39 | + | } | |
| 40 | + | ||
| 41 | + | // Apply performance pragmas | |
| 42 | + | if err := applyPragmas(db); err != nil { | |
| 43 | + | _ = db.Close() | |
| 44 | + | return nil, err | |
| 45 | + | } | |
| 46 | + | ||
| 47 | + | // Run migrations | |
| 48 | + | if err := RunMigrations(ctx, db); err != nil { | |
| 49 | + | _ = db.Close() | |
| 50 | + | return nil, fmt.Errorf("failed to run migrations: %w", err) | |
| 51 | + | } | |
| 52 | + | ||
| 53 | + | log.Info().Str("path", dbPath).Msg("database connection established") | |
| 54 | + | ||
| 55 | + | return db, nil | |
| 56 | + | } | |
| 57 | + | ||
| 58 | + | // applyPragmas configures SQLite for optimal performance. | |
| 59 | + | func applyPragmas(db *sql.DB) error { | |
| 60 | + | pragmas := []string{ | |
| 61 | + | "PRAGMA journal_mode = WAL", // Write-Ahead Logging for concurrent access | |
| 62 | + | "PRAGMA synchronous = NORMAL", // Safe in WAL mode | |
| 63 | + | "PRAGMA cache_size = -64000", // 64MB cache | |
| 64 | + | "PRAGMA temp_store = MEMORY", // Temporary tables in RAM | |
| 65 | + | "PRAGMA mmap_size = 30000000000", // 30GB memory-mapped I/O | |
| 66 | + | "PRAGMA busy_timeout = 5000", // Wait 5 seconds on lock contention | |
| 67 | + | } | |
| 68 | + | ||
| 69 | + | for _, pragma := range pragmas { | |
| 70 | + | if _, err := db.Exec(pragma); err != nil { | |
| 71 | + | return fmt.Errorf("failed to set pragma %q: %w", pragma, err) | |
| 72 | + | } | |
| 73 | + | } | |
| 74 | + | ||
| 75 | + | return nil | |
| 76 | + | } | |
| 77 | + | ||
| 78 | + | // Close closes the database connection gracefully. | |
| 79 | + | func Close(db *sql.DB) error { | |
| 80 | + | if db == nil { | |
| 81 | + | return nil | |
| 82 | + | } | |
| 83 | + | return db.Close() | |
| 84 | + | } | |
| @@ -0,0 +1,84 @@ | |||||
| 1 | + | package sqlite | |||
| 2 | + | ||||
| 3 | + | import ( | |||
| 4 | + | "context" | |||
| 5 | + | "database/sql" | |||
| 6 | + | "fmt" | |||
| 7 | + | "os" | |||
| 8 | + | "path/filepath" | |||
| 9 | + | ||||
| 10 | + | "github.com/bnema/dumber/internal/logging" | |||
| 11 | + | _ "github.com/ncruces/go-sqlite3/driver" // SQLite driver (pure Go) | |||
| 12 | + | _ "github.com/ncruces/go-sqlite3/embed" // Embed SQLite WASM binary | |||
| 13 | + | ) | |||
| 14 | + | ||||
| 15 | + | // NewConnection creates a new SQLite database connection with optimized settings. | |||
| 16 | + | // It creates the database directory if it doesn't exist and applies performance pragmas. | |||
| 17 | + | func NewConnection(ctx context.Context, dbPath string) (*sql.DB, error) { | |||
| 18 | + | log := logging.FromContext(ctx) | |||
| 19 | + | ||||
| 20 | + | if dbPath == "" { | |||
| 21 | + | return nil, fmt.Errorf("database path cannot be empty") | |||
| 22 | + | } | |||
| 23 | + | ||||
| 24 | + | // Ensure database directory exists | |||
| 25 | + | if err := os.MkdirAll(filepath.Dir(dbPath), 0750); err != nil { | |||
| 26 | + | return nil, fmt.Errorf("failed to create database directory: %w", err) | |||
| 27 | + | } | |||
| 28 | + | ||||
| 29 | + | // Open database connection | |||
| 30 | + | db, err := sql.Open("sqlite3", dbPath) | |||
| 31 | + | if err != nil { | |||
| 32 | + | return nil, fmt.Errorf("failed to open database: %w", err) | |||
| 33 | + | } | |||
| 34 | + | ||||
| 35 | + | // Test the connection | |||
| 36 | + | if err := db.Ping(); err != nil { | |||
| 37 | + | _ = db.Close() | |||
| 38 | + | return nil, fmt.Errorf("failed to connect to database: %w", err) | |||
| 39 | + | } | |||
| 40 | + | ||||
| 41 | + | // Apply performance pragmas | |||
| 42 | + | if err := applyPragmas(db); err != nil { | |||
| 43 | + | _ = db.Close() | |||
| 44 | + | return nil, err | |||
| 45 | + | } | |||
| 46 | + | ||||
| 47 | + | // Run migrations | |||
| 48 | + | if err := RunMigrations(ctx, db); err != nil { | |||
| 49 | + | _ = db.Close() | |||
| 50 | + | return nil, fmt.Errorf("failed to run migrations: %w", err) | |||
| 51 | + | } | |||
| 52 | + | ||||
| 53 | + | log.Info().Str("path", dbPath).Msg("database connection established") | |||
| 54 | + | ||||
| 55 | + | return db, nil | |||
| 56 | + | } | |||
| 57 | + | ||||
| 58 | + | // applyPragmas configures SQLite for optimal performance. | |||
| 59 | + | func applyPragmas(db *sql.DB) error { | |||
| 60 | + | pragmas := []string{ | |||
| 61 | + | "PRAGMA journal_mode = WAL", // Write-Ahead Logging for concurrent access | |||
| 62 | + | "PRAGMA synchronous = NORMAL", // Safe in WAL mode | |||
| 63 | + | "PRAGMA cache_size = -64000", // 64MB cache | |||
| 64 | + | "PRAGMA temp_store = MEMORY", // Temporary tables in RAM | |||
| 65 | + | "PRAGMA mmap_size = 30000000000", // 30GB memory-mapped I/O | |||
| 66 | + | "PRAGMA busy_timeout = 5000", // Wait 5 seconds on lock contention | |||
| 67 | + | } | |||
| 68 | + | ||||
| 69 | + | for _, pragma := range pragmas { | |||
| 70 | + | if _, err := db.Exec(pragma); err != nil { | |||
| 71 | + | return fmt.Errorf("failed to set pragma %q: %w", pragma, err) | |||
| 72 | + | } | |||
| 73 | + | } | |||
| 74 | + | ||||
| 75 | + | return nil | |||
| 76 | + | } | |||
| 77 | + | ||||
| 78 | + | // Close closes the database connection gracefully. | |||
| 79 | + | func Close(db *sql.DB) error { | |||
| 80 | + | if db == nil { | |||
| 81 | + | return nil | |||
| 82 | + | } | |||
| 83 | + | return db.Close() | |||
| 84 | + | } | |||
A internal/infrastructure/persistence/sqlite/favorite_repo.go +361 -0
| @@ -0,0 +1,361 @@ | |||
| 1 | + | package sqlite | |
| 2 | + | ||
| 3 | + | import ( | |
| 4 | + | "context" | |
| 5 | + | "database/sql" | |
| 6 | + | ||
| 7 | + | "github.com/bnema/dumber/internal/domain/entity" | |
| 8 | + | "github.com/bnema/dumber/internal/domain/repository" | |
| 9 | + | "github.com/bnema/dumber/internal/infrastructure/persistence/sqlite/sqlc" | |
| 10 | + | "github.com/bnema/dumber/internal/logging" | |
| 11 | + | ) | |
| 12 | + | ||
| 13 | + | // ==================== Favorite Repository ==================== | |
| 14 | + | ||
| 15 | + | type favoriteRepo struct { | |
| 16 | + | queries *sqlc.Queries | |
| 17 | + | } | |
| 18 | + | ||
| 19 | + | // NewFavoriteRepository creates a new SQLite-backed favorite repository. | |
| 20 | + | func NewFavoriteRepository(db *sql.DB) repository.FavoriteRepository { | |
| 21 | + | return &favoriteRepo{queries: sqlc.New(db)} | |
| 22 | + | } | |
| 23 | + | ||
| 24 | + | func (r *favoriteRepo) Save(ctx context.Context, fav *entity.Favorite) error { | |
| 25 | + | log := logging.FromContext(ctx) | |
| 26 | + | log.Debug().Str("url", fav.URL).Msg("saving favorite") | |
| 27 | + | ||
| 28 | + | var folderID sql.NullInt64 | |
| 29 | + | if fav.FolderID != nil { | |
| 30 | + | folderID = sql.NullInt64{Int64: int64(*fav.FolderID), Valid: true} | |
| 31 | + | } | |
| 32 | + | ||
| 33 | + | row, err := r.queries.CreateFavorite(ctx, sqlc.CreateFavoriteParams{ | |
| 34 | + | Url: fav.URL, | |
| 35 | + | Title: sql.NullString{String: fav.Title, Valid: fav.Title != ""}, | |
| 36 | + | FaviconUrl: sql.NullString{String: fav.FaviconURL, Valid: fav.FaviconURL != ""}, | |
| 37 | + | FolderID: folderID, | |
| 38 | + | }) | |
| 39 | + | if err != nil { | |
| 40 | + | return err | |
| 41 | + | } | |
| 42 | + | fav.ID = entity.FavoriteID(row.ID) | |
| 43 | + | return nil | |
| 44 | + | } | |
| 45 | + | ||
| 46 | + | func (r *favoriteRepo) FindByID(ctx context.Context, id entity.FavoriteID) (*entity.Favorite, error) { | |
| 47 | + | row, err := r.queries.GetFavoriteByID(ctx, int64(id)) | |
| 48 | + | if err != nil { | |
| 49 | + | if err == sql.ErrNoRows { | |
| 50 | + | return nil, nil | |
| 51 | + | } | |
| 52 | + | return nil, err | |
| 53 | + | } | |
| 54 | + | return favoriteFromRow(row), nil | |
| 55 | + | } | |
| 56 | + | ||
| 57 | + | func (r *favoriteRepo) FindByURL(ctx context.Context, url string) (*entity.Favorite, error) { | |
| 58 | + | row, err := r.queries.GetFavoriteByURL(ctx, url) | |
| 59 | + | if err != nil { | |
| 60 | + | if err == sql.ErrNoRows { | |
| 61 | + | return nil, nil | |
| 62 | + | } | |
| 63 | + | return nil, err | |
| 64 | + | } | |
| 65 | + | return favoriteFromRow(row), nil | |
| 66 | + | } | |
| 67 | + | ||
| 68 | + | func (r *favoriteRepo) GetAll(ctx context.Context) ([]*entity.Favorite, error) { | |
| 69 | + | rows, err := r.queries.GetAllFavorites(ctx) | |
| 70 | + | if err != nil { | |
| 71 | + | return nil, err | |
| 72 | + | } | |
| 73 | + | return favoritesFromRows(rows), nil | |
| 74 | + | } | |
| 75 | + | ||
| 76 | + | func (r *favoriteRepo) GetByFolder(ctx context.Context, folderID *entity.FolderID) ([]*entity.Favorite, error) { | |
| 77 | + | var rows []sqlc.Favorite | |
| 78 | + | var err error | |
| 79 | + | ||
| 80 | + | if folderID == nil { | |
| 81 | + | rows, err = r.queries.GetFavoritesWithoutFolder(ctx) | |
| 82 | + | } else { | |
| 83 | + | rows, err = r.queries.GetFavoritesByFolder(ctx, sql.NullInt64{Int64: int64(*folderID), Valid: true}) | |
| 84 | + | } | |
| 85 | + | if err != nil { | |
| 86 | + | return nil, err | |
| 87 | + | } | |
| 88 | + | return favoritesFromRows(rows), nil | |
| 89 | + | } | |
| 90 | + | ||
| 91 | + | func (r *favoriteRepo) GetByShortcut(ctx context.Context, key int) (*entity.Favorite, error) { | |
| 92 | + | row, err := r.queries.GetFavoriteByShortcut(ctx, sql.NullInt64{Int64: int64(key), Valid: true}) | |
| 93 | + | if err != nil { | |
| 94 | + | if err == sql.ErrNoRows { | |
| 95 | + | return nil, nil | |
| 96 | + | } | |
| 97 | + | return nil, err | |
| 98 | + | } | |
| 99 | + | return favoriteFromRow(row), nil | |
| 100 | + | } | |
| 101 | + | ||
| 102 | + | func (r *favoriteRepo) UpdatePosition(ctx context.Context, id entity.FavoriteID, position int) error { | |
| 103 | + | return r.queries.UpdateFavoritePosition(ctx, sqlc.UpdateFavoritePositionParams{ | |
| 104 | + | Position: int64(position), | |
| 105 | + | ID: int64(id), | |
| 106 | + | }) | |
| 107 | + | } | |
| 108 | + | ||
| 109 | + | func (r *favoriteRepo) SetFolder(ctx context.Context, id entity.FavoriteID, folderID *entity.FolderID) error { | |
| 110 | + | var fid sql.NullInt64 | |
| 111 | + | if folderID != nil { | |
| 112 | + | fid = sql.NullInt64{Int64: int64(*folderID), Valid: true} | |
| 113 | + | } | |
| 114 | + | return r.queries.SetFavoriteFolder(ctx, sqlc.SetFavoriteFolderParams{ | |
| 115 | + | FolderID: fid, | |
| 116 | + | ID: int64(id), | |
| 117 | + | }) | |
| 118 | + | } | |
| 119 | + | ||
| 120 | + | func (r *favoriteRepo) SetShortcut(ctx context.Context, id entity.FavoriteID, key *int) error { | |
| 121 | + | var k sql.NullInt64 | |
| 122 | + | if key != nil { | |
| 123 | + | k = sql.NullInt64{Int64: int64(*key), Valid: true} | |
| 124 | + | } | |
| 125 | + | return r.queries.SetFavoriteShortcut(ctx, sqlc.SetFavoriteShortcutParams{ | |
| 126 | + | ShortcutKey: k, | |
| 127 | + | ID: int64(id), | |
| 128 | + | }) | |
| 129 | + | } | |
| 130 | + | ||
| 131 | + | func (r *favoriteRepo) Delete(ctx context.Context, id entity.FavoriteID) error { | |
| 132 | + | return r.queries.DeleteFavorite(ctx, int64(id)) | |
| 133 | + | } | |
| 134 | + | ||
| 135 | + | func favoriteFromRow(row sqlc.Favorite) *entity.Favorite { | |
| 136 | + | fav := &entity.Favorite{ | |
| 137 | + | ID: entity.FavoriteID(row.ID), | |
| 138 | + | URL: row.Url, | |
| 139 | + | Title: row.Title.String, | |
| 140 | + | FaviconURL: row.FaviconUrl.String, | |
| 141 | + | Position: int(row.Position), | |
| 142 | + | CreatedAt: row.CreatedAt.Time, | |
| 143 | + | UpdatedAt: row.UpdatedAt.Time, | |
| 144 | + | } | |
| 145 | + | if row.FolderID.Valid { | |
| 146 | + | fid := entity.FolderID(row.FolderID.Int64) | |
| 147 | + | fav.FolderID = &fid | |
| 148 | + | } | |
| 149 | + | if row.ShortcutKey.Valid { | |
| 150 | + | key := int(row.ShortcutKey.Int64) | |
| 151 | + | fav.ShortcutKey = &key | |
| 152 | + | } | |
| 153 | + | return fav | |
| 154 | + | } | |
| 155 | + | ||
| 156 | + | func favoritesFromRows(rows []sqlc.Favorite) []*entity.Favorite { | |
| 157 | + | favorites := make([]*entity.Favorite, len(rows)) | |
| 158 | + | for i, row := range rows { | |
| 159 | + | favorites[i] = favoriteFromRow(row) | |
| 160 | + | } | |
| 161 | + | return favorites | |
| 162 | + | } | |
| 163 | + | ||
| 164 | + | // ==================== Folder Repository ==================== | |
| 165 | + | ||
| 166 | + | type folderRepo struct { | |
| 167 | + | queries *sqlc.Queries | |
| 168 | + | } | |
| 169 | + | ||
| 170 | + | // NewFolderRepository creates a new SQLite-backed folder repository. | |
| 171 | + | func NewFolderRepository(db *sql.DB) repository.FolderRepository { | |
| 172 | + | return &folderRepo{queries: sqlc.New(db)} | |
| 173 | + | } | |
| 174 | + | ||
| 175 | + | func (r *folderRepo) Save(ctx context.Context, folder *entity.Folder) error { | |
| 176 | + | log := logging.FromContext(ctx) | |
| 177 | + | log.Debug().Str("name", folder.Name).Msg("saving folder") | |
| 178 | + | ||
| 179 | + | var parentID sql.NullInt64 | |
| 180 | + | if folder.ParentID != nil { | |
| 181 | + | parentID = sql.NullInt64{Int64: int64(*folder.ParentID), Valid: true} | |
| 182 | + | } | |
| 183 | + | ||
| 184 | + | row, err := r.queries.CreateFolder(ctx, sqlc.CreateFolderParams{ | |
| 185 | + | Name: folder.Name, | |
| 186 | + | Icon: sql.NullString{String: folder.Icon, Valid: folder.Icon != ""}, | |
| 187 | + | ParentID: parentID, | |
| 188 | + | }) | |
| 189 | + | if err != nil { | |
| 190 | + | return err | |
| 191 | + | } | |
| 192 | + | folder.ID = entity.FolderID(row.ID) | |
| 193 | + | return nil | |
| 194 | + | } | |
| 195 | + | ||
| 196 | + | func (r *folderRepo) FindByID(ctx context.Context, id entity.FolderID) (*entity.Folder, error) { | |
| 197 | + | row, err := r.queries.GetFolderByID(ctx, int64(id)) | |
| 198 | + | if err != nil { | |
| 199 | + | if err == sql.ErrNoRows { | |
| 200 | + | return nil, nil | |
| 201 | + | } | |
| 202 | + | return nil, err | |
| 203 | + | } | |
| 204 | + | return folderFromRow(row), nil | |
| 205 | + | } | |
| 206 | + | ||
| 207 | + | func (r *folderRepo) GetAll(ctx context.Context) ([]*entity.Folder, error) { | |
| 208 | + | rows, err := r.queries.GetAllFolders(ctx) | |
| 209 | + | if err != nil { | |
| 210 | + | return nil, err | |
| 211 | + | } | |
| 212 | + | return foldersFromRows(rows), nil | |
| 213 | + | } | |
| 214 | + | ||
| 215 | + | func (r *folderRepo) GetChildren(ctx context.Context, parentID *entity.FolderID) ([]*entity.Folder, error) { | |
| 216 | + | var rows []sqlc.FavoriteFolder | |
| 217 | + | var err error | |
| 218 | + | ||
| 219 | + | if parentID == nil { | |
| 220 | + | rows, err = r.queries.GetRootFolders(ctx) | |
| 221 | + | } else { | |
| 222 | + | rows, err = r.queries.GetChildFolders(ctx, sql.NullInt64{Int64: int64(*parentID), Valid: true}) | |
| 223 | + | } | |
| 224 | + | if err != nil { | |
| 225 | + | return nil, err | |
| 226 | + | } | |
| 227 | + | return foldersFromRows(rows), nil | |
| 228 | + | } | |
| 229 | + | ||
| 230 | + | func (r *folderRepo) UpdatePosition(ctx context.Context, id entity.FolderID, position int) error { | |
| 231 | + | return r.queries.UpdateFolderPosition(ctx, sqlc.UpdateFolderPositionParams{ | |
| 232 | + | Position: int64(position), | |
| 233 | + | ID: int64(id), | |
| 234 | + | }) | |
| 235 | + | } | |
| 236 | + | ||
| 237 | + | func (r *folderRepo) Delete(ctx context.Context, id entity.FolderID) error { | |
| 238 | + | return r.queries.DeleteFolder(ctx, int64(id)) | |
| 239 | + | } | |
| 240 | + | ||
| 241 | + | func folderFromRow(row sqlc.FavoriteFolder) *entity.Folder { | |
| 242 | + | folder := &entity.Folder{ | |
| 243 | + | ID: entity.FolderID(row.ID), | |
| 244 | + | Name: row.Name, | |
| 245 | + | Icon: row.Icon.String, | |
| 246 | + | Position: int(row.Position), | |
| 247 | + | CreatedAt: row.CreatedAt.Time, | |
| 248 | + | } | |
| 249 | + | if row.ParentID.Valid { | |
| 250 | + | pid := entity.FolderID(row.ParentID.Int64) | |
| 251 | + | folder.ParentID = &pid | |
| 252 | + | } | |
| 253 | + | return folder | |
| 254 | + | } | |
| 255 | + | ||
| 256 | + | func foldersFromRows(rows []sqlc.FavoriteFolder) []*entity.Folder { | |
| 257 | + | folders := make([]*entity.Folder, len(rows)) | |
| 258 | + | for i, row := range rows { | |
| 259 | + | folders[i] = folderFromRow(row) | |
| 260 | + | } | |
| 261 | + | return folders | |
| 262 | + | } | |
| 263 | + | ||
| 264 | + | // ==================== Tag Repository ==================== | |
| 265 | + | ||
| 266 | + | type tagRepo struct { | |
| 267 | + | queries *sqlc.Queries | |
| 268 | + | } | |
| 269 | + | ||
| 270 | + | // NewTagRepository creates a new SQLite-backed tag repository. | |
| 271 | + | func NewTagRepository(db *sql.DB) repository.TagRepository { | |
| 272 | + | return &tagRepo{queries: sqlc.New(db)} | |
| 273 | + | } | |
| 274 | + | ||
| 275 | + | func (r *tagRepo) Save(ctx context.Context, tag *entity.Tag) error { | |
| 276 | + | log := logging.FromContext(ctx) | |
| 277 | + | log.Debug().Str("name", tag.Name).Msg("saving tag") | |
| 278 | + | ||
| 279 | + | row, err := r.queries.CreateTag(ctx, sqlc.CreateTagParams{ | |
| 280 | + | Name: tag.Name, | |
| 281 | + | Color: tag.Color, | |
| 282 | + | }) | |
| 283 | + | if err != nil { | |
| 284 | + | return err | |
| 285 | + | } | |
| 286 | + | tag.ID = entity.TagID(row.ID) | |
| 287 | + | return nil | |
| 288 | + | } | |
| 289 | + | ||
| 290 | + | func (r *tagRepo) FindByID(ctx context.Context, id entity.TagID) (*entity.Tag, error) { | |
| 291 | + | row, err := r.queries.GetTagByID(ctx, int64(id)) | |
| 292 | + | if err != nil { | |
| 293 | + | if err == sql.ErrNoRows { | |
| 294 | + | return nil, nil | |
| 295 | + | } | |
| 296 | + | return nil, err | |
| 297 | + | } | |
| 298 | + | return tagFromRow(row), nil | |
| 299 | + | } | |
| 300 | + | ||
| 301 | + | func (r *tagRepo) FindByName(ctx context.Context, name string) (*entity.Tag, error) { | |
| 302 | + | row, err := r.queries.GetTagByName(ctx, name) | |
| 303 | + | if err != nil { | |
| 304 | + | if err == sql.ErrNoRows { | |
| 305 | + | return nil, nil | |
| 306 | + | } | |
| 307 | + | return nil, err | |
| 308 | + | } | |
| 309 | + | return tagFromRow(row), nil | |
| 310 | + | } | |
| 311 | + | ||
| 312 | + | func (r *tagRepo) GetAll(ctx context.Context) ([]*entity.Tag, error) { | |
| 313 | + | rows, err := r.queries.GetAllTags(ctx) | |
| 314 | + | if err != nil { | |
| 315 | + | return nil, err | |
| 316 | + | } | |
| 317 | + | return tagsFromRows(rows), nil | |
| 318 | + | } | |
| 319 | + | ||
| 320 | + | func (r *tagRepo) AssignToFavorite(ctx context.Context, tagID entity.TagID, favID entity.FavoriteID) error { | |
| 321 | + | return r.queries.AssignTagToFavorite(ctx, sqlc.AssignTagToFavoriteParams{ | |
| 322 | + | FavoriteID: int64(favID), | |
| 323 | + | TagID: int64(tagID), | |
| 324 | + | }) | |
| 325 | + | } | |
| 326 | + | ||
| 327 | + | func (r *tagRepo) RemoveFromFavorite(ctx context.Context, tagID entity.TagID, favID entity.FavoriteID) error { | |
| 328 | + | return r.queries.RemoveTagFromFavorite(ctx, sqlc.RemoveTagFromFavoriteParams{ | |
| 329 | + | FavoriteID: int64(favID), | |
| 330 | + | TagID: int64(tagID), | |
| 331 | + | }) | |
| 332 | + | } | |
| 333 | + | ||
| 334 | + | func (r *tagRepo) GetForFavorite(ctx context.Context, favID entity.FavoriteID) ([]*entity.Tag, error) { | |
| 335 | + | rows, err := r.queries.GetTagsForFavorite(ctx, int64(favID)) | |
| 336 | + | if err != nil { | |
| 337 | + | return nil, err | |
| 338 | + | } | |
| 339 | + | return tagsFromRows(rows), nil | |
| 340 | + | } | |
| 341 | + | ||
| 342 | + | func (r *tagRepo) Delete(ctx context.Context, id entity.TagID) error { | |
| 343 | + | return r.queries.DeleteTag(ctx, int64(id)) | |
| 344 | + | } | |
| 345 | + | ||
| 346 | + | func tagFromRow(row sqlc.FavoriteTag) *entity.Tag { | |
| 347 | + | return &entity.Tag{ | |
| 348 | + | ID: entity.TagID(row.ID), | |
| 349 | + | Name: row.Name, | |
| 350 | + | Color: row.Color, | |
| 351 | + | CreatedAt: row.CreatedAt.Time, | |
| 352 | + | } | |
| 353 | + | } | |
| 354 | + | ||
| 355 | + | func tagsFromRows(rows []sqlc.FavoriteTag) []*entity.Tag { | |
| 356 | + | tags := make([]*entity.Tag, len(rows)) | |
| 357 | + | for i, row := range rows { | |
| 358 | + | tags[i] = tagFromRow(row) | |
| 359 | + | } | |
| 360 | + | return tags | |
| 361 | + | } | |
| @@ -0,0 +1,361 @@ | |||||
| 1 | + | package sqlite | |||
| 2 | + | ||||
| 3 | + | import ( | |||
| 4 | + | "context" | |||
| 5 | + | "database/sql" | |||
| 6 | + | ||||
| 7 | + | "github.com/bnema/dumber/internal/domain/entity" | |||
| 8 | + | "github.com/bnema/dumber/internal/domain/repository" | |||
| 9 | + | "github.com/bnema/dumber/internal/infrastructure/persistence/sqlite/sqlc" | |||
| 10 | + | "github.com/bnema/dumber/internal/logging" | |||
| 11 | + | ) | |||
| 12 | + | ||||
| 13 | + | // ==================== Favorite Repository ==================== | |||
| 14 | + | ||||
| 15 | + | type favoriteRepo struct { | |||
| 16 | + | queries *sqlc.Queries | |||
| 17 | + | } | |||
| 18 | + | ||||
| 19 | + | // NewFavoriteRepository creates a new SQLite-backed favorite repository. | |||
| 20 | + | func NewFavoriteRepository(db *sql.DB) repository.FavoriteRepository { | |||
| 21 | + | return &favoriteRepo{queries: sqlc.New(db)} | |||
| 22 | + | } | |||
| 23 | + | ||||
| 24 | + | func (r *favoriteRepo) Save(ctx context.Context, fav *entity.Favorite) error { | |||
| 25 | + | log := logging.FromContext(ctx) | |||
| 26 | + | log.Debug().Str("url", fav.URL).Msg("saving favorite") | |||
| 27 | + | ||||
| 28 | + | var folderID sql.NullInt64 | |||
| 29 | + | if fav.FolderID != nil { | |||
| 30 | + | folderID = sql.NullInt64{Int64: int64(*fav.FolderID), Valid: true} | |||
| 31 | + | } | |||
| 32 | + | ||||
| 33 | + | row, err := r.queries.CreateFavorite(ctx, sqlc.CreateFavoriteParams{ | |||
| 34 | + | Url: fav.URL, | |||
| 35 | + | Title: sql.NullString{String: fav.Title, Valid: fav.Title != ""}, | |||
| 36 | + | FaviconUrl: sql.NullString{String: fav.FaviconURL, Valid: fav.FaviconURL != ""}, | |||
| 37 | + | FolderID: folderID, | |||
| 38 | + | }) | |||
| 39 | + | if err != nil { | |||
| 40 | + | return err | |||
| 41 | + | } | |||
| 42 | + | fav.ID = entity.FavoriteID(row.ID) | |||
| 43 | + | return nil | |||
| 44 | + | } | |||
| 45 | + | ||||
| 46 | + | func (r *favoriteRepo) FindByID(ctx context.Context, id entity.FavoriteID) (*entity.Favorite, error) { | |||
| 47 | + | row, err := r.queries.GetFavoriteByID(ctx, int64(id)) | |||
| 48 | + | if err != nil { | |||
| 49 | + | if err == sql.ErrNoRows { | |||
| 50 | + | return nil, nil | |||
| 51 | + | } | |||
| 52 | + | return nil, err | |||
| 53 | + | } | |||
| 54 | + | return favoriteFromRow(row), nil | |||
| 55 | + | } | |||
| 56 | + | ||||
| 57 | + | func (r *favoriteRepo) FindByURL(ctx context.Context, url string) (*entity.Favorite, error) { | |||
| 58 | + | row, err := r.queries.GetFavoriteByURL(ctx, url) | |||
| 59 | + | if err != nil { | |||
| 60 | + | if err == sql.ErrNoRows { | |||
| 61 | + | return nil, nil | |||
| 62 | + | } | |||
| 63 | + | return nil, err | |||
| 64 | + | } | |||
| 65 | + | return favoriteFromRow(row), nil | |||
| 66 | + | } | |||
| 67 | + | ||||
| 68 | + | func (r *favoriteRepo) GetAll(ctx context.Context) ([]*entity.Favorite, error) { | |||
| 69 | + | rows, err := r.queries.GetAllFavorites(ctx) | |||
| 70 | + | if err != nil { | |||
| 71 | + | return nil, err | |||
| 72 | + | } | |||
| 73 | + | return favoritesFromRows(rows), nil | |||
| 74 | + | } | |||
| 75 | + | ||||
| 76 | + | func (r *favoriteRepo) GetByFolder(ctx context.Context, folderID *entity.FolderID) ([]*entity.Favorite, error) { | |||
| 77 | + | var rows []sqlc.Favorite | |||
| 78 | + | var err error | |||
| 79 | + | ||||
| 80 | + | if folderID == nil { | |||
| 81 | + | rows, err = r.queries.GetFavoritesWithoutFolder(ctx) | |||
| 82 | + | } else { | |||
| 83 | + | rows, err = r.queries.GetFavoritesByFolder(ctx, sql.NullInt64{Int64: int64(*folderID), Valid: true}) | |||
| 84 | + | } | |||
| 85 | + | if err != nil { | |||
| 86 | + | return nil, err | |||
| 87 | + | } | |||
| 88 | + | return favoritesFromRows(rows), nil | |||
| 89 | + | } | |||
| 90 | + | ||||
| 91 | + | func (r *favoriteRepo) GetByShortcut(ctx context.Context, key int) (*entity.Favorite, error) { | |||
| 92 | + | row, err := r.queries.GetFavoriteByShortcut(ctx, sql.NullInt64{Int64: int64(key), Valid: true}) | |||
| 93 | + | if err != nil { | |||
| 94 | + | if err == sql.ErrNoRows { | |||
| 95 | + | return nil, nil | |||
| 96 | + | } | |||
| 97 | + | return nil, err | |||
| 98 | + | } | |||
| 99 | + | return favoriteFromRow(row), nil | |||
| 100 | + | } | |||
| 101 | + | ||||
| 102 | + | func (r *favoriteRepo) UpdatePosition(ctx context.Context, id entity.FavoriteID, position int) error { | |||
| 103 | + | return r.queries.UpdateFavoritePosition(ctx, sqlc.UpdateFavoritePositionParams{ | |||
| 104 | + | Position: int64(position), | |||
| 105 | + | ID: int64(id), | |||
| 106 | + | }) | |||
| 107 | + | } | |||
| 108 | + | ||||
| 109 | + | func (r *favoriteRepo) SetFolder(ctx context.Context, id entity.FavoriteID, folderID *entity.FolderID) error { | |||
| 110 | + | var fid sql.NullInt64 | |||
| 111 | + | if folderID != nil { | |||
| 112 | + | fid = sql.NullInt64{Int64: int64(*folderID), Valid: true} | |||
| 113 | + | } | |||
| 114 | + | return r.queries.SetFavoriteFolder(ctx, sqlc.SetFavoriteFolderParams{ | |||
| 115 | + | FolderID: fid, | |||
| 116 | + | ID: int64(id), | |||
| 117 | + | }) | |||
| 118 | + | } | |||
| 119 | + | ||||
| 120 | + | func (r *favoriteRepo) SetShortcut(ctx context.Context, id entity.FavoriteID, key *int) error { | |||
| 121 | + | var k sql.NullInt64 | |||
| 122 | + | if key != nil { | |||
| 123 | + | k = sql.NullInt64{Int64: int64(*key), Valid: true} | |||
| 124 | + | } | |||
| 125 | + | return r.queries.SetFavoriteShortcut(ctx, sqlc.SetFavoriteShortcutParams{ | |||
| 126 | + | ShortcutKey: k, | |||
| 127 | + | ID: int64(id), | |||
| 128 | + | }) | |||
| 129 | + | } | |||
| 130 | + | ||||
| 131 | + | func (r *favoriteRepo) Delete(ctx context.Context, id entity.FavoriteID) error { | |||
| 132 | + | return r.queries.DeleteFavorite(ctx, int64(id)) | |||
| 133 | + | } | |||
| 134 | + | ||||
| 135 | + | func favoriteFromRow(row sqlc.Favorite) *entity.Favorite { | |||
| 136 | + | fav := &entity.Favorite{ | |||
| 137 | + | ID: entity.FavoriteID(row.ID), | |||
| 138 | + | URL: row.Url, | |||
| 139 | + | Title: row.Title.String, | |||
| 140 | + | FaviconURL: row.FaviconUrl.String, | |||
| 141 | + | Position: int(row.Position), | |||
| 142 | + | CreatedAt: row.CreatedAt.Time, | |||
| 143 | + | UpdatedAt: row.UpdatedAt.Time, | |||
| 144 | + | } | |||
| 145 | + | if row.FolderID.Valid { | |||
| 146 | + | fid := entity.FolderID(row.FolderID.Int64) | |||
| 147 | + | fav.FolderID = &fid | |||
| 148 | + | } | |||
| 149 | + | if row.ShortcutKey.Valid { | |||
| 150 | + | key := int(row.ShortcutKey.Int64) | |||
| 151 | + | fav.ShortcutKey = &key | |||
| 152 | + | } | |||
| 153 | + | return fav | |||
| 154 | + | } | |||
| 155 | + | ||||
| 156 | + | func favoritesFromRows(rows []sqlc.Favorite) []*entity.Favorite { | |||
| 157 | + | favorites := make([]*entity.Favorite, len(rows)) | |||
| 158 | + | for i, row := range rows { | |||
| 159 | + | favorites[i] = favoriteFromRow(row) | |||
| 160 | + | } | |||
| 161 | + | return favorites | |||
| 162 | + | } | |||
| 163 | + | ||||
| 164 | + | // ==================== Folder Repository ==================== | |||
| 165 | + | ||||
| 166 | + | type folderRepo struct { | |||
| 167 | + | queries *sqlc.Queries | |||
| 168 | + | } | |||
| 169 | + | ||||
| 170 | + | // NewFolderRepository creates a new SQLite-backed folder repository. | |||
| 171 | + | func NewFolderRepository(db *sql.DB) repository.FolderRepository { | |||
| 172 | + | return &folderRepo{queries: sqlc.New(db)} | |||
| 173 | + | } | |||
| 174 | + | ||||
| 175 | + | func (r *folderRepo) Save(ctx context.Context, folder *entity.Folder) error { | |||
| 176 | + | log := logging.FromContext(ctx) | |||
| 177 | + | log.Debug().Str("name", folder.Name).Msg("saving folder") | |||
| 178 | + | ||||
| 179 | + | var parentID sql.NullInt64 | |||
| 180 | + | if folder.ParentID != nil { | |||
| 181 | + | parentID = sql.NullInt64{Int64: int64(*folder.ParentID), Valid: true} | |||
| 182 | + | } | |||
| 183 | + | ||||
| 184 | + | row, err := r.queries.CreateFolder(ctx, sqlc.CreateFolderParams{ | |||
| 185 | + | Name: folder.Name, | |||
| 186 | + | Icon: sql.NullString{String: folder.Icon, Valid: folder.Icon != ""}, | |||
| 187 | + | ParentID: parentID, | |||
| 188 | + | }) | |||
| 189 | + | if err != nil { | |||
| 190 | + | return err | |||
| 191 | + | } | |||
| 192 | + | folder.ID = entity.FolderID(row.ID) | |||
| 193 | + | return nil | |||
| 194 | + | } | |||
| 195 | + | ||||
| 196 | + | func (r *folderRepo) FindByID(ctx context.Context, id entity.FolderID) (*entity.Folder, error) { | |||
| 197 | + | row, err := r.queries.GetFolderByID(ctx, int64(id)) | |||
| 198 | + | if err != nil { | |||
| 199 | + | if err == sql.ErrNoRows { | |||
| 200 | + | return nil, nil | |||
| 201 | + | } | |||
| 202 | + | return nil, err | |||
| 203 | + | } | |||
| 204 | + | return folderFromRow(row), nil | |||
| 205 | + | } | |||
| 206 | + | ||||
| 207 | + | func (r *folderRepo) GetAll(ctx context.Context) ([]*entity.Folder, error) { | |||
| 208 | + | rows, err := r.queries.GetAllFolders(ctx) | |||
| 209 | + | if err != nil { | |||
| 210 | + | return nil, err | |||
| 211 | + | } | |||
| 212 | + | return foldersFromRows(rows), nil | |||
| 213 | + | } | |||
| 214 | + | ||||
| 215 | + | func (r *folderRepo) GetChildren(ctx context.Context, parentID *entity.FolderID) ([]*entity.Folder, error) { | |||
| 216 | + | var rows []sqlc.FavoriteFolder | |||
| 217 | + | var err error | |||
| 218 | + | ||||
| 219 | + | if parentID == nil { | |||
| 220 | + | rows, err = r.queries.GetRootFolders(ctx) | |||
| 221 | + | } else { | |||
| 222 | + | rows, err = r.queries.GetChildFolders(ctx, sql.NullInt64{Int64: int64(*parentID), Valid: true}) | |||
| 223 | + | } | |||
| 224 | + | if err != nil { | |||
| 225 | + | return nil, err | |||
| 226 | + | } | |||
| 227 | + | return foldersFromRows(rows), nil | |||
| 228 | + | } | |||
| 229 | + | ||||
| 230 | + | func (r *folderRepo) UpdatePosition(ctx context.Context, id entity.FolderID, position int) error { | |||
| 231 | + | return r.queries.UpdateFolderPosition(ctx, sqlc.UpdateFolderPositionParams{ | |||
| 232 | + | Position: int64(position), | |||
| 233 | + | ID: int64(id), | |||
| 234 | + | }) | |||
| 235 | + | } | |||
| 236 | + | ||||
| 237 | + | func (r *folderRepo) Delete(ctx context.Context, id entity.FolderID) error { | |||
| 238 | + | return r.queries.DeleteFolder(ctx, int64(id)) | |||
| 239 | + | } | |||
| 240 | + | ||||
| 241 | + | func folderFromRow(row sqlc.FavoriteFolder) *entity.Folder { | |||
| 242 | + | folder := &entity.Folder{ | |||
| 243 | + | ID: entity.FolderID(row.ID), | |||
| 244 | + | Name: row.Name, | |||
| 245 | + | Icon: row.Icon.String, | |||
| 246 | + | Position: int(row.Position), | |||
| 247 | + | CreatedAt: row.CreatedAt.Time, | |||
| 248 | + | } | |||
| 249 | + | if row.ParentID.Valid { | |||
| 250 | + | pid := entity.FolderID(row.ParentID.Int64) | |||
| 251 | + | folder.ParentID = &pid | |||
| 252 | + | } | |||
| 253 | + | return folder | |||
| 254 | + | } | |||
| 255 | + | ||||
| 256 | + | func foldersFromRows(rows []sqlc.FavoriteFolder) []*entity.Folder { | |||
| 257 | + | folders := make([]*entity.Folder, len(rows)) | |||
| 258 | + | for i, row := range rows { | |||
| 259 | + | folders[i] = folderFromRow(row) | |||
| 260 | + | } | |||
| 261 | + | return folders | |||
| 262 | + | } | |||
| 263 | + | ||||
| 264 | + | // ==================== Tag Repository ==================== | |||
| 265 | + | ||||
| 266 | + | type tagRepo struct { | |||
| 267 | + | queries *sqlc.Queries | |||
| 268 | + | } | |||
| 269 | + | ||||
| 270 | + | // NewTagRepository creates a new SQLite-backed tag repository. | |||
| 271 | + | func NewTagRepository(db *sql.DB) repository.TagRepository { | |||
| 272 | + | return &tagRepo{queries: sqlc.New(db)} | |||
| 273 | + | } | |||
| 274 | + | ||||
| 275 | + | func (r *tagRepo) Save(ctx context.Context, tag *entity.Tag) error { | |||
| 276 | + | log := logging.FromContext(ctx) | |||
| 277 | + | log.Debug().Str("name", tag.Name).Msg("saving tag") | |||
| 278 | + | ||||
| 279 | + | row, err := r.queries.CreateTag(ctx, sqlc.CreateTagParams{ | |||
| 280 | + | Name: tag.Name, | |||
| 281 | + | Color: tag.Color, | |||
| 282 | + | }) | |||
| 283 | + | if err != nil { | |||
| 284 | + | return err | |||
| 285 | + | } | |||
| 286 | + | tag.ID = entity.TagID(row.ID) | |||
| 287 | + | return nil | |||
| 288 | + | } | |||
| 289 | + | ||||
| 290 | + | func (r *tagRepo) FindByID(ctx context.Context, id entity.TagID) (*entity.Tag, error) { | |||
| 291 | + | row, err := r.queries.GetTagByID(ctx, int64(id)) | |||
| 292 | + | if err != nil { | |||
| 293 | + | if err == sql.ErrNoRows { | |||
| 294 | + | return nil, nil | |||
| 295 | + | } | |||
| 296 | + | return nil, err | |||
| 297 | + | } | |||
| 298 | + | return tagFromRow(row), nil | |||
| 299 | + | } | |||
| 300 | + | ||||
| 301 | + | func (r *tagRepo) FindByName(ctx context.Context, name string) (*entity.Tag, error) { | |||
| 302 | + | row, err := r.queries.GetTagByName(ctx, name) | |||
| 303 | + | if err != nil { | |||
| 304 | + | if err == sql.ErrNoRows { | |||
| 305 | + | return nil, nil | |||
| 306 | + | } | |||
| 307 | + | return nil, err | |||
| 308 | + | } | |||
| 309 | + | return tagFromRow(row), nil | |||
| 310 | + | } | |||
| 311 | + | ||||
| 312 | + | func (r *tagRepo) GetAll(ctx context.Context) ([]*entity.Tag, error) { | |||
| 313 | + | rows, err := r.queries.GetAllTags(ctx) | |||
| 314 | + | if err != nil { | |||
| 315 | + | return nil, err | |||
| 316 | + | } | |||
| 317 | + | return tagsFromRows(rows), nil | |||
| 318 | + | } | |||
| 319 | + | ||||
| 320 | + | func (r *tagRepo) AssignToFavorite(ctx context.Context, tagID entity.TagID, favID entity.FavoriteID) error { | |||
| 321 | + | return r.queries.AssignTagToFavorite(ctx, sqlc.AssignTagToFavoriteParams{ | |||
| 322 | + | FavoriteID: int64(favID), | |||
| 323 | + | TagID: int64(tagID), | |||
| 324 | + | }) | |||
| 325 | + | } | |||
| 326 | + | ||||
| 327 | + | func (r *tagRepo) RemoveFromFavorite(ctx context.Context, tagID entity.TagID, favID entity.FavoriteID) error { | |||
| 328 | + | return r.queries.RemoveTagFromFavorite(ctx, sqlc.RemoveTagFromFavoriteParams{ | |||
| 329 | + | FavoriteID: int64(favID), | |||
| 330 | + | TagID: int64(tagID), | |||
| 331 | + | }) | |||
| 332 | + | } | |||
| 333 | + | ||||
| 334 | + | func (r *tagRepo) GetForFavorite(ctx context.Context, favID entity.FavoriteID) ([]*entity.Tag, error) { | |||
| 335 | + | rows, err := r.queries.GetTagsForFavorite(ctx, int64(favID)) | |||
| 336 | + | if err != nil { | |||
| 337 | + | return nil, err | |||
| 338 | + | } | |||
| 339 | + | return tagsFromRows(rows), nil | |||
| 340 | + | } | |||
| 341 | + | ||||
| 342 | + | func (r *tagRepo) Delete(ctx context.Context, id entity.TagID) error { | |||
| 343 | + | return r.queries.DeleteTag(ctx, int64(id)) | |||
| 344 | + | } | |||
| 345 | + | ||||
| 346 | + | func tagFromRow(row sqlc.FavoriteTag) *entity.Tag { | |||
| 347 | + | return &entity.Tag{ | |||
| 348 | + | ID: entity.TagID(row.ID), | |||
| 349 | + | Name: row.Name, | |||
| 350 | + | Color: row.Color, | |||
| 351 | + | CreatedAt: row.CreatedAt.Time, | |||
| 352 | + | } | |||
| 353 | + | } | |||
| 354 | + | ||||
| 355 | + | func tagsFromRows(rows []sqlc.FavoriteTag) []*entity.Tag { | |||
| 356 | + | tags := make([]*entity.Tag, len(rows)) | |||
| 357 | + | for i, row := range rows { | |||
| 358 | + | tags[i] = tagFromRow(row) | |||
| 359 | + | } | |||
| 360 | + | return tags | |||
| 361 | + | } | |||
A internal/infrastructure/persistence/sqlite/filter_repo.go +42 -0
| @@ -0,0 +1,42 @@ | |||
| 1 | + | package sqlite | |
| 2 | + | ||
| 3 | + | import ( | |
| 4 | + | "context" | |
| 5 | + | "database/sql" | |
| 6 | + | ||
| 7 | + | "github.com/bnema/dumber/internal/domain/repository" | |
| 8 | + | "github.com/bnema/dumber/internal/infrastructure/persistence/sqlite/sqlc" | |
| 9 | + | "github.com/bnema/dumber/internal/logging" | |
| 10 | + | ) | |
| 11 | + | ||
| 12 | + | type contentWhitelistRepo struct { | |
| 13 | + | queries *sqlc.Queries | |
| 14 | + | } | |
| 15 | + | ||
| 16 | + | // NewContentWhitelistRepository creates a new SQLite-backed content whitelist repository. | |
| 17 | + | func NewContentWhitelistRepository(db *sql.DB) repository.ContentWhitelistRepository { | |
| 18 | + | return &contentWhitelistRepo{queries: sqlc.New(db)} | |
| 19 | + | } | |
| 20 | + | ||
| 21 | + | func (r *contentWhitelistRepo) Add(ctx context.Context, domain string) error { | |
| 22 | + | log := logging.FromContext(ctx) | |
| 23 | + | log.Debug().Str("domain", domain).Msg("adding domain to whitelist") | |
| 24 | + | ||
| 25 | + | return r.queries.AddToWhitelist(ctx, domain) | |
| 26 | + | } | |
| 27 | + | ||
| 28 | + | func (r *contentWhitelistRepo) Remove(ctx context.Context, domain string) error { | |
| 29 | + | return r.queries.RemoveFromWhitelist(ctx, domain) | |
| 30 | + | } | |
| 31 | + | ||
| 32 | + | func (r *contentWhitelistRepo) Contains(ctx context.Context, domain string) (bool, error) { | |
| 33 | + | count, err := r.queries.IsWhitelisted(ctx, domain) | |
| 34 | + | if err != nil { | |
| 35 | + | return false, err | |
| 36 | + | } | |
| 37 | + | return count > 0, nil | |
| 38 | + | } | |
| 39 | + | ||
| 40 | + | func (r *contentWhitelistRepo) GetAll(ctx context.Context) ([]string, error) { | |
| 41 | + | return r.queries.GetAllWhitelistedDomains(ctx) | |
| 42 | + | } | |
| @@ -0,0 +1,42 @@ | |||||
| 1 | + | package sqlite | |||
| 2 | + | ||||
| 3 | + | import ( | |||
| 4 | + | "context" | |||
| 5 | + | "database/sql" | |||
| 6 | + | ||||
| 7 | + | "github.com/bnema/dumber/internal/domain/repository" | |||
| 8 | + | "github.com/bnema/dumber/internal/infrastructure/persistence/sqlite/sqlc" | |||
| 9 | + | "github.com/bnema/dumber/internal/logging" | |||
| 10 | + | ) | |||
| 11 | + | ||||
| 12 | + | type contentWhitelistRepo struct { | |||
| 13 | + | queries *sqlc.Queries | |||
| 14 | + | } | |||
| 15 | + | ||||
| 16 | + | // NewContentWhitelistRepository creates a new SQLite-backed content whitelist repository. | |||
| 17 | + | func NewContentWhitelistRepository(db *sql.DB) repository.ContentWhitelistRepository { | |||
| 18 | + | return &contentWhitelistRepo{queries: sqlc.New(db)} | |||
| 19 | + | } | |||
| 20 | + | ||||
| 21 | + | func (r *contentWhitelistRepo) Add(ctx context.Context, domain string) error { | |||
| 22 | + | log := logging.FromContext(ctx) | |||
| 23 | + | log.Debug().Str("domain", domain).Msg("adding domain to whitelist") | |||
| 24 | + | ||||
| 25 | + | return r.queries.AddToWhitelist(ctx, domain) | |||
| 26 | + | } | |||
| 27 | + | ||||
| 28 | + | func (r *contentWhitelistRepo) Remove(ctx context.Context, domain string) error { | |||
| 29 | + | return r.queries.RemoveFromWhitelist(ctx, domain) | |||
| 30 | + | } | |||
| 31 | + | ||||
| 32 | + | func (r *contentWhitelistRepo) Contains(ctx context.Context, domain string) (bool, error) { | |||
| 33 | + | count, err := r.queries.IsWhitelisted(ctx, domain) | |||
| 34 | + | if err != nil { | |||
| 35 | + | return false, err | |||
| 36 | + | } | |||
| 37 | + | return count > 0, nil | |||
| 38 | + | } | |||
| 39 | + | ||||
| 40 | + | func (r *contentWhitelistRepo) GetAll(ctx context.Context) ([]string, error) { | |||
| 41 | + | return r.queries.GetAllWhitelistedDomains(ctx) | |||
| 42 | + | } | |||
A internal/infrastructure/persistence/sqlite/history_repo.go +107 -0
| @@ -0,0 +1,107 @@ | |||
| 1 | + | package sqlite | |
| 2 | + | ||
| 3 | + | import ( | |
| 4 | + | "context" | |
| 5 | + | "database/sql" | |
| 6 | + | "time" | |
| 7 | + | ||
| 8 | + | "github.com/bnema/dumber/internal/domain/entity" | |
| 9 | + | "github.com/bnema/dumber/internal/domain/repository" | |
| 10 | + | "github.com/bnema/dumber/internal/infrastructure/persistence/sqlite/sqlc" | |
| 11 | + | "github.com/bnema/dumber/internal/logging" | |
| 12 | + | ) | |
| 13 | + | ||
| 14 | + | type historyRepo struct { | |
| 15 | + | queries *sqlc.Queries | |
| 16 | + | } | |
| 17 | + | ||
| 18 | + | // NewHistoryRepository creates a new SQLite-backed history repository. | |
| 19 | + | func NewHistoryRepository(db *sql.DB) repository.HistoryRepository { | |
| 20 | + | return &historyRepo{queries: sqlc.New(db)} | |
| 21 | + | } | |
| 22 | + | ||
| 23 | + | func (r *historyRepo) Save(ctx context.Context, entry *entity.HistoryEntry) error { | |
| 24 | + | log := logging.FromContext(ctx) | |
| 25 | + | log.Debug().Str("url", entry.URL).Msg("saving history entry") | |
| 26 | + | ||
| 27 | + | return r.queries.UpsertHistory(ctx, sqlc.UpsertHistoryParams{ | |
| 28 | + | Url: entry.URL, | |
| 29 | + | Title: sql.NullString{String: entry.Title, Valid: entry.Title != ""}, | |
| 30 | + | FaviconUrl: sql.NullString{String: entry.FaviconURL, Valid: entry.FaviconURL != ""}, | |
| 31 | + | }) | |
| 32 | + | } | |
| 33 | + | ||
| 34 | + | func (r *historyRepo) FindByURL(ctx context.Context, url string) (*entity.HistoryEntry, error) { | |
| 35 | + | row, err := r.queries.GetHistoryByURL(ctx, url) | |
| 36 | + | if err != nil { | |
| 37 | + | if err == sql.ErrNoRows { | |
| 38 | + | return nil, nil | |
| 39 | + | } | |
| 40 | + | return nil, err | |
| 41 | + | } | |
| 42 | + | return historyFromRow(row), nil | |
| 43 | + | } | |
| 44 | + | ||
| 45 | + | func (r *historyRepo) Search(ctx context.Context, query string, limit int) ([]entity.HistoryMatch, error) { | |
| 46 | + | rows, err := r.queries.SearchHistory(ctx, sqlc.SearchHistoryParams{ | |
| 47 | + | Column1: sql.NullString{String: query, Valid: true}, | |
| 48 | + | Column2: sql.NullString{String: query, Valid: true}, | |
| 49 | + | Limit: int64(limit), | |
| 50 | + | }) | |
| 51 | + | if err != nil { | |
| 52 | + | return nil, err | |
| 53 | + | } | |
| 54 | + | ||
| 55 | + | matches := make([]entity.HistoryMatch, len(rows)) | |
| 56 | + | for i, row := range rows { | |
| 57 | + | matches[i] = entity.HistoryMatch{ | |
| 58 | + | Entry: historyFromRow(row), | |
| 59 | + | Score: 1.0, // Basic score, could be enhanced with FTS ranking | |
| 60 | + | } | |
| 61 | + | } | |
| 62 | + | return matches, nil | |
| 63 | + | } | |
| 64 | + | ||
| 65 | + | func (r *historyRepo) GetRecent(ctx context.Context, limit, offset int) ([]*entity.HistoryEntry, error) { | |
| 66 | + | rows, err := r.queries.GetRecentHistory(ctx, sqlc.GetRecentHistoryParams{ | |
| 67 | + | Limit: int64(limit), | |
| 68 | + | Offset: int64(offset), | |
| 69 | + | }) | |
| 70 | + | if err != nil { | |
| 71 | + | return nil, err | |
| 72 | + | } | |
| 73 | + | ||
| 74 | + | entries := make([]*entity.HistoryEntry, len(rows)) | |
| 75 | + | for i, row := range rows { | |
| 76 | + | entries[i] = historyFromRow(row) | |
| 77 | + | } | |
| 78 | + | return entries, nil | |
| 79 | + | } | |
| 80 | + | ||
| 81 | + | func (r *historyRepo) IncrementVisitCount(ctx context.Context, url string) error { | |
| 82 | + | return r.queries.IncrementVisitCount(ctx, url) | |
| 83 | + | } | |
| 84 | + | ||
| 85 | + | func (r *historyRepo) Delete(ctx context.Context, id int64) error { | |
| 86 | + | return r.queries.DeleteHistoryByID(ctx, id) | |
| 87 | + | } | |
| 88 | + | ||
| 89 | + | func (r *historyRepo) DeleteOlderThan(ctx context.Context, before time.Time) error { | |
| 90 | + | return r.queries.DeleteHistoryOlderThan(ctx, sql.NullTime{Time: before, Valid: true}) | |
| 91 | + | } | |
| 92 | + | ||
| 93 | + | func (r *historyRepo) DeleteAll(ctx context.Context) error { | |
| 94 | + | return r.queries.DeleteAllHistory(ctx) | |
| 95 | + | } | |
| 96 | + | ||
| 97 | + | func historyFromRow(row sqlc.History) *entity.HistoryEntry { | |
| 98 | + | return &entity.HistoryEntry{ | |
| 99 | + | ID: row.ID, | |
| 100 | + | URL: row.Url, | |
| 101 | + | Title: row.Title.String, | |
| 102 | + | FaviconURL: row.FaviconUrl.String, | |
| 103 | + | VisitCount: row.VisitCount.Int64, | |
| 104 | + | LastVisited: row.LastVisited.Time, | |
| 105 | + | CreatedAt: row.CreatedAt.Time, | |
| 106 | + | } | |
| 107 | + | } | |
| @@ -0,0 +1,107 @@ | |||||
| 1 | + | package sqlite | |||
| 2 | + | ||||
| 3 | + | import ( | |||
| 4 | + | "context" | |||
| 5 | + | "database/sql" | |||
| 6 | + | "time" | |||
| 7 | + | ||||
| 8 | + | "github.com/bnema/dumber/internal/domain/entity" | |||
| 9 | + | "github.com/bnema/dumber/internal/domain/repository" | |||
| 10 | + | "github.com/bnema/dumber/internal/infrastructure/persistence/sqlite/sqlc" | |||
| 11 | + | "github.com/bnema/dumber/internal/logging" | |||
| 12 | + | ) | |||
| 13 | + | ||||
| 14 | + | type historyRepo struct { | |||
| 15 | + | queries *sqlc.Queries | |||
| 16 | + | } | |||
| 17 | + | ||||
| 18 | + | // NewHistoryRepository creates a new SQLite-backed history repository. | |||
| 19 | + | func NewHistoryRepository(db *sql.DB) repository.HistoryRepository { | |||
| 20 | + | return &historyRepo{queries: sqlc.New(db)} | |||
| 21 | + | } | |||
| 22 | + | ||||
| 23 | + | func (r *historyRepo) Save(ctx context.Context, entry *entity.HistoryEntry) error { | |||
| 24 | + | log := logging.FromContext(ctx) | |||
| 25 | + | log.Debug().Str("url", entry.URL).Msg("saving history entry") | |||
| 26 | + | ||||
| 27 | + | return r.queries.UpsertHistory(ctx, sqlc.UpsertHistoryParams{ | |||
| 28 | + | Url: entry.URL, | |||
| 29 | + | Title: sql.NullString{String: entry.Title, Valid: entry.Title != ""}, | |||
| 30 | + | FaviconUrl: sql.NullString{String: entry.FaviconURL, Valid: entry.FaviconURL != ""}, | |||
| 31 | + | }) | |||
| 32 | + | } | |||
| 33 | + | ||||
| 34 | + | func (r *historyRepo) FindByURL(ctx context.Context, url string) (*entity.HistoryEntry, error) { | |||
| 35 | + | row, err := r.queries.GetHistoryByURL(ctx, url) | |||
| 36 | + | if err != nil { | |||
| 37 | + | if err == sql.ErrNoRows { | |||
| 38 | + | return nil, nil | |||
| 39 | + | } | |||
| 40 | + | return nil, err | |||
| 41 | + | } | |||
| 42 | + | return historyFromRow(row), nil | |||
| 43 | + | } | |||
| 44 | + | ||||
| 45 | + | func (r *historyRepo) Search(ctx context.Context, query string, limit int) ([]entity.HistoryMatch, error) { | |||
| 46 | + | rows, err := r.queries.SearchHistory(ctx, sqlc.SearchHistoryParams{ | |||
| 47 | + | Column1: sql.NullString{String: query, Valid: true}, | |||
| 48 | + | Column2: sql.NullString{String: query, Valid: true}, | |||
| 49 | + | Limit: int64(limit), | |||
| 50 | + | }) | |||
| 51 | + | if err != nil { | |||
| 52 | + | return nil, err | |||
| 53 | + | } | |||
| 54 | + | ||||
| 55 | + | matches := make([]entity.HistoryMatch, len(rows)) | |||
| 56 | + | for i, row := range rows { | |||
| 57 | + | matches[i] = entity.HistoryMatch{ | |||
| 58 | + | Entry: historyFromRow(row), | |||
| 59 | + | Score: 1.0, // Basic score, could be enhanced with FTS ranking | |||
| 60 | + | } | |||
| 61 | + | } | |||
| 62 | + | return matches, nil | |||
| 63 | + | } | |||
| 64 | + | ||||
| 65 | + | func (r *historyRepo) GetRecent(ctx context.Context, limit, offset int) ([]*entity.HistoryEntry, error) { | |||
| 66 | + | rows, err := r.queries.GetRecentHistory(ctx, sqlc.GetRecentHistoryParams{ | |||
| 67 | + | Limit: int64(limit), | |||
| 68 | + | Offset: int64(offset), | |||
| 69 | + | }) | |||
| 70 | + | if err != nil { | |||
| 71 | + | return nil, err | |||
| 72 | + | } | |||
| 73 | + | ||||
| 74 | + | entries := make([]*entity.HistoryEntry, len(rows)) | |||
| 75 | + | for i, row := range rows { | |||
| 76 | + | entries[i] = historyFromRow(row) | |||
| 77 | + | } | |||
| 78 | + | return entries, nil | |||
| 79 | + | } | |||
| 80 | + | ||||
| 81 | + | func (r *historyRepo) IncrementVisitCount(ctx context.Context, url string) error { | |||
| 82 | + | return r.queries.IncrementVisitCount(ctx, url) | |||
| 83 | + | } | |||
| 84 | + | ||||
| 85 | + | func (r *historyRepo) Delete(ctx context.Context, id int64) error { | |||
| 86 | + | return r.queries.DeleteHistoryByID(ctx, id) | |||
| 87 | + | } | |||
| 88 | + | ||||
| 89 | + | func (r *historyRepo) DeleteOlderThan(ctx context.Context, before time.Time) error { | |||
| 90 | + | return r.queries.DeleteHistoryOlderThan(ctx, sql.NullTime{Time: before, Valid: true}) | |||
| 91 | + | } | |||
| 92 | + | ||||
| 93 | + | func (r *historyRepo) DeleteAll(ctx context.Context) error { | |||
| 94 | + | return r.queries.DeleteAllHistory(ctx) | |||
| 95 | + | } | |||
| 96 | + | ||||
| 97 | + | func historyFromRow(row sqlc.History) *entity.HistoryEntry { | |||
| 98 | + | return &entity.HistoryEntry{ | |||
| 99 | + | ID: row.ID, | |||
| 100 | + | URL: row.Url, | |||
| 101 | + | Title: row.Title.String, | |||
| 102 | + | FaviconURL: row.FaviconUrl.String, | |||
| 103 | + | VisitCount: row.VisitCount.Int64, | |||
| 104 | + | LastVisited: row.LastVisited.Time, | |||
| 105 | + | CreatedAt: row.CreatedAt.Time, | |||
| 106 | + | } | |||
| 107 | + | } | |||
A internal/infrastructure/persistence/sqlite/migrations.go +68 -0
| @@ -0,0 +1,68 @@ | |||
| 1 | + | package sqlite | |
| 2 | + | ||
| 3 | + | import ( | |
| 4 | + | "context" | |
| 5 | + | "database/sql" | |
| 6 | + | "embed" | |
| 7 | + | "fmt" | |
| 8 | + | ||
| 9 | + | "github.com/bnema/dumber/internal/logging" | |
| 10 | + | "github.com/pressly/goose/v3" | |
| 11 | + | ) | |
| 12 | + | ||
| 13 | + | //go:embed migrations/*.sql | |
| 14 | + | var embedMigrations embed.FS | |
| 15 | + | ||
| 16 | + | // RunMigrations applies all pending migrations to the database. | |
| 17 | + | // Uses goose with embedded SQL files for automatic schema management. | |
| 18 | + | func RunMigrations(ctx context.Context, db *sql.DB) error { | |
| 19 | + | log := logging.FromContext(ctx) | |
| 20 | + | ||
| 21 | + | // Configure goose | |
| 22 | + | goose.SetBaseFS(embedMigrations) | |
| 23 | + | ||
| 24 | + | if err := goose.SetDialect("sqlite3"); err != nil { | |
| 25 | + | return fmt.Errorf("failed to set goose dialect: %w", err) | |
| 26 | + | } | |
| 27 | + | ||
| 28 | + | // Disable verbose logging from goose | |
| 29 | + | goose.SetLogger(goose.NopLogger()) | |
| 30 | + | ||
| 31 | + | // Get current version before migration | |
| 32 | + | currentVersion, err := goose.GetDBVersion(db) | |
| 33 | + | if err != nil { | |
| 34 | + | log.Debug().Err(err).Msg("could not get current db version (may be new database)") | |
| 35 | + | currentVersion = 0 | |
| 36 | + | } | |
| 37 | + | ||
| 38 | + | // Run migrations | |
| 39 | + | if err := goose.Up(db, "migrations"); err != nil { | |
| 40 | + | return fmt.Errorf("failed to run migrations: %w", err) | |
| 41 | + | } | |
| 42 | + | ||
| 43 | + | // Get new version after migration | |
| 44 | + | newVersion, err := goose.GetDBVersion(db) | |
| 45 | + | if err != nil { | |
| 46 | + | return fmt.Errorf("failed to get db version after migration: %w", err) | |
| 47 | + | } | |
| 48 | + | ||
| 49 | + | if newVersion > currentVersion { | |
| 50 | + | log.Info(). | |
| 51 | + | Int64("from_version", currentVersion). | |
| 52 | + | Int64("to_version", newVersion). | |
| 53 | + | Msg("database migrations applied") | |
| 54 | + | } else { | |
| 55 | + | log.Debug().Int64("version", newVersion).Msg("database schema up to date") | |
| 56 | + | } | |
| 57 | + | ||
| 58 | + | return nil | |
| 59 | + | } | |
| 60 | + | ||
| 61 | + | // GetMigrationStatus returns the current migration version. | |
| 62 | + | func GetMigrationStatus(db *sql.DB) (int64, error) { | |
| 63 | + | goose.SetBaseFS(embedMigrations) | |
| 64 | + | if err := goose.SetDialect("sqlite3"); err != nil { | |
| 65 | + | return 0, err | |
| 66 | + | } | |
| 67 | + | return goose.GetDBVersion(db) | |
| 68 | + | } | |
| @@ -0,0 +1,68 @@ | |||||
| 1 | + | package sqlite | |||
| 2 | + | ||||
| 3 | + | import ( | |||
| 4 | + | "context" | |||
| 5 | + | "database/sql" | |||
| 6 | + | "embed" | |||
| 7 | + | "fmt" | |||
| 8 | + | ||||
| 9 | + | "github.com/bnema/dumber/internal/logging" | |||
| 10 | + | "github.com/pressly/goose/v3" | |||
| 11 | + | ) | |||
| 12 | + | ||||
| 13 | + | //go:embed migrations/*.sql | |||
| 14 | + | var embedMigrations embed.FS | |||
| 15 | + | ||||
| 16 | + | // RunMigrations applies all pending migrations to the database. | |||
| 17 | + | // Uses goose with embedded SQL files for automatic schema management. | |||
| 18 | + | func RunMigrations(ctx context.Context, db *sql.DB) error { | |||
| 19 | + | log := logging.FromContext(ctx) | |||
| 20 | + | ||||
| 21 | + | // Configure goose | |||
| 22 | + | goose.SetBaseFS(embedMigrations) | |||
| 23 | + | ||||
| 24 | + | if err := goose.SetDialect("sqlite3"); err != nil { | |||
| 25 | + | return fmt.Errorf("failed to set goose dialect: %w", err) | |||
| 26 | + | } | |||
| 27 | + | ||||
| 28 | + | // Disable verbose logging from goose | |||
| 29 | + | goose.SetLogger(goose.NopLogger()) | |||
| 30 | + | ||||
| 31 | + | // Get current version before migration | |||
| 32 | + | currentVersion, err := goose.GetDBVersion(db) | |||
| 33 | + | if err != nil { | |||
| 34 | + | log.Debug().Err(err).Msg("could not get current db version (may be new database)") | |||
| 35 | + | currentVersion = 0 | |||
| 36 | + | } | |||
| 37 | + | ||||
| 38 | + | // Run migrations | |||
| 39 | + | if err := goose.Up(db, "migrations"); err != nil { | |||
| 40 | + | return fmt.Errorf("failed to run migrations: %w", err) | |||
| 41 | + | } | |||
| 42 | + | ||||
| 43 | + | // Get new version after migration | |||
| 44 | + | newVersion, err := goose.GetDBVersion(db) | |||
| 45 | + | if err != nil { | |||
| 46 | + | return fmt.Errorf("failed to get db version after migration: %w", err) | |||
| 47 | + | } | |||
| 48 | + | ||||
| 49 | + | if newVersion > currentVersion { | |||
| 50 | + | log.Info(). | |||
| 51 | + | Int64("from_version", currentVersion). | |||
| 52 | + | Int64("to_version", newVersion). | |||
| 53 | + | Msg("database migrations applied") | |||
| 54 | + | } else { | |||
| 55 | + | log.Debug().Int64("version", newVersion).Msg("database schema up to date") | |||
| 56 | + | } | |||
| 57 | + | ||||
| 58 | + | return nil | |||
| 59 | + | } | |||
| 60 | + | ||||
| 61 | + | // GetMigrationStatus returns the current migration version. | |||
| 62 | + | func GetMigrationStatus(db *sql.DB) (int64, error) { | |||
| 63 | + | goose.SetBaseFS(embedMigrations) | |||
| 64 | + | if err := goose.SetDialect("sqlite3"); err != nil { | |||
| 65 | + | return 0, err | |||
| 66 | + | } | |||
| 67 | + | return goose.GetDBVersion(db) | |||
| 68 | + | } | |||
A internal/infrastructure/persistence/sqlite/migrations/001_history.sql +45 -0
| @@ -0,0 +1,45 @@ | |||
| 1 | + | -- +goose Up | |
| 2 | + | -- History tracking for visited URLs | |
| 3 | + | ||
| 4 | + | CREATE TABLE IF NOT EXISTS history ( | |
| 5 | + | id INTEGER PRIMARY KEY AUTOINCREMENT, | |
| 6 | + | url TEXT NOT NULL UNIQUE, | |
| 7 | + | title TEXT, | |
| 8 | + | favicon_url TEXT, | |
| 9 | + | visit_count INTEGER DEFAULT 1, | |
| 10 | + | last_visited DATETIME DEFAULT CURRENT_TIMESTAMP, | |
| 11 | + | created_at DATETIME DEFAULT CURRENT_TIMESTAMP | |
| 12 | + | ); | |
| 13 | + | ||
| 14 | + | CREATE INDEX IF NOT EXISTS idx_history_url ON history(url); | |
| 15 | + | CREATE INDEX IF NOT EXISTS idx_history_last_visited ON history(last_visited); | |
| 16 | + | CREATE INDEX IF NOT EXISTS idx_history_visit_count_last_visited ON history(visit_count DESC, last_visited DESC); | |
| 17 | + | ||
| 18 | + | -- FTS5 for history search | |
| 19 | + | CREATE VIRTUAL TABLE IF NOT EXISTS history_fts USING fts5( | |
| 20 | + | url, | |
| 21 | + | title, | |
| 22 | + | content='history', | |
| 23 | + | content_rowid='id' | |
| 24 | + | ); | |
| 25 | + | ||
| 26 | + | -- Triggers to keep FTS in sync | |
| 27 | + | CREATE TRIGGER IF NOT EXISTS history_fts_insert AFTER INSERT ON history BEGIN | |
| 28 | + | INSERT INTO history_fts(rowid, url, title) VALUES (NEW.id, NEW.url, NEW.title); | |
| 29 | + | END; | |
| 30 | + | ||
| 31 | + | CREATE TRIGGER IF NOT EXISTS history_fts_delete AFTER DELETE ON history BEGIN | |
| 32 | + | INSERT INTO history_fts(history_fts, rowid, url, title) VALUES('delete', OLD.id, OLD.url, OLD.title); | |
| 33 | + | END; | |
| 34 | + | ||
| 35 | + | CREATE TRIGGER IF NOT EXISTS history_fts_update AFTER UPDATE ON history BEGIN | |
| 36 | + | INSERT INTO history_fts(history_fts, rowid, url, title) VALUES('delete', OLD.id, OLD.url, OLD.title); | |
| 37 | + | INSERT INTO history_fts(rowid, url, title) VALUES (NEW.id, NEW.url, NEW.title); | |
| 38 | + | END; | |
| 39 | + | ||
| 40 | + | -- +goose Down | |
| 41 | + | DROP TRIGGER IF EXISTS history_fts_update; | |
| 42 | + | DROP TRIGGER IF EXISTS history_fts_delete; | |
| 43 | + | DROP TRIGGER IF EXISTS history_fts_insert; | |
| 44 | + | DROP TABLE IF EXISTS history_fts; | |
| 45 | + | DROP TABLE IF EXISTS history; | |
| @@ -0,0 +1,45 @@ | |||||
| 1 | + | -- +goose Up | |||
| 2 | + | -- History tracking for visited URLs | |||
| 3 | + | ||||
| 4 | + | CREATE TABLE IF NOT EXISTS history ( | |||
| 5 | + | id INTEGER PRIMARY KEY AUTOINCREMENT, | |||
| 6 | + | url TEXT NOT NULL UNIQUE, | |||
| 7 | + | title TEXT, | |||
| 8 | + | favicon_url TEXT, | |||
| 9 | + | visit_count INTEGER DEFAULT 1, | |||
| 10 | + | last_visited DATETIME DEFAULT CURRENT_TIMESTAMP, | |||
| 11 | + | created_at DATETIME DEFAULT CURRENT_TIMESTAMP | |||
| 12 | + | ); | |||
| 13 | + | ||||
| 14 | + | CREATE INDEX IF NOT EXISTS idx_history_url ON history(url); | |||
| 15 | + | CREATE INDEX IF NOT EXISTS idx_history_last_visited ON history(last_visited); | |||
| 16 | + | CREATE INDEX IF NOT EXISTS idx_history_visit_count_last_visited ON history(visit_count DESC, last_visited DESC); | |||
| 17 | + | ||||
| 18 | + | -- FTS5 for history search | |||
| 19 | + | CREATE VIRTUAL TABLE IF NOT EXISTS history_fts USING fts5( | |||
| 20 | + | url, | |||
| 21 | + | title, | |||
| 22 | + | content='history', | |||
| 23 | + | content_rowid='id' | |||
| 24 | + | ); | |||
| 25 | + | ||||
| 26 | + | -- Triggers to keep FTS in sync | |||
| 27 | + | CREATE TRIGGER IF NOT EXISTS history_fts_insert AFTER INSERT ON history BEGIN | |||
| 28 | + | INSERT INTO history_fts(rowid, url, title) VALUES (NEW.id, NEW.url, NEW.title); | |||
| 29 | + | END; | |||
| 30 | + | ||||
| 31 | + | CREATE TRIGGER IF NOT EXISTS history_fts_delete AFTER DELETE ON history BEGIN | |||
| 32 | + | INSERT INTO history_fts(history_fts, rowid, url, title) VALUES('delete', OLD.id, OLD.url, OLD.title); | |||
| 33 | + | END; | |||
| 34 | + | ||||
| 35 | + | CREATE TRIGGER IF NOT EXISTS history_fts_update AFTER UPDATE ON history BEGIN | |||
| 36 | + | INSERT INTO history_fts(history_fts, rowid, url, title) VALUES('delete', OLD.id, OLD.url, OLD.title); | |||
| 37 | + | INSERT INTO history_fts(rowid, url, title) VALUES (NEW.id, NEW.url, NEW.title); | |||
| 38 | + | END; | |||
| 39 | + | ||||
| 40 | + | -- +goose Down | |||
| 41 | + | DROP TRIGGER IF EXISTS history_fts_update; | |||
| 42 | + | DROP TRIGGER IF EXISTS history_fts_delete; | |||
| 43 | + | DROP TRIGGER IF EXISTS history_fts_insert; | |||
| 44 | + | DROP TABLE IF EXISTS history_fts; | |||
| 45 | + | DROP TABLE IF EXISTS history; | |||
A internal/infrastructure/persistence/sqlite/migrations/002_zoom_levels.sql +13 -0
| @@ -0,0 +1,13 @@ | |||
| 1 | + | -- +goose Up | |
| 2 | + | -- Per-domain zoom levels with constraints | |
| 3 | + | ||
| 4 | + | CREATE TABLE IF NOT EXISTS zoom_levels ( | |
| 5 | + | domain TEXT PRIMARY KEY, | |
| 6 | + | zoom_factor REAL NOT NULL DEFAULT 1.0 CHECK(zoom_factor >= 0.25 AND zoom_factor <= 5.0), | |
| 7 | + | updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP | |
| 8 | + | ); | |
| 9 | + | ||
| 10 | + | CREATE INDEX IF NOT EXISTS idx_zoom_levels_updated_at ON zoom_levels(updated_at); | |
| 11 | + | ||
| 12 | + | -- +goose Down | |
| 13 | + | DROP TABLE IF EXISTS zoom_levels; | |
| @@ -0,0 +1,13 @@ | |||||
| 1 | + | -- +goose Up | |||
| 2 | + | -- Per-domain zoom levels with constraints | |||
| 3 | + | ||||
| 4 | + | CREATE TABLE IF NOT EXISTS zoom_levels ( | |||
| 5 | + | domain TEXT PRIMARY KEY, | |||
| 6 | + | zoom_factor REAL NOT NULL DEFAULT 1.0 CHECK(zoom_factor >= 0.25 AND zoom_factor <= 5.0), | |||
| 7 | + | updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP | |||
| 8 | + | ); | |||
| 9 | + | ||||
| 10 | + | CREATE INDEX IF NOT EXISTS idx_zoom_levels_updated_at ON zoom_levels(updated_at); | |||
| 11 | + | ||||
| 12 | + | -- +goose Down | |||
| 13 | + | DROP TABLE IF EXISTS zoom_levels; | |||
A internal/infrastructure/persistence/sqlite/migrations/003_favorites.sql +59 -0
| @@ -0,0 +1,59 @@ | |||
| 1 | + | -- +goose Up | |
| 2 | + | -- Favorites (bookmarks) with folders, tags, and shortcuts | |
| 3 | + | ||
| 4 | + | -- Folders for organizing favorites | |
| 5 | + | CREATE TABLE IF NOT EXISTS favorite_folders ( | |
| 6 | + | id INTEGER PRIMARY KEY AUTOINCREMENT, | |
| 7 | + | name TEXT NOT NULL, | |
| 8 | + | icon TEXT, | |
| 9 | + | parent_id INTEGER REFERENCES favorite_folders(id) ON DELETE CASCADE, | |
| 10 | + | position INTEGER NOT NULL DEFAULT 0, | |
| 11 | + | created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP | |
| 12 | + | ); | |
| 13 | + | ||
| 14 | + | CREATE INDEX IF NOT EXISTS idx_favorite_folders_position ON favorite_folders(position); | |
| 15 | + | CREATE INDEX IF NOT EXISTS idx_favorite_folders_parent ON favorite_folders(parent_id); | |
| 16 | + | ||
| 17 | + | -- Tags for labeling favorites | |
| 18 | + | CREATE TABLE IF NOT EXISTS favorite_tags ( | |
| 19 | + | id INTEGER PRIMARY KEY AUTOINCREMENT, | |
| 20 | + | name TEXT NOT NULL UNIQUE, | |
| 21 | + | color TEXT NOT NULL DEFAULT '#808080', | |
| 22 | + | created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP | |
| 23 | + | ); | |
| 24 | + | ||
| 25 | + | -- Favorites table | |
| 26 | + | CREATE TABLE IF NOT EXISTS favorites ( | |
| 27 | + | id INTEGER PRIMARY KEY AUTOINCREMENT, | |
| 28 | + | url TEXT NOT NULL UNIQUE, | |
| 29 | + | title TEXT, | |
| 30 | + | favicon_url TEXT, | |
| 31 | + | folder_id INTEGER REFERENCES favorite_folders(id) ON DELETE SET NULL, | |
| 32 | + | shortcut_key INTEGER CHECK (shortcut_key BETWEEN 1 AND 9), | |
| 33 | + | position INTEGER NOT NULL DEFAULT 0, | |
| 34 | + | created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, | |
| 35 | + | updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP | |
| 36 | + | ); | |
| 37 | + | ||
| 38 | + | CREATE INDEX IF NOT EXISTS idx_favorites_position ON favorites(position); | |
| 39 | + | CREATE INDEX IF NOT EXISTS idx_favorites_url ON favorites(url); | |
| 40 | + | CREATE INDEX IF NOT EXISTS idx_favorites_folder ON favorites(folder_id); | |
| 41 | + | CREATE UNIQUE INDEX IF NOT EXISTS idx_favorites_shortcut ON favorites(shortcut_key) WHERE shortcut_key IS NOT NULL; | |
| 42 | + | ||
| 43 | + | -- Tag assignments (junction table) | |
| 44 | + | CREATE TABLE IF NOT EXISTS favorite_tag_assignments ( | |
| 45 | + | favorite_id INTEGER NOT NULL, | |
| 46 | + | tag_id INTEGER NOT NULL, | |
| 47 | + | PRIMARY KEY (favorite_id, tag_id), | |
| 48 | + | FOREIGN KEY (favorite_id) REFERENCES favorites(id) ON DELETE CASCADE, | |
| 49 | + | FOREIGN KEY (tag_id) REFERENCES favorite_tags(id) ON DELETE CASCADE | |
| 50 | + | ); | |
| 51 | + | ||
| 52 | + | CREATE INDEX IF NOT EXISTS idx_fta_favorite ON favorite_tag_assignments(favorite_id); | |
| 53 | + | CREATE INDEX IF NOT EXISTS idx_fta_tag ON favorite_tag_assignments(tag_id); | |
| 54 | + | ||
| 55 | + | -- +goose Down | |
| 56 | + | DROP TABLE IF EXISTS favorite_tag_assignments; | |
| 57 | + | DROP TABLE IF EXISTS favorites; | |
| 58 | + | DROP TABLE IF EXISTS favorite_tags; | |
| 59 | + | DROP TABLE IF EXISTS favorite_folders; | |
| @@ -0,0 +1,59 @@ | |||||
| 1 | + | -- +goose Up | |||
| 2 | + | -- Favorites (bookmarks) with folders, tags, and shortcuts | |||
| 3 | + | ||||
| 4 | + | -- Folders for organizing favorites | |||
| 5 | + | CREATE TABLE IF NOT EXISTS favorite_folders ( | |||
| 6 | + | id INTEGER PRIMARY KEY AUTOINCREMENT, | |||
| 7 | + | name TEXT NOT NULL, | |||
| 8 | + | icon TEXT, | |||
| 9 | + | parent_id INTEGER REFERENCES favorite_folders(id) ON DELETE CASCADE, | |||
| 10 | + | position INTEGER NOT NULL DEFAULT 0, | |||
| 11 | + | created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP | |||
| 12 | + | ); | |||
| 13 | + | ||||
| 14 | + | CREATE INDEX IF NOT EXISTS idx_favorite_folders_position ON favorite_folders(position); | |||
| 15 | + | CREATE INDEX IF NOT EXISTS idx_favorite_folders_parent ON favorite_folders(parent_id); | |||
| 16 | + | ||||
| 17 | + | -- Tags for labeling favorites | |||
| 18 | + | CREATE TABLE IF NOT EXISTS favorite_tags ( | |||
| 19 | + | id INTEGER PRIMARY KEY AUTOINCREMENT, | |||
| 20 | + | name TEXT NOT NULL UNIQUE, | |||
| 21 | + | color TEXT NOT NULL DEFAULT '#808080', | |||
| 22 | + | created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP | |||
| 23 | + | ); | |||
| 24 | + | ||||
| 25 | + | -- Favorites table | |||
| 26 | + | CREATE TABLE IF NOT EXISTS favorites ( | |||
| 27 | + | id INTEGER PRIMARY KEY AUTOINCREMENT, | |||
| 28 | + | url TEXT NOT NULL UNIQUE, | |||
| 29 | + | title TEXT, | |||
| 30 | + | favicon_url TEXT, | |||
| 31 | + | folder_id INTEGER REFERENCES favorite_folders(id) ON DELETE SET NULL, | |||
| 32 | + | shortcut_key INTEGER CHECK (shortcut_key BETWEEN 1 AND 9), | |||
| 33 | + | position INTEGER NOT NULL DEFAULT 0, | |||
| 34 | + | created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, | |||
| 35 | + | updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP | |||
| 36 | + | ); | |||
| 37 | + | ||||
| 38 | + | CREATE INDEX IF NOT EXISTS idx_favorites_position ON favorites(position); | |||
| 39 | + | CREATE INDEX IF NOT EXISTS idx_favorites_url ON favorites(url); | |||
| 40 | + | CREATE INDEX IF NOT EXISTS idx_favorites_folder ON favorites(folder_id); | |||
| 41 | + | CREATE UNIQUE INDEX IF NOT EXISTS idx_favorites_shortcut ON favorites(shortcut_key) WHERE shortcut_key IS NOT NULL; | |||
| 42 | + | ||||
| 43 | + | -- Tag assignments (junction table) | |||
| 44 | + | CREATE TABLE IF NOT EXISTS favorite_tag_assignments ( | |||
| 45 | + | favorite_id INTEGER NOT NULL, | |||
| 46 | + | tag_id INTEGER NOT NULL, | |||
| 47 | + | PRIMARY KEY (favorite_id, tag_id), | |||
| 48 | + | FOREIGN KEY (favorite_id) REFERENCES favorites(id) ON DELETE CASCADE, | |||
| 49 | + | FOREIGN KEY (tag_id) REFERENCES favorite_tags(id) ON DELETE CASCADE | |||
| 50 | + | ); | |||
| 51 | + | ||||
| 52 | + | CREATE INDEX IF NOT EXISTS idx_fta_favorite ON favorite_tag_assignments(favorite_id); | |||
| 53 | + | CREATE INDEX IF NOT EXISTS idx_fta_tag ON favorite_tag_assignments(tag_id); | |||
| 54 | + | ||||
| 55 | + | -- +goose Down | |||
| 56 | + | DROP TABLE IF EXISTS favorite_tag_assignments; | |||
| 57 | + | DROP TABLE IF EXISTS favorites; | |||
| 58 | + | DROP TABLE IF EXISTS favorite_tags; | |||
| 59 | + | DROP TABLE IF EXISTS favorite_folders; | |||
A internal/infrastructure/persistence/sqlite/migrations/004_content_whitelist.sql +13 -0
| @@ -0,0 +1,13 @@ | |||
| 1 | + | -- +goose Up | |
| 2 | + | -- Content filter whitelist - domains that bypass content filtering | |
| 3 | + | ||
| 4 | + | CREATE TABLE IF NOT EXISTS content_whitelist ( | |
| 5 | + | id INTEGER PRIMARY KEY AUTOINCREMENT, | |
| 6 | + | domain TEXT NOT NULL UNIQUE, | |
| 7 | + | created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP | |
| 8 | + | ); | |
| 9 | + | ||
| 10 | + | CREATE INDEX IF NOT EXISTS idx_content_whitelist_domain ON content_whitelist(domain); | |
| 11 | + | ||
| 12 | + | -- +goose Down | |
| 13 | + | DROP TABLE IF EXISTS content_whitelist; | |
| @@ -0,0 +1,13 @@ | |||||
| 1 | + | -- +goose Up | |||
| 2 | + | -- Content filter whitelist - domains that bypass content filtering | |||
| 3 | + | ||||
| 4 | + | CREATE TABLE IF NOT EXISTS content_whitelist ( | |||
| 5 | + | id INTEGER PRIMARY KEY AUTOINCREMENT, | |||
| 6 | + | domain TEXT NOT NULL UNIQUE, | |||
| 7 | + | created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP | |||
| 8 | + | ); | |||
| 9 | + | ||||
| 10 | + | CREATE INDEX IF NOT EXISTS idx_content_whitelist_domain ON content_whitelist(domain); | |||
| 11 | + | ||||
| 12 | + | -- +goose Down | |||
| 13 | + | DROP TABLE IF EXISTS content_whitelist; | |||
A internal/infrastructure/persistence/sqlite/queries/favorites.sql +39 -0
| @@ -0,0 +1,39 @@ | |||
| 1 | + | -- name: GetAllFavorites :many | |
| 2 | + | SELECT * FROM favorites ORDER BY position ASC; | |
| 3 | + | ||
| 4 | + | -- name: GetFavoriteByID :one | |
| 5 | + | SELECT * FROM favorites WHERE id = ? LIMIT 1; | |
| 6 | + | ||
| 7 | + | -- name: GetFavoriteByURL :one | |
| 8 | + | SELECT * FROM favorites WHERE url = ? LIMIT 1; | |
| 9 | + | ||
| 10 | + | -- name: GetFavoritesByFolder :many | |
| 11 | + | SELECT * FROM favorites WHERE folder_id = ? ORDER BY position ASC; | |
| 12 | + | ||
| 13 | + | -- name: GetFavoritesWithoutFolder :many | |
| 14 | + | SELECT * FROM favorites WHERE folder_id IS NULL ORDER BY position ASC; | |
| 15 | + | ||
| 16 | + | -- name: GetFavoriteByShortcut :one | |
| 17 | + | SELECT * FROM favorites WHERE shortcut_key = ? LIMIT 1; | |
| 18 | + | ||
| 19 | + | -- name: CreateFavorite :one | |
| 20 | + | INSERT INTO favorites (url, title, favicon_url, folder_id, position, created_at, updated_at) | |
| 21 | + | VALUES (?, ?, ?, ?, COALESCE((SELECT MAX(position) + 1 FROM favorites), 0), CURRENT_TIMESTAMP, CURRENT_TIMESTAMP) | |
| 22 | + | RETURNING *; | |
| 23 | + | ||
| 24 | + | -- name: UpdateFavorite :exec | |
| 25 | + | UPDATE favorites | |
| 26 | + | SET title = ?, favicon_url = ?, updated_at = CURRENT_TIMESTAMP | |
| 27 | + | WHERE id = ?; | |
| 28 | + | ||
| 29 | + | -- name: UpdateFavoritePosition :exec | |
| 30 | + | UPDATE favorites SET position = ?, updated_at = CURRENT_TIMESTAMP WHERE id = ?; | |
| 31 | + | ||
| 32 | + | -- name: SetFavoriteFolder :exec | |
| 33 | + | UPDATE favorites SET folder_id = ?, updated_at = CURRENT_TIMESTAMP WHERE id = ?; | |
| 34 | + | ||
| 35 | + | -- name: SetFavoriteShortcut :exec | |
| 36 | + | UPDATE favorites SET shortcut_key = ?, updated_at = CURRENT_TIMESTAMP WHERE id = ?; | |
| 37 | + | ||
| 38 | + | -- name: DeleteFavorite :exec | |
| 39 | + | DELETE FROM favorites WHERE id = ?; | |
| @@ -0,0 +1,39 @@ | |||||
| 1 | + | -- name: GetAllFavorites :many | |||
| 2 | + | SELECT * FROM favorites ORDER BY position ASC; | |||
| 3 | + | ||||
| 4 | + | -- name: GetFavoriteByID :one | |||
| 5 | + | SELECT * FROM favorites WHERE id = ? LIMIT 1; | |||
| 6 | + | ||||
| 7 | + | -- name: GetFavoriteByURL :one | |||
| 8 | + | SELECT * FROM favorites WHERE url = ? LIMIT 1; | |||
| 9 | + | ||||
| 10 | + | -- name: GetFavoritesByFolder :many | |||
| 11 | + | SELECT * FROM favorites WHERE folder_id = ? ORDER BY position ASC; | |||
| 12 | + | ||||
| 13 | + | -- name: GetFavoritesWithoutFolder :many | |||
| 14 | + | SELECT * FROM favorites WHERE folder_id IS NULL ORDER BY position ASC; | |||
| 15 | + | ||||
| 16 | + | -- name: GetFavoriteByShortcut :one | |||
| 17 | + | SELECT * FROM favorites WHERE shortcut_key = ? LIMIT 1; | |||
| 18 | + | ||||
| 19 | + | -- name: CreateFavorite :one | |||
| 20 | + | INSERT INTO favorites (url, title, favicon_url, folder_id, position, created_at, updated_at) | |||
| 21 | + | VALUES (?, ?, ?, ?, COALESCE((SELECT MAX(position) + 1 FROM favorites), 0), CURRENT_TIMESTAMP, CURRENT_TIMESTAMP) | |||
| 22 | + | RETURNING *; | |||
| 23 | + | ||||
| 24 | + | -- name: UpdateFavorite :exec | |||
| 25 | + | UPDATE favorites | |||
| 26 | + | SET title = ?, favicon_url = ?, updated_at = CURRENT_TIMESTAMP | |||
| 27 | + | WHERE id = ?; | |||
| 28 | + | ||||
| 29 | + | -- name: UpdateFavoritePosition :exec | |||
| 30 | + | UPDATE favorites SET position = ?, updated_at = CURRENT_TIMESTAMP WHERE id = ?; | |||
| 31 | + | ||||
| 32 | + | -- name: SetFavoriteFolder :exec | |||
| 33 | + | UPDATE favorites SET folder_id = ?, updated_at = CURRENT_TIMESTAMP WHERE id = ?; | |||
| 34 | + | ||||
| 35 | + | -- name: SetFavoriteShortcut :exec | |||
| 36 | + | UPDATE favorites SET shortcut_key = ?, updated_at = CURRENT_TIMESTAMP WHERE id = ?; | |||
| 37 | + | ||||
| 38 | + | -- name: DeleteFavorite :exec | |||
| 39 | + | DELETE FROM favorites WHERE id = ?; | |||
A internal/infrastructure/persistence/sqlite/queries/folders.sql +25 -0
| @@ -0,0 +1,25 @@ | |||
| 1 | + | -- name: GetAllFolders :many | |
| 2 | + | SELECT * FROM favorite_folders ORDER BY position ASC; | |
| 3 | + | ||
| 4 | + | -- name: GetFolderByID :one | |
| 5 | + | SELECT * FROM favorite_folders WHERE id = ? LIMIT 1; | |
| 6 | + | ||
| 7 | + | -- name: GetChildFolders :many | |
| 8 | + | SELECT * FROM favorite_folders WHERE parent_id = ? ORDER BY position ASC; | |
| 9 | + | ||
| 10 | + | -- name: GetRootFolders :many | |
| 11 | + | SELECT * FROM favorite_folders WHERE parent_id IS NULL ORDER BY position ASC; | |
| 12 | + | ||
| 13 | + | -- name: CreateFolder :one | |
| 14 | + | INSERT INTO favorite_folders (name, icon, parent_id, position, created_at) | |
| 15 | + | VALUES (?, ?, ?, COALESCE((SELECT MAX(position) + 1 FROM favorite_folders), 0), CURRENT_TIMESTAMP) | |
| 16 | + | RETURNING *; | |
| 17 | + | ||
| 18 | + | -- name: UpdateFolder :exec | |
| 19 | + | UPDATE favorite_folders SET name = ?, icon = ? WHERE id = ?; | |
| 20 | + | ||
| 21 | + | -- name: UpdateFolderPosition :exec | |
| 22 | + | UPDATE favorite_folders SET position = ? WHERE id = ?; | |
| 23 | + | ||
| 24 | + | -- name: DeleteFolder :exec | |
| 25 | + | DELETE FROM favorite_folders WHERE id = ?; | |
| @@ -0,0 +1,25 @@ | |||||
| 1 | + | -- name: GetAllFolders :many | |||
| 2 | + | SELECT * FROM favorite_folders ORDER BY position ASC; | |||
| 3 | + | ||||
| 4 | + | -- name: GetFolderByID :one | |||
| 5 | + | SELECT * FROM favorite_folders WHERE id = ? LIMIT 1; | |||
| 6 | + | ||||
| 7 | + | -- name: GetChildFolders :many | |||
| 8 | + | SELECT * FROM favorite_folders WHERE parent_id = ? ORDER BY position ASC; | |||
| 9 | + | ||||
| 10 | + | -- name: GetRootFolders :many | |||
| 11 | + | SELECT * FROM favorite_folders WHERE parent_id IS NULL ORDER BY position ASC; | |||
| 12 | + | ||||
| 13 | + | -- name: CreateFolder :one | |||
| 14 | + | INSERT INTO favorite_folders (name, icon, parent_id, position, created_at) | |||
| 15 | + | VALUES (?, ?, ?, COALESCE((SELECT MAX(position) + 1 FROM favorite_folders), 0), CURRENT_TIMESTAMP) | |||
| 16 | + | RETURNING *; | |||
| 17 | + | ||||
| 18 | + | -- name: UpdateFolder :exec | |||
| 19 | + | UPDATE favorite_folders SET name = ?, icon = ? WHERE id = ?; | |||
| 20 | + | ||||
| 21 | + | -- name: UpdateFolderPosition :exec | |||
| 22 | + | UPDATE favorite_folders SET position = ? WHERE id = ?; | |||
| 23 | + | ||||
| 24 | + | -- name: DeleteFolder :exec | |||
| 25 | + | DELETE FROM favorite_folders WHERE id = ?; | |||
A internal/infrastructure/persistence/sqlite/queries/history.sql +35 -0
| @@ -0,0 +1,35 @@ | |||
| 1 | + | -- name: GetRecentHistory :many | |
| 2 | + | SELECT * FROM history ORDER BY last_visited DESC LIMIT ? OFFSET ?; | |
| 3 | + | ||
| 4 | + | -- name: GetHistoryByURL :one | |
| 5 | + | SELECT * FROM history WHERE url = ? LIMIT 1; | |
| 6 | + | ||
| 7 | + | -- name: UpsertHistory :exec | |
| 8 | + | INSERT INTO history (url, title, favicon_url) | |
| 9 | + | VALUES (?, ?, ?) | |
| 10 | + | ON CONFLICT(url) | |
| 11 | + | DO UPDATE SET | |
| 12 | + | visit_count = visit_count + 1, | |
| 13 | + | last_visited = CURRENT_TIMESTAMP, | |
| 14 | + | title = COALESCE(EXCLUDED.title, history.title), | |
| 15 | + | favicon_url = COALESCE(EXCLUDED.favicon_url, history.favicon_url); | |
| 16 | + | ||
| 17 | + | -- name: IncrementVisitCount :exec | |
| 18 | + | UPDATE history | |
| 19 | + | SET visit_count = visit_count + 1, last_visited = CURRENT_TIMESTAMP | |
| 20 | + | WHERE url = ?; | |
| 21 | + | ||
| 22 | + | -- name: SearchHistory :many | |
| 23 | + | SELECT * FROM history | |
| 24 | + | WHERE url LIKE '%' || ? || '%' OR title LIKE '%' || ? || '%' | |
| 25 | + | ORDER BY visit_count DESC, last_visited DESC | |
| 26 | + | LIMIT ?; | |
| 27 | + | ||
| 28 | + | -- name: DeleteHistoryByID :exec | |
| 29 | + | DELETE FROM history WHERE id = ?; | |
| 30 | + | ||
| 31 | + | -- name: DeleteHistoryOlderThan :exec | |
| 32 | + | DELETE FROM history WHERE last_visited < ?; | |
| 33 | + | ||
| 34 | + | -- name: DeleteAllHistory :exec | |
| 35 | + | DELETE FROM history; | |
| @@ -0,0 +1,35 @@ | |||||
| 1 | + | -- name: GetRecentHistory :many | |||
| 2 | + | SELECT * FROM history ORDER BY last_visited DESC LIMIT ? OFFSET ?; | |||
| 3 | + | ||||
| 4 | + | -- name: GetHistoryByURL :one | |||
| 5 | + | SELECT * FROM history WHERE url = ? LIMIT 1; | |||
| 6 | + | ||||
| 7 | + | -- name: UpsertHistory :exec | |||
| 8 | + | INSERT INTO history (url, title, favicon_url) | |||
| 9 | + | VALUES (?, ?, ?) | |||
| 10 | + | ON CONFLICT(url) | |||
| 11 | + | DO UPDATE SET | |||
| 12 | + | visit_count = visit_count + 1, | |||
| 13 | + | last_visited = CURRENT_TIMESTAMP, | |||
| 14 | + | title = COALESCE(EXCLUDED.title, history.title), | |||
| 15 | + | favicon_url = COALESCE(EXCLUDED.favicon_url, history.favicon_url); | |||
| 16 | + | ||||
| 17 | + | -- name: IncrementVisitCount :exec | |||
| 18 | + | UPDATE history | |||
| 19 | + | SET visit_count = visit_count + 1, last_visited = CURRENT_TIMESTAMP | |||
| 20 | + | WHERE url = ?; | |||
| 21 | + | ||||
| 22 | + | -- name: SearchHistory :many | |||
| 23 | + | SELECT * FROM history | |||
| 24 | + | WHERE url LIKE '%' || ? || '%' OR title LIKE '%' || ? || '%' | |||
| 25 | + | ORDER BY visit_count DESC, last_visited DESC | |||
| 26 | + | LIMIT ?; | |||
| 27 | + | ||||
| 28 | + | -- name: DeleteHistoryByID :exec | |||
| 29 | + | DELETE FROM history WHERE id = ?; | |||
| 30 | + | ||||
| 31 | + | -- name: DeleteHistoryOlderThan :exec | |||
| 32 | + | DELETE FROM history WHERE last_visited < ?; | |||
| 33 | + | ||||
| 34 | + | -- name: DeleteAllHistory :exec | |||
| 35 | + | DELETE FROM history; | |||
A internal/infrastructure/persistence/sqlite/queries/tags.sql +29 -0
| @@ -0,0 +1,29 @@ | |||
| 1 | + | -- name: GetAllTags :many | |
| 2 | + | SELECT * FROM favorite_tags ORDER BY name ASC; | |
| 3 | + | ||
| 4 | + | -- name: GetTagByID :one | |
| 5 | + | SELECT * FROM favorite_tags WHERE id = ? LIMIT 1; | |
| 6 | + | ||
| 7 | + | -- name: GetTagByName :one | |
| 8 | + | SELECT * FROM favorite_tags WHERE name = ? LIMIT 1; | |
| 9 | + | ||
| 10 | + | -- name: CreateTag :one | |
| 11 | + | INSERT INTO favorite_tags (name, color) VALUES (?, ?) RETURNING *; | |
| 12 | + | ||
| 13 | + | -- name: UpdateTag :exec | |
| 14 | + | UPDATE favorite_tags SET name = ?, color = ? WHERE id = ?; | |
| 15 | + | ||
| 16 | + | -- name: DeleteTag :exec | |
| 17 | + | DELETE FROM favorite_tags WHERE id = ?; | |
| 18 | + | ||
| 19 | + | -- name: AssignTagToFavorite :exec | |
| 20 | + | INSERT OR IGNORE INTO favorite_tag_assignments (favorite_id, tag_id) VALUES (?, ?); | |
| 21 | + | ||
| 22 | + | -- name: RemoveTagFromFavorite :exec | |
| 23 | + | DELETE FROM favorite_tag_assignments WHERE favorite_id = ? AND tag_id = ?; | |
| 24 | + | ||
| 25 | + | -- name: GetTagsForFavorite :many | |
| 26 | + | SELECT t.* FROM favorite_tags t | |
| 27 | + | INNER JOIN favorite_tag_assignments fta ON t.id = fta.tag_id | |
| 28 | + | WHERE fta.favorite_id = ? | |
| 29 | + | ORDER BY t.name ASC; | |
| @@ -0,0 +1,29 @@ | |||||
| 1 | + | -- name: GetAllTags :many | |||
| 2 | + | SELECT * FROM favorite_tags ORDER BY name ASC; | |||
| 3 | + | ||||
| 4 | + | -- name: GetTagByID :one | |||
| 5 | + | SELECT * FROM favorite_tags WHERE id = ? LIMIT 1; | |||
| 6 | + | ||||
| 7 | + | -- name: GetTagByName :one | |||
| 8 | + | SELECT * FROM favorite_tags WHERE name = ? LIMIT 1; | |||
| 9 | + | ||||
| 10 | + | -- name: CreateTag :one | |||
| 11 | + | INSERT INTO favorite_tags (name, color) VALUES (?, ?) RETURNING *; | |||
| 12 | + | ||||
| 13 | + | -- name: UpdateTag :exec | |||
| 14 | + | UPDATE favorite_tags SET name = ?, color = ? WHERE id = ?; | |||
| 15 | + | ||||
| 16 | + | -- name: DeleteTag :exec | |||
| 17 | + | DELETE FROM favorite_tags WHERE id = ?; | |||
| 18 | + | ||||
| 19 | + | -- name: AssignTagToFavorite :exec | |||
| 20 | + | INSERT OR IGNORE INTO favorite_tag_assignments (favorite_id, tag_id) VALUES (?, ?); | |||
| 21 | + | ||||
| 22 | + | -- name: RemoveTagFromFavorite :exec | |||
| 23 | + | DELETE FROM favorite_tag_assignments WHERE favorite_id = ? AND tag_id = ?; | |||
| 24 | + | ||||
| 25 | + | -- name: GetTagsForFavorite :many | |||
| 26 | + | SELECT t.* FROM favorite_tags t | |||
| 27 | + | INNER JOIN favorite_tag_assignments fta ON t.id = fta.tag_id | |||
| 28 | + | WHERE fta.favorite_id = ? | |||
| 29 | + | ORDER BY t.name ASC; | |||
A internal/infrastructure/persistence/sqlite/queries/whitelist.sql +11 -0
| @@ -0,0 +1,11 @@ | |||
| 1 | + | -- name: GetAllWhitelistedDomains :many | |
| 2 | + | SELECT domain FROM content_whitelist ORDER BY domain; | |
| 3 | + | ||
| 4 | + | -- name: AddToWhitelist :exec | |
| 5 | + | INSERT OR IGNORE INTO content_whitelist (domain, created_at) VALUES (?, CURRENT_TIMESTAMP); | |
| 6 | + | ||
| 7 | + | -- name: RemoveFromWhitelist :exec | |
| 8 | + | DELETE FROM content_whitelist WHERE domain = ?; | |
| 9 | + | ||
| 10 | + | -- name: IsWhitelisted :one | |
| 11 | + | SELECT COUNT(*) FROM content_whitelist WHERE domain = ? LIMIT 1; | |
| @@ -0,0 +1,11 @@ | |||||
| 1 | + | -- name: GetAllWhitelistedDomains :many | |||
| 2 | + | SELECT domain FROM content_whitelist ORDER BY domain; | |||
| 3 | + | ||||
| 4 | + | -- name: AddToWhitelist :exec | |||
| 5 | + | INSERT OR IGNORE INTO content_whitelist (domain, created_at) VALUES (?, CURRENT_TIMESTAMP); | |||
| 6 | + | ||||
| 7 | + | -- name: RemoveFromWhitelist :exec | |||
| 8 | + | DELETE FROM content_whitelist WHERE domain = ?; | |||
| 9 | + | ||||
| 10 | + | -- name: IsWhitelisted :one | |||
| 11 | + | SELECT COUNT(*) FROM content_whitelist WHERE domain = ? LIMIT 1; | |||
A internal/infrastructure/persistence/sqlite/queries/zoom.sql +15 -0
| @@ -0,0 +1,15 @@ | |||
| 1 | + | -- name: GetZoomLevel :one | |
| 2 | + | SELECT * FROM zoom_levels WHERE domain = ? LIMIT 1; | |
| 3 | + | ||
| 4 | + | -- name: SetZoomLevel :exec | |
| 5 | + | INSERT INTO zoom_levels (domain, zoom_factor, updated_at) | |
| 6 | + | VALUES (?, ?, CURRENT_TIMESTAMP) | |
| 7 | + | ON CONFLICT(domain) DO UPDATE SET | |
| 8 | + | zoom_factor = excluded.zoom_factor, | |
| 9 | + | updated_at = CURRENT_TIMESTAMP; | |
| 10 | + | ||
| 11 | + | -- name: DeleteZoomLevel :exec | |
| 12 | + | DELETE FROM zoom_levels WHERE domain = ?; | |
| 13 | + | ||
| 14 | + | -- name: ListZoomLevels :many | |
| 15 | + | SELECT * FROM zoom_levels ORDER BY updated_at DESC; | |
| @@ -0,0 +1,15 @@ | |||||
| 1 | + | -- name: GetZoomLevel :one | |||
| 2 | + | SELECT * FROM zoom_levels WHERE domain = ? LIMIT 1; | |||
| 3 | + | ||||
| 4 | + | -- name: SetZoomLevel :exec | |||
| 5 | + | INSERT INTO zoom_levels (domain, zoom_factor, updated_at) | |||
| 6 | + | VALUES (?, ?, CURRENT_TIMESTAMP) | |||
| 7 | + | ON CONFLICT(domain) DO UPDATE SET | |||
| 8 | + | zoom_factor = excluded.zoom_factor, | |||
| 9 | + | updated_at = CURRENT_TIMESTAMP; | |||
| 10 | + | ||||
| 11 | + | -- name: DeleteZoomLevel :exec | |||
| 12 | + | DELETE FROM zoom_levels WHERE domain = ?; | |||
| 13 | + | ||||
| 14 | + | -- name: ListZoomLevels :many | |||
| 15 | + | SELECT * FROM zoom_levels ORDER BY updated_at DESC; | |||
A internal/infrastructure/persistence/sqlite/sqlc/db.go +31 -0
| @@ -0,0 +1,31 @@ | |||
| 1 | + | // Code generated by sqlc. DO NOT EDIT. | |
| 2 | + | // versions: | |
| 3 | + | // sqlc v1.30.0 | |
| 4 | + | ||
| 5 | + | package sqlc | |
| 6 | + | ||
| 7 | + | import ( | |
| 8 | + | "context" | |
| 9 | + | "database/sql" | |
| 10 | + | ) | |
| 11 | + | ||
| 12 | + | type DBTX interface { | |
| 13 | + | ExecContext(context.Context, string, ...interface{}) (sql.Result, error) | |
| 14 | + | PrepareContext(context.Context, string) (*sql.Stmt, error) | |
| 15 | + | QueryContext(context.Context, string, ...interface{}) (*sql.Rows, error) | |
| 16 | + | QueryRowContext(context.Context, string, ...interface{}) *sql.Row | |
| 17 | + | } | |
| 18 | + | ||
| 19 | + | func New(db DBTX) *Queries { | |
| 20 | + | return &Queries{db: db} | |
| 21 | + | } | |
| 22 | + | ||
| 23 | + | type Queries struct { | |
| 24 | + | db DBTX | |
| 25 | + | } | |
| 26 | + | ||
| 27 | + | func (q *Queries) WithTx(tx *sql.Tx) *Queries { | |
| 28 | + | return &Queries{ | |
| 29 | + | db: tx, | |
| 30 | + | } | |
| 31 | + | } | |
| @@ -0,0 +1,31 @@ | |||||
| 1 | + | // Code generated by sqlc. DO NOT EDIT. | |||
| 2 | + | // versions: | |||
| 3 | + | // sqlc v1.30.0 | |||
| 4 | + | ||||
| 5 | + | package sqlc | |||
| 6 | + | ||||
| 7 | + | import ( | |||
| 8 | + | "context" | |||
| 9 | + | "database/sql" | |||
| 10 | + | ) | |||
| 11 | + | ||||
| 12 | + | type DBTX interface { | |||
| 13 | + | ExecContext(context.Context, string, ...interface{}) (sql.Result, error) | |||
| 14 | + | PrepareContext(context.Context, string) (*sql.Stmt, error) | |||
| 15 | + | QueryContext(context.Context, string, ...interface{}) (*sql.Rows, error) | |||
| 16 | + | QueryRowContext(context.Context, string, ...interface{}) *sql.Row | |||
| 17 | + | } | |||
| 18 | + | ||||
| 19 | + | func New(db DBTX) *Queries { | |||
| 20 | + | return &Queries{db: db} | |||
| 21 | + | } | |||
| 22 | + | ||||
| 23 | + | type Queries struct { | |||
| 24 | + | db DBTX | |||
| 25 | + | } | |||
| 26 | + | ||||
| 27 | + | func (q *Queries) WithTx(tx *sql.Tx) *Queries { | |||
| 28 | + | return &Queries{ | |||
| 29 | + | db: tx, | |||
| 30 | + | } | |||
| 31 | + | } | |||
A internal/infrastructure/persistence/sqlite/sqlc/favorites.sql.go +288 -0
| @@ -0,0 +1,288 @@ | |||
| 1 | + | // Code generated by sqlc. DO NOT EDIT. | |
| 2 | + | // versions: | |
| 3 | + | // sqlc v1.30.0 | |
| 4 | + | // source: favorites.sql | |
| 5 | + | ||
| 6 | + | package sqlc | |
| 7 | + | ||
| 8 | + | import ( | |
| 9 | + | "context" | |
| 10 | + | "database/sql" | |
| 11 | + | ) | |
| 12 | + | ||
| 13 | + | const CreateFavorite = `-- name: CreateFavorite :one | |
| 14 | + | INSERT INTO favorites (url, title, favicon_url, folder_id, position, created_at, updated_at) | |
| 15 | + | VALUES (?, ?, ?, ?, COALESCE((SELECT MAX(position) + 1 FROM favorites), 0), CURRENT_TIMESTAMP, CURRENT_TIMESTAMP) | |
| 16 | + | RETURNING id, url, title, favicon_url, folder_id, shortcut_key, position, created_at, updated_at | |
| 17 | + | ` | |
| 18 | + | ||
| 19 | + | type CreateFavoriteParams struct { | |
| 20 | + | Url string `json:"url"` | |
| 21 | + | Title sql.NullString `json:"title"` | |
| 22 | + | FaviconUrl sql.NullString `json:"favicon_url"` | |
| 23 | + | FolderID sql.NullInt64 `json:"folder_id"` | |
| 24 | + | } | |
| 25 | + | ||
| 26 | + | func (q *Queries) CreateFavorite(ctx context.Context, arg CreateFavoriteParams) (Favorite, error) { | |
| 27 | + | row := q.db.QueryRowContext(ctx, CreateFavorite, | |
| 28 | + | arg.Url, | |
| 29 | + | arg.Title, | |
| 30 | + | arg.FaviconUrl, | |
| 31 | + | arg.FolderID, | |
| 32 | + | ) | |
| 33 | + | var i Favorite | |
| 34 | + | err := row.Scan( | |
| 35 | + | &i.ID, | |
| 36 | + | &i.Url, | |
| 37 | + | &i.Title, | |
| 38 | + | &i.FaviconUrl, | |
| 39 | + | &i.FolderID, | |
| 40 | + | &i.ShortcutKey, | |
| 41 | + | &i.Position, | |
| 42 | + | &i.CreatedAt, | |
| 43 | + | &i.UpdatedAt, | |
| 44 | + | ) | |
| 45 | + | return i, err | |
| 46 | + | } | |
| 47 | + | ||
| 48 | + | const DeleteFavorite = `-- name: DeleteFavorite :exec | |
| 49 | + | DELETE FROM favorites WHERE id = ? | |
| 50 | + | ` | |
| 51 | + | ||
| 52 | + | func (q *Queries) DeleteFavorite(ctx context.Context, id int64) error { | |
| 53 | + | _, err := q.db.ExecContext(ctx, DeleteFavorite, id) | |
| 54 | + | return err | |
| 55 | + | } | |
| 56 | + | ||
| 57 | + | const GetAllFavorites = `-- name: GetAllFavorites :many | |
| 58 | + | SELECT id, url, title, favicon_url, folder_id, shortcut_key, position, created_at, updated_at FROM favorites ORDER BY position ASC | |
| 59 | + | ` | |
| 60 | + | ||
| 61 | + | func (q *Queries) GetAllFavorites(ctx context.Context) ([]Favorite, error) { | |
| 62 | + | rows, err := q.db.QueryContext(ctx, GetAllFavorites) | |
| 63 | + | if err != nil { | |
| 64 | + | return nil, err | |
| 65 | + | } | |
| 66 | + | defer rows.Close() | |
| 67 | + | items := []Favorite{} | |
| 68 | + | for rows.Next() { | |
| 69 | + | var i Favorite | |
| 70 | + | if err := rows.Scan( | |
| 71 | + | &i.ID, | |
| 72 | + | &i.Url, | |
| 73 | + | &i.Title, | |
| 74 | + | &i.FaviconUrl, | |
| 75 | + | &i.FolderID, | |
| 76 | + | &i.ShortcutKey, | |
| 77 | + | &i.Position, | |
| 78 | + | &i.CreatedAt, | |
| 79 | + | &i.UpdatedAt, | |
| 80 | + | ); err != nil { | |
| 81 | + | return nil, err | |
| 82 | + | } | |
| 83 | + | items = append(items, i) | |
| 84 | + | } | |
| 85 | + | if err := rows.Close(); err != nil { | |
| 86 | + | return nil, err | |
| 87 | + | } | |
| 88 | + | if err := rows.Err(); err != nil { | |
| 89 | + | return nil, err | |
| 90 | + | } | |
| 91 | + | return items, nil | |
| 92 | + | } | |
| 93 | + | ||
| 94 | + | const GetFavoriteByID = `-- name: GetFavoriteByID :one | |
| 95 | + | SELECT id, url, title, favicon_url, folder_id, shortcut_key, position, created_at, updated_at FROM favorites WHERE id = ? LIMIT 1 | |
| 96 | + | ` | |
| 97 | + | ||
| 98 | + | func (q *Queries) GetFavoriteByID(ctx context.Context, id int64) (Favorite, error) { | |
| 99 | + | row := q.db.QueryRowContext(ctx, GetFavoriteByID, id) | |
| 100 | + | var i Favorite | |
| 101 | + | err := row.Scan( | |
| 102 | + | &i.ID, | |
| 103 | + | &i.Url, | |
| 104 | + | &i.Title, | |
| 105 | + | &i.FaviconUrl, | |
| 106 | + | &i.FolderID, | |
| 107 | + | &i.ShortcutKey, | |
| 108 | + | &i.Position, | |
| 109 | + | &i.CreatedAt, | |
| 110 | + | &i.UpdatedAt, | |
| 111 | + | ) | |
| 112 | + | return i, err | |
| 113 | + | } | |
| 114 | + | ||
| 115 | + | const GetFavoriteByShortcut = `-- name: GetFavoriteByShortcut :one | |
| 116 | + | SELECT id, url, title, favicon_url, folder_id, shortcut_key, position, created_at, updated_at FROM favorites WHERE shortcut_key = ? LIMIT 1 | |
| 117 | + | ` | |
| 118 | + | ||
| 119 | + | func (q *Queries) GetFavoriteByShortcut(ctx context.Context, shortcutKey sql.NullInt64) (Favorite, error) { | |
| 120 | + | row := q.db.QueryRowContext(ctx, GetFavoriteByShortcut, shortcutKey) | |
| 121 | + | var i Favorite | |
| 122 | + | err := row.Scan( | |
| 123 | + | &i.ID, | |
| 124 | + | &i.Url, | |
| 125 | + | &i.Title, | |
| 126 | + | &i.FaviconUrl, | |
| 127 | + | &i.FolderID, | |
| 128 | + | &i.ShortcutKey, | |
| 129 | + | &i.Position, | |
| 130 | + | &i.CreatedAt, | |
| 131 | + | &i.UpdatedAt, | |
| 132 | + | ) | |
| 133 | + | return i, err | |
| 134 | + | } | |
| 135 | + | ||
| 136 | + | const GetFavoriteByURL = `-- name: GetFavoriteByURL :one | |
| 137 | + | SELECT id, url, title, favicon_url, folder_id, shortcut_key, position, created_at, updated_at FROM favorites WHERE url = ? LIMIT 1 | |
| 138 | + | ` | |
| 139 | + | ||
| 140 | + | func (q *Queries) GetFavoriteByURL(ctx context.Context, url string) (Favorite, error) { | |
| 141 | + | row := q.db.QueryRowContext(ctx, GetFavoriteByURL, url) | |
| 142 | + | var i Favorite | |
| 143 | + | err := row.Scan( | |
| 144 | + | &i.ID, | |
| 145 | + | &i.Url, | |
| 146 | + | &i.Title, | |
| 147 | + | &i.FaviconUrl, | |
| 148 | + | &i.FolderID, | |
| 149 | + | &i.ShortcutKey, | |
| 150 | + | &i.Position, | |
| 151 | + | &i.CreatedAt, | |
| 152 | + | &i.UpdatedAt, | |
| 153 | + | ) | |
| 154 | + | return i, err | |
| 155 | + | } | |
| 156 | + | ||
| 157 | + | const GetFavoritesByFolder = `-- name: GetFavoritesByFolder :many | |
| 158 | + | SELECT id, url, title, favicon_url, folder_id, shortcut_key, position, created_at, updated_at FROM favorites WHERE folder_id = ? ORDER BY position ASC | |
| 159 | + | ` | |
| 160 | + | ||
| 161 | + | func (q *Queries) GetFavoritesByFolder(ctx context.Context, folderID sql.NullInt64) ([]Favorite, error) { | |
| 162 | + | rows, err := q.db.QueryContext(ctx, GetFavoritesByFolder, folderID) | |
| 163 | + | if err != nil { | |
| 164 | + | return nil, err | |
| 165 | + | } | |
| 166 | + | defer rows.Close() | |
| 167 | + | items := []Favorite{} | |
| 168 | + | for rows.Next() { | |
| 169 | + | var i Favorite | |
| 170 | + | if err := rows.Scan( | |
| 171 | + | &i.ID, | |
| 172 | + | &i.Url, | |
| 173 | + | &i.Title, | |
| 174 | + | &i.FaviconUrl, | |
| 175 | + | &i.FolderID, | |
| 176 | + | &i.ShortcutKey, | |
| 177 | + | &i.Position, | |
| 178 | + | &i.CreatedAt, | |
| 179 | + | &i.UpdatedAt, | |
| 180 | + | ); err != nil { | |
| 181 | + | return nil, err | |
| 182 | + | } | |
| 183 | + | items = append(items, i) | |
| 184 | + | } | |
| 185 | + | if err := rows.Close(); err != nil { | |
| 186 | + | return nil, err | |
| 187 | + | } | |
| 188 | + | if err := rows.Err(); err != nil { | |
| 189 | + | return nil, err | |
| 190 | + | } | |
| 191 | + | return items, nil | |
| 192 | + | } | |
| 193 | + | ||
| 194 | + | const GetFavoritesWithoutFolder = `-- name: GetFavoritesWithoutFolder :many | |
| 195 | + | SELECT id, url, title, favicon_url, folder_id, shortcut_key, position, created_at, updated_at FROM favorites WHERE folder_id IS NULL ORDER BY position ASC | |
| 196 | + | ` | |
| 197 | + | ||
| 198 | + | func (q *Queries) GetFavoritesWithoutFolder(ctx context.Context) ([]Favorite, error) { | |
| 199 | + | rows, err := q.db.QueryContext(ctx, GetFavoritesWithoutFolder) | |
| 200 | + | if err != nil { | |
| 201 | + | return nil, err | |
| 202 | + | } | |
| 203 | + | defer rows.Close() | |
| 204 | + | items := []Favorite{} | |
| 205 | + | for rows.Next() { | |
| 206 | + | var i Favorite | |
| 207 | + | if err := rows.Scan( | |
| 208 | + | &i.ID, | |
| 209 | + | &i.Url, | |
| 210 | + | &i.Title, | |
| 211 | + | &i.FaviconUrl, | |
| 212 | + | &i.FolderID, | |
| 213 | + | &i.ShortcutKey, | |
| 214 | + | &i.Position, | |
| 215 | + | &i.CreatedAt, | |
| 216 | + | &i.UpdatedAt, | |
| 217 | + | ); err != nil { | |
| 218 | + | return nil, err | |
| 219 | + | } | |
| 220 | + | items = append(items, i) | |
| 221 | + | } | |
| 222 | + | if err := rows.Close(); err != nil { | |
| 223 | + | return nil, err | |
| 224 | + | } | |
| 225 | + | if err := rows.Err(); err != nil { | |
| 226 | + | return nil, err | |
| 227 | + | } | |
| 228 | + | return items, nil | |
| 229 | + | } | |
| 230 | + | ||
| 231 | + | const SetFavoriteFolder = `-- name: SetFavoriteFolder :exec | |
| 232 | + | UPDATE favorites SET folder_id = ?, updated_at = CURRENT_TIMESTAMP WHERE id = ? | |
| 233 | + | ` | |
| 234 | + | ||
| 235 | + | type SetFavoriteFolderParams struct { | |
| 236 | + | FolderID sql.NullInt64 `json:"folder_id"` | |
| 237 | + | ID int64 `json:"id"` | |
| 238 | + | } | |
| 239 | + | ||
| 240 | + | func (q *Queries) SetFavoriteFolder(ctx context.Context, arg SetFavoriteFolderParams) error { | |
| 241 | + | _, err := q.db.ExecContext(ctx, SetFavoriteFolder, arg.FolderID, arg.ID) | |
| 242 | + | return err | |
| 243 | + | } | |
| 244 | + | ||
| 245 | + | const SetFavoriteShortcut = `-- name: SetFavoriteShortcut :exec | |
| 246 | + | UPDATE favorites SET shortcut_key = ?, updated_at = CURRENT_TIMESTAMP WHERE id = ? | |
| 247 | + | ` | |
| 248 | + | ||
| 249 | + | type SetFavoriteShortcutParams struct { | |
| 250 | + | ShortcutKey sql.NullInt64 `json:"shortcut_key"` | |
| 251 | + | ID int64 `json:"id"` | |
| 252 | + | } | |
| 253 | + | ||
| 254 | + | func (q *Queries) SetFavoriteShortcut(ctx context.Context, arg SetFavoriteShortcutParams) error { | |
| 255 | + | _, err := q.db.ExecContext(ctx, SetFavoriteShortcut, arg.ShortcutKey, arg.ID) | |
| 256 | + | return err | |
| 257 | + | } | |
| 258 | + | ||
| 259 | + | const UpdateFavorite = `-- name: UpdateFavorite :exec | |
| 260 | + | UPDATE favorites | |
| 261 | + | SET title = ?, favicon_url = ?, updated_at = CURRENT_TIMESTAMP | |
| 262 | + | WHERE id = ? | |
| 263 | + | ` | |
| 264 | + | ||
| 265 | + | type UpdateFavoriteParams struct { | |
| 266 | + | Title sql.NullString `json:"title"` | |
| 267 | + | FaviconUrl sql.NullString `json:"favicon_url"` | |
| 268 | + | ID int64 `json:"id"` | |
| 269 | + | } | |
| 270 | + | ||
| 271 | + | func (q *Queries) UpdateFavorite(ctx context.Context, arg UpdateFavoriteParams) error { | |
| 272 | + | _, err := q.db.ExecContext(ctx, UpdateFavorite, arg.Title, arg.FaviconUrl, arg.ID) | |
| 273 | + | return err | |
| 274 | + | } | |
| 275 | + | ||
| 276 | + | const UpdateFavoritePosition = `-- name: UpdateFavoritePosition :exec | |
| 277 | + | UPDATE favorites SET position = ?, updated_at = CURRENT_TIMESTAMP WHERE id = ? | |
| 278 | + | ` | |
| 279 | + | ||
| 280 | + | type UpdateFavoritePositionParams struct { | |
| 281 | + | Position int64 `json:"position"` | |
| 282 | + | ID int64 `json:"id"` | |
| 283 | + | } | |
| 284 | + | ||
| 285 | + | func (q *Queries) UpdateFavoritePosition(ctx context.Context, arg UpdateFavoritePositionParams) error { | |
| 286 | + | _, err := q.db.ExecContext(ctx, UpdateFavoritePosition, arg.Position, arg.ID) | |
| 287 | + | return err | |
| 288 | + | } | |
| @@ -0,0 +1,288 @@ | |||||
| 1 | + | // Code generated by sqlc. DO NOT EDIT. | |||
| 2 | + | // versions: | |||
| 3 | + | // sqlc v1.30.0 | |||
| 4 | + | // source: favorites.sql | |||
| 5 | + | ||||
| 6 | + | package sqlc | |||
| 7 | + | ||||
| 8 | + | import ( | |||
| 9 | + | "context" | |||
| 10 | + | "database/sql" | |||
| 11 | + | ) | |||
| 12 | + | ||||
| 13 | + | const CreateFavorite = `-- name: CreateFavorite :one | |||
| 14 | + | INSERT INTO favorites (url, title, favicon_url, folder_id, position, created_at, updated_at) | |||
| 15 | + | VALUES (?, ?, ?, ?, COALESCE((SELECT MAX(position) + 1 FROM favorites), 0), CURRENT_TIMESTAMP, CURRENT_TIMESTAMP) | |||
| 16 | + | RETURNING id, url, title, favicon_url, folder_id, shortcut_key, position, created_at, updated_at | |||
| 17 | + | ` | |||
| 18 | + | ||||
| 19 | + | type CreateFavoriteParams struct { | |||
| 20 | + | Url string `json:"url"` | |||
| 21 | + | Title sql.NullString `json:"title"` | |||
| 22 | + | FaviconUrl sql.NullString `json:"favicon_url"` | |||
| 23 | + | FolderID sql.NullInt64 `json:"folder_id"` | |||
| 24 | + | } | |||
| 25 | + | ||||
| 26 | + | func (q *Queries) CreateFavorite(ctx context.Context, arg CreateFavoriteParams) (Favorite, error) { | |||
| 27 | + | row := q.db.QueryRowContext(ctx, CreateFavorite, | |||
| 28 | + | arg.Url, | |||
| 29 | + | arg.Title, | |||
| 30 | + | arg.FaviconUrl, | |||
| 31 | + | arg.FolderID, | |||
| 32 | + | ) | |||
| 33 | + | var i Favorite | |||
| 34 | + | err := row.Scan( | |||
| 35 | + | &i.ID, | |||
| 36 | + | &i.Url, | |||
| 37 | + | &i.Title, | |||
| 38 | + | &i.FaviconUrl, | |||
| 39 | + | &i.FolderID, | |||
| 40 | + | &i.ShortcutKey, | |||
| 41 | + | &i.Position, | |||
| 42 | + | &i.CreatedAt, | |||
| 43 | + | &i.UpdatedAt, | |||
| 44 | + | ) | |||
| 45 | + | return i, err | |||
| 46 | + | } | |||
| 47 | + | ||||
| 48 | + | const DeleteFavorite = `-- name: DeleteFavorite :exec | |||
| 49 | + | DELETE FROM favorites WHERE id = ? | |||
| 50 | + | ` | |||
| 51 | + | ||||
| 52 | + | func (q *Queries) DeleteFavorite(ctx context.Context, id int64) error { | |||
| 53 | + | _, err := q.db.ExecContext(ctx, DeleteFavorite, id) | |||
| 54 | + | return err | |||
| 55 | + | } | |||
| 56 | + | ||||
| 57 | + | const GetAllFavorites = `-- name: GetAllFavorites :many | |||
| 58 | + | SELECT id, url, title, favicon_url, folder_id, shortcut_key, position, created_at, updated_at FROM favorites ORDER BY position ASC | |||
| 59 | + | ` | |||
| 60 | + | ||||
| 61 | + | func (q *Queries) GetAllFavorites(ctx context.Context) ([]Favorite, error) { | |||
| 62 | + | rows, err := q.db.QueryContext(ctx, GetAllFavorites) | |||
| 63 | + | if err != nil { | |||
| 64 | + | return nil, err | |||
| 65 | + | } | |||
| 66 | + | defer rows.Close() | |||
| 67 | + | items := []Favorite{} | |||
| 68 | + | for rows.Next() { | |||
| 69 | + | var i Favorite | |||
| 70 | + | if err := rows.Scan( | |||
| 71 | + | &i.ID, | |||
| 72 | + | &i.Url, | |||
| 73 | + | &i.Title, | |||
| 74 | + | &i.FaviconUrl, | |||
| 75 | + | &i.FolderID, | |||
| 76 | + | &i.ShortcutKey, | |||
| 77 | + | &i.Position, | |||
| 78 | + | &i.CreatedAt, | |||
| 79 | + | &i.UpdatedAt, | |||
| 80 | + | ); err != nil { | |||
| 81 | + | return nil, err | |||
| 82 | + | } | |||
| 83 | + | items = append(items, i) | |||
| 84 | + | } | |||
| 85 | + | if err := rows.Close(); err != nil { | |||
| 86 | + | return nil, err | |||
| 87 | + | } | |||
| 88 | + | if err := rows.Err(); err != nil { | |||
| 89 | + | return nil, err | |||
| 90 | + | } | |||
| 91 | + | return items, nil | |||
| 92 | + | } | |||
| 93 | + | ||||
| 94 | + | const GetFavoriteByID = `-- name: GetFavoriteByID :one | |||
| 95 | + | SELECT id, url, title, favicon_url, folder_id, shortcut_key, position, created_at, updated_at FROM favorites WHERE id = ? LIMIT 1 | |||
| 96 | + | ` | |||
| 97 | + | ||||
| 98 | + | func (q *Queries) GetFavoriteByID(ctx context.Context, id int64) (Favorite, error) { | |||
| 99 | + | row := q.db.QueryRowContext(ctx, GetFavoriteByID, id) | |||
| 100 | + | var i Favorite | |||
| 101 | + | err := row.Scan( | |||
| 102 | + | &i.ID, | |||
| 103 | + | &i.Url, | |||
| 104 | + | &i.Title, | |||
| 105 | + | &i.FaviconUrl, | |||
| 106 | + | &i.FolderID, | |||
| 107 | + | &i.ShortcutKey, | |||
| 108 | + | &i.Position, | |||
| 109 | + | &i.CreatedAt, | |||
| 110 | + | &i.UpdatedAt, | |||
| 111 | + | ) | |||
| 112 | + | return i, err | |||
| 113 | + | } | |||
| 114 | + | ||||
| 115 | + | const GetFavoriteByShortcut = `-- name: GetFavoriteByShortcut :one | |||
| 116 | + | SELECT id, url, title, favicon_url, folder_id, shortcut_key, position, created_at, updated_at FROM favorites WHERE shortcut_key = ? LIMIT 1 | |||
| 117 | + | ` | |||
| 118 | + | ||||
| 119 | + | func (q *Queries) GetFavoriteByShortcut(ctx context.Context, shortcutKey sql.NullInt64) (Favorite, error) { | |||
| 120 | + | row := q.db.QueryRowContext(ctx, GetFavoriteByShortcut, shortcutKey) | |||
| 121 | + | var i Favorite | |||
| 122 | + | err := row.Scan( | |||
| 123 | + | &i.ID, | |||
| 124 | + | &i.Url, | |||
| 125 | + | &i.Title, | |||
| 126 | + | &i.FaviconUrl, | |||
| 127 | + | &i.FolderID, | |||
| 128 | + | &i.ShortcutKey, | |||
| 129 | + | &i.Position, | |||
| 130 | + | &i.CreatedAt, | |||
| 131 | + | &i.UpdatedAt, | |||
| 132 | + | ) | |||
| 133 | + | return i, err | |||
| 134 | + | } | |||
| 135 | + | ||||
| 136 | + | const GetFavoriteByURL = `-- name: GetFavoriteByURL :one | |||
| 137 | + | SELECT id, url, title, favicon_url, folder_id, shortcut_key, position, created_at, updated_at FROM favorites WHERE url = ? LIMIT 1 | |||
| 138 | + | ` | |||
| 139 | + | ||||
| 140 | + | func (q *Queries) GetFavoriteByURL(ctx context.Context, url string) (Favorite, error) { | |||
| 141 | + | row := q.db.QueryRowContext(ctx, GetFavoriteByURL, url) | |||
| 142 | + | var i Favorite | |||
| 143 | + | err := row.Scan( | |||
| 144 | + | &i.ID, | |||
| 145 | + | &i.Url, | |||
| 146 | + | &i.Title, | |||
| 147 | + | &i.FaviconUrl, | |||
| 148 | + | &i.FolderID, | |||
| 149 | + | &i.ShortcutKey, | |||
| 150 | + | &i.Position, | |||
| 151 | + | &i.CreatedAt, | |||
| 152 | + | &i.UpdatedAt, | |||
| 153 | + | ) | |||
| 154 | + | return i, err | |||
| 155 | + | } | |||
| 156 | + | ||||
| 157 | + | const GetFavoritesByFolder = `-- name: GetFavoritesByFolder :many | |||
| 158 | + | SELECT id, url, title, favicon_url, folder_id, shortcut_key, position, created_at, updated_at FROM favorites WHERE folder_id = ? ORDER BY position ASC | |||
| 159 | + | ` | |||
| 160 | + | ||||
| 161 | + | func (q *Queries) GetFavoritesByFolder(ctx context.Context, folderID sql.NullInt64) ([]Favorite, error) { | |||
| 162 | + | rows, err := q.db.QueryContext(ctx, GetFavoritesByFolder, folderID) | |||
| 163 | + | if err != nil { | |||
| 164 | + | return nil, err | |||
| 165 | + | } | |||
| 166 | + | defer rows.Close() | |||
| 167 | + | items := []Favorite{} | |||
| 168 | + | for rows.Next() { | |||
| 169 | + | var i Favorite | |||
| 170 | + | if err := rows.Scan( | |||
| 171 | + | &i.ID, | |||
| 172 | + | &i.Url, | |||
| 173 | + | &i.Title, | |||
| 174 | + | &i.FaviconUrl, | |||
| 175 | + | &i.FolderID, | |||
| 176 | + | &i.ShortcutKey, | |||
| 177 | + | &i.Position, | |||
| 178 | + | &i.CreatedAt, | |||
| 179 | + | &i.UpdatedAt, | |||
| 180 | + | ); err != nil { | |||
| 181 | + | return nil, err | |||
| 182 | + | } | |||
| 183 | + | items = append(items, i) | |||
| 184 | + | } | |||
| 185 | + | if err := rows.Close(); err != nil { | |||
| 186 | + | return nil, err | |||
| 187 | + | } | |||
| 188 | + | if err := rows.Err(); err != nil { | |||
| 189 | + | return nil, err | |||
| 190 | + | } | |||
| 191 | + | return items, nil | |||
| 192 | + | } | |||
| 193 | + | ||||
| 194 | + | const GetFavoritesWithoutFolder = `-- name: GetFavoritesWithoutFolder :many | |||
| 195 | + | SELECT id, url, title, favicon_url, folder_id, shortcut_key, position, created_at, updated_at FROM favorites WHERE folder_id IS NULL ORDER BY position ASC | |||
| 196 | + | ` | |||
| 197 | + | ||||
| 198 | + | func (q *Queries) GetFavoritesWithoutFolder(ctx context.Context) ([]Favorite, error) { | |||
| 199 | + | rows, err := q.db.QueryContext(ctx, GetFavoritesWithoutFolder) | |||
| 200 | + | if err != nil { | |||
| 201 | + | return nil, err | |||
| 202 | + | } | |||
| 203 | + | defer rows.Close() | |||
| 204 | + | items := []Favorite{} | |||
| 205 | + | for rows.Next() { | |||
| 206 | + | var i Favorite | |||
| 207 | + | if err := rows.Scan( | |||
| 208 | + | &i.ID, | |||
| 209 | + | &i.Url, | |||
| 210 | + | &i.Title, | |||
| 211 | + | &i.FaviconUrl, | |||
| 212 | + | &i.FolderID, | |||
| 213 | + | &i.ShortcutKey, | |||
| 214 | + | &i.Position, | |||
| 215 | + | &i.CreatedAt, | |||
| 216 | + | &i.UpdatedAt, | |||
| 217 | + | ); err != nil { | |||
| 218 | + | return nil, err | |||
| 219 | + | } | |||
| 220 | + | items = append(items, i) | |||
| 221 | + | } | |||
| 222 | + | if err := rows.Close(); err != nil { | |||
| 223 | + | return nil, err | |||
| 224 | + | } | |||
| 225 | + | if err := rows.Err(); err != nil { | |||
| 226 | + | return nil, err | |||
| 227 | + | } | |||
| 228 | + | return items, nil | |||
| 229 | + | } | |||
| 230 | + | ||||
| 231 | + | const SetFavoriteFolder = `-- name: SetFavoriteFolder :exec | |||
| 232 | + | UPDATE favorites SET folder_id = ?, updated_at = CURRENT_TIMESTAMP WHERE id = ? | |||
| 233 | + | ` | |||
| 234 | + | ||||
| 235 | + | type SetFavoriteFolderParams struct { | |||
| 236 | + | FolderID sql.NullInt64 `json:"folder_id"` | |||
| 237 | + | ID int64 `json:"id"` | |||
| 238 | + | } | |||
| 239 | + | ||||
| 240 | + | func (q *Queries) SetFavoriteFolder(ctx context.Context, arg SetFavoriteFolderParams) error { | |||
| 241 | + | _, err := q.db.ExecContext(ctx, SetFavoriteFolder, arg.FolderID, arg.ID) | |||
| 242 | + | return err | |||
| 243 | + | } | |||
| 244 | + | ||||
| 245 | + | const SetFavoriteShortcut = `-- name: SetFavoriteShortcut :exec | |||
| 246 | + | UPDATE favorites SET shortcut_key = ?, updated_at = CURRENT_TIMESTAMP WHERE id = ? | |||
| 247 | + | ` | |||
| 248 | + | ||||
| 249 | + | type SetFavoriteShortcutParams struct { | |||
| 250 | + | ShortcutKey sql.NullInt64 `json:"shortcut_key"` | |||
| 251 | + | ID int64 `json:"id"` | |||
| 252 | + | } | |||
| 253 | + | ||||
| 254 | + | func (q *Queries) SetFavoriteShortcut(ctx context.Context, arg SetFavoriteShortcutParams) error { | |||
| 255 | + | _, err := q.db.ExecContext(ctx, SetFavoriteShortcut, arg.ShortcutKey, arg.ID) | |||
| 256 | + | return err | |||
| 257 | + | } | |||
| 258 | + | ||||
| 259 | + | const UpdateFavorite = `-- name: UpdateFavorite :exec | |||
| 260 | + | UPDATE favorites | |||
| 261 | + | SET title = ?, favicon_url = ?, updated_at = CURRENT_TIMESTAMP | |||
| 262 | + | WHERE id = ? | |||
| 263 | + | ` | |||
| 264 | + | ||||
| 265 | + | type UpdateFavoriteParams struct { | |||
| 266 | + | Title sql.NullString `json:"title"` | |||
| 267 | + | FaviconUrl sql.NullString `json:"favicon_url"` | |||
| 268 | + | ID int64 `json:"id"` | |||
| 269 | + | } | |||
| 270 | + | ||||
| 271 | + | func (q *Queries) UpdateFavorite(ctx context.Context, arg UpdateFavoriteParams) error { | |||
| 272 | + | _, err := q.db.ExecContext(ctx, UpdateFavorite, arg.Title, arg.FaviconUrl, arg.ID) | |||
| 273 | + | return err | |||
| 274 | + | } | |||
| 275 | + | ||||
| 276 | + | const UpdateFavoritePosition = `-- name: UpdateFavoritePosition :exec | |||
| 277 | + | UPDATE favorites SET position = ?, updated_at = CURRENT_TIMESTAMP WHERE id = ? | |||
| 278 | + | ` | |||
| 279 | + | ||||
| 280 | + | type UpdateFavoritePositionParams struct { | |||
| 281 | + | Position int64 `json:"position"` | |||
| 282 | + | ID int64 `json:"id"` | |||
| 283 | + | } | |||
| 284 | + | ||||
| 285 | + | func (q *Queries) UpdateFavoritePosition(ctx context.Context, arg UpdateFavoritePositionParams) error { | |||
| 286 | + | _, err := q.db.ExecContext(ctx, UpdateFavoritePosition, arg.Position, arg.ID) | |||
| 287 | + | return err | |||
| 288 | + | } | |||
A internal/infrastructure/persistence/sqlite/sqlc/folders.sql.go +195 -0
| @@ -0,0 +1,195 @@ | |||
| 1 | + | // Code generated by sqlc. DO NOT EDIT. | |
| 2 | + | // versions: | |
| 3 | + | // sqlc v1.30.0 | |
| 4 | + | // source: folders.sql | |
| 5 | + | ||
| 6 | + | package sqlc | |
| 7 | + | ||
| 8 | + | import ( | |
| 9 | + | "context" | |
| 10 | + | "database/sql" | |
| 11 | + | ) | |
| 12 | + | ||
| 13 | + | const CreateFolder = `-- name: CreateFolder :one | |
| 14 | + | INSERT INTO favorite_folders (name, icon, parent_id, position, created_at) | |
| 15 | + | VALUES (?, ?, ?, COALESCE((SELECT MAX(position) + 1 FROM favorite_folders), 0), CURRENT_TIMESTAMP) | |
| 16 | + | RETURNING id, name, icon, parent_id, position, created_at | |
| 17 | + | ` | |
| 18 | + | ||
| 19 | + | type CreateFolderParams struct { | |
| 20 | + | Name string `json:"name"` | |
| 21 | + | Icon sql.NullString `json:"icon"` | |
| 22 | + | ParentID sql.NullInt64 `json:"parent_id"` | |
| 23 | + | } | |
| 24 | + | ||
| 25 | + | func (q *Queries) CreateFolder(ctx context.Context, arg CreateFolderParams) (FavoriteFolder, error) { | |
| 26 | + | row := q.db.QueryRowContext(ctx, CreateFolder, arg.Name, arg.Icon, arg.ParentID) | |
| 27 | + | var i FavoriteFolder | |
| 28 | + | err := row.Scan( | |
| 29 | + | &i.ID, | |
| 30 | + | &i.Name, | |
| 31 | + | &i.Icon, | |
| 32 | + | &i.ParentID, | |
| 33 | + | &i.Position, | |
| 34 | + | &i.CreatedAt, | |
| 35 | + | ) | |
| 36 | + | return i, err | |
| 37 | + | } | |
| 38 | + | ||
| 39 | + | const DeleteFolder = `-- name: DeleteFolder :exec | |
| 40 | + | DELETE FROM favorite_folders WHERE id = ? | |
| 41 | + | ` | |
| 42 | + | ||
| 43 | + | func (q *Queries) DeleteFolder(ctx context.Context, id int64) error { | |
| 44 | + | _, err := q.db.ExecContext(ctx, DeleteFolder, id) | |
| 45 | + | return err | |
| 46 | + | } | |
| 47 | + | ||
| 48 | + | const GetAllFolders = `-- name: GetAllFolders :many | |
| 49 | + | SELECT id, name, icon, parent_id, position, created_at FROM favorite_folders ORDER BY position ASC | |
| 50 | + | ` | |
| 51 | + | ||
| 52 | + | func (q *Queries) GetAllFolders(ctx context.Context) ([]FavoriteFolder, error) { | |
| 53 | + | rows, err := q.db.QueryContext(ctx, GetAllFolders) | |
| 54 | + | if err != nil { | |
| 55 | + | return nil, err | |
| 56 | + | } | |
| 57 | + | defer rows.Close() | |
| 58 | + | items := []FavoriteFolder{} | |
| 59 | + | for rows.Next() { | |
| 60 | + | var i FavoriteFolder | |
| 61 | + | if err := rows.Scan( | |
| 62 | + | &i.ID, | |
| 63 | + | &i.Name, | |
| 64 | + | &i.Icon, | |
| 65 | + | &i.ParentID, | |
| 66 | + | &i.Position, | |
| 67 | + | &i.CreatedAt, | |
| 68 | + | ); err != nil { | |
| 69 | + | return nil, err | |
| 70 | + | } | |
| 71 | + | items = append(items, i) | |
| 72 | + | } | |
| 73 | + | if err := rows.Close(); err != nil { | |
| 74 | + | return nil, err | |
| 75 | + | } | |
| 76 | + | if err := rows.Err(); err != nil { | |
| 77 | + | return nil, err | |
| 78 | + | } | |
| 79 | + | return items, nil | |
| 80 | + | } | |
| 81 | + | ||
| 82 | + | const GetChildFolders = `-- name: GetChildFolders :many | |
| 83 | + | SELECT id, name, icon, parent_id, position, created_at FROM favorite_folders WHERE parent_id = ? ORDER BY position ASC | |
| 84 | + | ` | |
| 85 | + | ||
| 86 | + | func (q *Queries) GetChildFolders(ctx context.Context, parentID sql.NullInt64) ([]FavoriteFolder, error) { | |
| 87 | + | rows, err := q.db.QueryContext(ctx, GetChildFolders, parentID) | |
| 88 | + | if err != nil { | |
| 89 | + | return nil, err | |
| 90 | + | } | |
| 91 | + | defer rows.Close() | |
| 92 | + | items := []FavoriteFolder{} | |
| 93 | + | for rows.Next() { | |
| 94 | + | var i FavoriteFolder | |
| 95 | + | if err := rows.Scan( | |
| 96 | + | &i.ID, | |
| 97 | + | &i.Name, | |
| 98 | + | &i.Icon, | |
| 99 | + | &i.ParentID, | |
| 100 | + | &i.Position, | |
| 101 | + | &i.CreatedAt, | |
| 102 | + | ); err != nil { | |
| 103 | + | return nil, err | |
| 104 | + | } | |
| 105 | + | items = append(items, i) | |
| 106 | + | } | |
| 107 | + | if err := rows.Close(); err != nil { | |
| 108 | + | return nil, err | |
| 109 | + | } | |
| 110 | + | if err := rows.Err(); err != nil { | |
| 111 | + | return nil, err | |
| 112 | + | } | |
| 113 | + | return items, nil | |
| 114 | + | } | |
| 115 | + | ||
| 116 | + | const GetFolderByID = `-- name: GetFolderByID :one | |
| 117 | + | SELECT id, name, icon, parent_id, position, created_at FROM favorite_folders WHERE id = ? LIMIT 1 | |
| 118 | + | ` | |
| 119 | + | ||
| 120 | + | func (q *Queries) GetFolderByID(ctx context.Context, id int64) (FavoriteFolder, error) { | |
| 121 | + | row := q.db.QueryRowContext(ctx, GetFolderByID, id) | |
| 122 | + | var i FavoriteFolder | |
| 123 | + | err := row.Scan( | |
| 124 | + | &i.ID, | |
| 125 | + | &i.Name, | |
| 126 | + | &i.Icon, | |
| 127 | + | &i.ParentID, | |
| 128 | + | &i.Position, | |
| 129 | + | &i.CreatedAt, | |
| 130 | + | ) | |
| 131 | + | return i, err | |
| 132 | + | } | |
| 133 | + | ||
| 134 | + | const GetRootFolders = `-- name: GetRootFolders :many | |
| 135 | + | SELECT id, name, icon, parent_id, position, created_at FROM favorite_folders WHERE parent_id IS NULL ORDER BY position ASC | |
| 136 | + | ` | |
| 137 | + | ||
| 138 | + | func (q *Queries) GetRootFolders(ctx context.Context) ([]FavoriteFolder, error) { | |
| 139 | + | rows, err := q.db.QueryContext(ctx, GetRootFolders) | |
| 140 | + | if err != nil { | |
| 141 | + | return nil, err | |
| 142 | + | } | |
| 143 | + | defer rows.Close() | |
| 144 | + | items := []FavoriteFolder{} | |
| 145 | + | for rows.Next() { | |
| 146 | + | var i FavoriteFolder | |
| 147 | + | if err := rows.Scan( | |
| 148 | + | &i.ID, | |
| 149 | + | &i.Name, | |
| 150 | + | &i.Icon, | |
| 151 | + | &i.ParentID, | |
| 152 | + | &i.Position, | |
| 153 | + | &i.CreatedAt, | |
| 154 | + | ); err != nil { | |
| 155 | + | return nil, err | |
| 156 | + | } | |
| 157 | + | items = append(items, i) | |
| 158 | + | } | |
| 159 | + | if err := rows.Close(); err != nil { | |
| 160 | + | return nil, err | |
| 161 | + | } | |
| 162 | + | if err := rows.Err(); err != nil { | |
| 163 | + | return nil, err | |
| 164 | + | } | |
| 165 | + | return items, nil | |
| 166 | + | } | |
| 167 | + | ||
| 168 | + | const UpdateFolder = `-- name: UpdateFolder :exec | |
| 169 | + | UPDATE favorite_folders SET name = ?, icon = ? WHERE id = ? | |
| 170 | + | ` | |
| 171 | + | ||
| 172 | + | type UpdateFolderParams struct { | |
| 173 | + | Name string `json:"name"` | |
| 174 | + | Icon sql.NullString `json:"icon"` | |
| 175 | + | ID int64 `json:"id"` | |
| 176 | + | } | |
| 177 | + | ||
| 178 | + | func (q *Queries) UpdateFolder(ctx context.Context, arg UpdateFolderParams) error { | |
| 179 | + | _, err := q.db.ExecContext(ctx, UpdateFolder, arg.Name, arg.Icon, arg.ID) | |
| 180 | + | return err | |
| 181 | + | } | |
| 182 | + | ||
| 183 | + | const UpdateFolderPosition = `-- name: UpdateFolderPosition :exec | |
| 184 | + | UPDATE favorite_folders SET position = ? WHERE id = ? | |
| 185 | + | ` | |
| 186 | + | ||
| 187 | + | type UpdateFolderPositionParams struct { | |
| 188 | + | Position int64 `json:"position"` | |
| 189 | + | ID int64 `json:"id"` | |
| 190 | + | } | |
| 191 | + | ||
| 192 | + | func (q *Queries) UpdateFolderPosition(ctx context.Context, arg UpdateFolderPositionParams) error { | |
| 193 | + | _, err := q.db.ExecContext(ctx, UpdateFolderPosition, arg.Position, arg.ID) | |
| 194 | + | return err | |
| 195 | + | } | |
| @@ -0,0 +1,195 @@ | |||||
| 1 | + | // Code generated by sqlc. DO NOT EDIT. | |||
| 2 | + | // versions: | |||
| 3 | + | // sqlc v1.30.0 | |||
| 4 | + | // source: folders.sql | |||
| 5 | + | ||||
| 6 | + | package sqlc | |||
| 7 | + | ||||
| 8 | + | import ( | |||
| 9 | + | "context" | |||
| 10 | + | "database/sql" | |||
| 11 | + | ) | |||
| 12 | + | ||||
| 13 | + | const CreateFolder = `-- name: CreateFolder :one | |||
| 14 | + | INSERT INTO favorite_folders (name, icon, parent_id, position, created_at) | |||
| 15 | + | VALUES (?, ?, ?, COALESCE((SELECT MAX(position) + 1 FROM favorite_folders), 0), CURRENT_TIMESTAMP) | |||
| 16 | + | RETURNING id, name, icon, parent_id, position, created_at | |||
| 17 | + | ` | |||
| 18 | + | ||||
| 19 | + | type CreateFolderParams struct { | |||
| 20 | + | Name string `json:"name"` | |||
| 21 | + | Icon sql.NullString `json:"icon"` | |||
| 22 | + | ParentID sql.NullInt64 `json:"parent_id"` | |||
| 23 | + | } | |||
| 24 | + | ||||
| 25 | + | func (q *Queries) CreateFolder(ctx context.Context, arg CreateFolderParams) (FavoriteFolder, error) { | |||
| 26 | + | row := q.db.QueryRowContext(ctx, CreateFolder, arg.Name, arg.Icon, arg.ParentID) | |||
| 27 | + | var i FavoriteFolder | |||
| 28 | + | err := row.Scan( | |||
| 29 | + | &i.ID, | |||
| 30 | + | &i.Name, | |||
| 31 | + | &i.Icon, | |||
| 32 | + | &i.ParentID, | |||
| 33 | + | &i.Position, | |||
| 34 | + | &i.CreatedAt, | |||
| 35 | + | ) | |||
| 36 | + | return i, err | |||
| 37 | + | } | |||
| 38 | + | ||||
| 39 | + | const DeleteFolder = `-- name: DeleteFolder :exec | |||
| 40 | + | DELETE FROM favorite_folders WHERE id = ? | |||
| 41 | + | ` | |||
| 42 | + | ||||
| 43 | + | func (q *Queries) DeleteFolder(ctx context.Context, id int64) error { | |||
| 44 | + | _, err := q.db.ExecContext(ctx, DeleteFolder, id) | |||
| 45 | + | return err | |||
| 46 | + | } | |||
| 47 | + | ||||
| 48 | + | const GetAllFolders = `-- name: GetAllFolders :many | |||
| 49 | + | SELECT id, name, icon, parent_id, position, created_at FROM favorite_folders ORDER BY position ASC | |||
| 50 | + | ` | |||
| 51 | + | ||||
| 52 | + | func (q *Queries) GetAllFolders(ctx context.Context) ([]FavoriteFolder, error) { | |||
| 53 | + | rows, err := q.db.QueryContext(ctx, GetAllFolders) | |||
| 54 | + | if err != nil { | |||
| 55 | + | return nil, err | |||
| 56 | + | } | |||
| 57 | + | defer rows.Close() | |||
| 58 | + | items := []FavoriteFolder{} | |||
| 59 | + | for rows.Next() { | |||
| 60 | + | var i FavoriteFolder | |||
| 61 | + | if err := rows.Scan( | |||
| 62 | + | &i.ID, | |||
| 63 | + | &i.Name, | |||
| 64 | + | &i.Icon, | |||
| 65 | + | &i.ParentID, | |||
| 66 | + | &i.Position, | |||
| 67 | + | &i.CreatedAt, | |||
| 68 | + | ); err != nil { | |||
| 69 | + | return nil, err | |||
| 70 | + | } | |||
| 71 | + | items = append(items, i) | |||
| 72 | + | } | |||
| 73 | + | if err := rows.Close(); err != nil { | |||
| 74 | + | return nil, err | |||
| 75 | + | } | |||
| 76 | + | if err := rows.Err(); err != nil { | |||
| 77 | + | return nil, err | |||
| 78 | + | } | |||
| 79 | + | return items, nil | |||
| 80 | + | } | |||
| 81 | + | ||||
| 82 | + | const GetChildFolders = `-- name: GetChildFolders :many | |||
| 83 | + | SELECT id, name, icon, parent_id, position, created_at FROM favorite_folders WHERE parent_id = ? ORDER BY position ASC | |||
| 84 | + | ` | |||
| 85 | + | ||||
| 86 | + | func (q *Queries) GetChildFolders(ctx context.Context, parentID sql.NullInt64) ([]FavoriteFolder, error) { | |||
| 87 | + | rows, err := q.db.QueryContext(ctx, GetChildFolders, parentID) | |||
| 88 | + | if err != nil { | |||
| 89 | + | return nil, err | |||
| 90 | + | } | |||
| 91 | + | defer rows.Close() | |||
| 92 | + | items := []FavoriteFolder{} | |||
| 93 | + | for rows.Next() { | |||
| 94 | + | var i FavoriteFolder | |||
| 95 | + | if err := rows.Scan( | |||
| 96 | + | &i.ID, | |||
| 97 | + | &i.Name, | |||
| 98 | + | &i.Icon, | |||
| 99 | + | &i.ParentID, | |||
| 100 | + | &i.Position, | |||
| 101 | + | &i.CreatedAt, | |||
| 102 | + | ); err != nil { | |||
| 103 | + | return nil, err | |||
| 104 | + | } | |||
| 105 | + | items = append(items, i) | |||
| 106 | + | } | |||
| 107 | + | if err := rows.Close(); err != nil { | |||
| 108 | + | return nil, err | |||
| 109 | + | } | |||
| 110 | + | if err := rows.Err(); err != nil { | |||
| 111 | + | return nil, err | |||
| 112 | + | } | |||
| 113 | + | return items, nil | |||
| 114 | + | } | |||
| 115 | + | ||||
| 116 | + | const GetFolderByID = `-- name: GetFolderByID :one | |||
| 117 | + | SELECT id, name, icon, parent_id, position, created_at FROM favorite_folders WHERE id = ? LIMIT 1 | |||
| 118 | + | ` | |||
| 119 | + | ||||
| 120 | + | func (q *Queries) GetFolderByID(ctx context.Context, id int64) (FavoriteFolder, error) { | |||
| 121 | + | row := q.db.QueryRowContext(ctx, GetFolderByID, id) | |||
| 122 | + | var i FavoriteFolder | |||
| 123 | + | err := row.Scan( | |||
| 124 | + | &i.ID, | |||
| 125 | + | &i.Name, | |||
| 126 | + | &i.Icon, | |||
| 127 | + | &i.ParentID, | |||
| 128 | + | &i.Position, | |||
| 129 | + | &i.CreatedAt, | |||
| 130 | + | ) | |||
| 131 | + | return i, err | |||
| 132 | + | } | |||
| 133 | + | ||||
| 134 | + | const GetRootFolders = `-- name: GetRootFolders :many | |||
| 135 | + | SELECT id, name, icon, parent_id, position, created_at FROM favorite_folders WHERE parent_id IS NULL ORDER BY position ASC | |||
| 136 | + | ` | |||
| 137 | + | ||||
| 138 | + | func (q *Queries) GetRootFolders(ctx context.Context) ([]FavoriteFolder, error) { | |||
| 139 | + | rows, err := q.db.QueryContext(ctx, GetRootFolders) | |||
| 140 | + | if err != nil { | |||
| 141 | + | return nil, err | |||
| 142 | + | } | |||
| 143 | + | defer rows.Close() | |||
| 144 | + | items := []FavoriteFolder{} | |||
| 145 | + | for rows.Next() { | |||
| 146 | + | var i FavoriteFolder | |||
| 147 | + | if err := rows.Scan( | |||
| 148 | + | &i.ID, | |||
| 149 | + | &i.Name, | |||
| 150 | + | &i.Icon, | |||
| 151 | + | &i.ParentID, | |||
| 152 | + | &i.Position, | |||
| 153 | + | &i.CreatedAt, | |||
| 154 | + | ); err != nil { | |||
| 155 | + | return nil, err | |||
| 156 | + | } | |||
| 157 | + | items = append(items, i) | |||
| 158 | + | } | |||
| 159 | + | if err := rows.Close(); err != nil { | |||
| 160 | + | return nil, err | |||
| 161 | + | } | |||
| 162 | + | if err := rows.Err(); err != nil { | |||
| 163 | + | return nil, err | |||
| 164 | + | } | |||
| 165 | + | return items, nil | |||
| 166 | + | } | |||
| 167 | + | ||||
| 168 | + | const UpdateFolder = `-- name: UpdateFolder :exec | |||
| 169 | + | UPDATE favorite_folders SET name = ?, icon = ? WHERE id = ? | |||
| 170 | + | ` | |||
| 171 | + | ||||
| 172 | + | type UpdateFolderParams struct { | |||
| 173 | + | Name string `json:"name"` | |||
| 174 | + | Icon sql.NullString `json:"icon"` | |||
| 175 | + | ID int64 `json:"id"` | |||
| 176 | + | } | |||
| 177 | + | ||||
| 178 | + | func (q *Queries) UpdateFolder(ctx context.Context, arg UpdateFolderParams) error { | |||
| 179 | + | _, err := q.db.ExecContext(ctx, UpdateFolder, arg.Name, arg.Icon, arg.ID) | |||
| 180 | + | return err | |||
| 181 | + | } | |||
| 182 | + | ||||
| 183 | + | const UpdateFolderPosition = `-- name: UpdateFolderPosition :exec | |||
| 184 | + | UPDATE favorite_folders SET position = ? WHERE id = ? | |||
| 185 | + | ` | |||
| 186 | + | ||||
| 187 | + | type UpdateFolderPositionParams struct { | |||
| 188 | + | Position int64 `json:"position"` | |||
| 189 | + | ID int64 `json:"id"` | |||
| 190 | + | } | |||
| 191 | + | ||||
| 192 | + | func (q *Queries) UpdateFolderPosition(ctx context.Context, arg UpdateFolderPositionParams) error { | |||
| 193 | + | _, err := q.db.ExecContext(ctx, UpdateFolderPosition, arg.Position, arg.ID) | |||
| 194 | + | return err | |||
| 195 | + | } | |||
A internal/infrastructure/persistence/sqlite/sqlc/history.sql.go +174 -0
| @@ -0,0 +1,174 @@ | |||
| 1 | + | // Code generated by sqlc. DO NOT EDIT. | |
| 2 | + | // versions: | |
| 3 | + | // sqlc v1.30.0 | |
| 4 | + | // source: history.sql | |
| 5 | + | ||
| 6 | + | package sqlc | |
| 7 | + | ||
| 8 | + | import ( | |
| 9 | + | "context" | |
| 10 | + | "database/sql" | |
| 11 | + | ) | |
| 12 | + | ||
| 13 | + | const DeleteAllHistory = `-- name: DeleteAllHistory :exec | |
| 14 | + | DELETE FROM history | |
| 15 | + | ` | |
| 16 | + | ||
| 17 | + | func (q *Queries) DeleteAllHistory(ctx context.Context) error { | |
| 18 | + | _, err := q.db.ExecContext(ctx, DeleteAllHistory) | |
| 19 | + | return err | |
| 20 | + | } | |
| 21 | + | ||
| 22 | + | const DeleteHistoryByID = `-- name: DeleteHistoryByID :exec | |
| 23 | + | DELETE FROM history WHERE id = ? | |
| 24 | + | ` | |
| 25 | + | ||
| 26 | + | func (q *Queries) DeleteHistoryByID(ctx context.Context, id int64) error { | |
| 27 | + | _, err := q.db.ExecContext(ctx, DeleteHistoryByID, id) | |
| 28 | + | return err | |
| 29 | + | } | |
| 30 | + | ||
| 31 | + | const DeleteHistoryOlderThan = `-- name: DeleteHistoryOlderThan :exec | |
| 32 | + | DELETE FROM history WHERE last_visited < ? | |
| 33 | + | ` | |
| 34 | + | ||
| 35 | + | func (q *Queries) DeleteHistoryOlderThan(ctx context.Context, lastVisited sql.NullTime) error { | |
| 36 | + | _, err := q.db.ExecContext(ctx, DeleteHistoryOlderThan, lastVisited) | |
| 37 | + | return err | |
| 38 | + | } | |
| 39 | + | ||
| 40 | + | const GetHistoryByURL = `-- name: GetHistoryByURL :one | |
| 41 | + | SELECT id, url, title, favicon_url, visit_count, last_visited, created_at FROM history WHERE url = ? LIMIT 1 | |
| 42 | + | ` | |
| 43 | + | ||
| 44 | + | func (q *Queries) GetHistoryByURL(ctx context.Context, url string) (History, error) { | |
| 45 | + | row := q.db.QueryRowContext(ctx, GetHistoryByURL, url) | |
| 46 | + | var i History | |
| 47 | + | err := row.Scan( | |
| 48 | + | &i.ID, | |
| 49 | + | &i.Url, | |
| 50 | + | &i.Title, | |
| 51 | + | &i.FaviconUrl, | |
| 52 | + | &i.VisitCount, | |
| 53 | + | &i.LastVisited, | |
| 54 | + | &i.CreatedAt, | |
| 55 | + | ) | |
| 56 | + | return i, err | |
| 57 | + | } | |
| 58 | + | ||
| 59 | + | const GetRecentHistory = `-- name: GetRecentHistory :many | |
| 60 | + | SELECT id, url, title, favicon_url, visit_count, last_visited, created_at FROM history ORDER BY last_visited DESC LIMIT ? OFFSET ? | |
| 61 | + | ` | |
| 62 | + | ||
| 63 | + | type GetRecentHistoryParams struct { | |
| 64 | + | Limit int64 `json:"limit"` | |
| 65 | + | Offset int64 `json:"offset"` | |
| 66 | + | } | |
| 67 | + | ||
| 68 | + | func (q *Queries) GetRecentHistory(ctx context.Context, arg GetRecentHistoryParams) ([]History, error) { | |
| 69 | + | rows, err := q.db.QueryContext(ctx, GetRecentHistory, arg.Limit, arg.Offset) | |
| 70 | + | if err != nil { | |
| 71 | + | return nil, err | |
| 72 | + | } | |
| 73 | + | defer rows.Close() | |
| 74 | + | items := []History{} | |
| 75 | + | for rows.Next() { | |
| 76 | + | var i History | |
| 77 | + | if err := rows.Scan( | |
| 78 | + | &i.ID, | |
| 79 | + | &i.Url, | |
| 80 | + | &i.Title, | |
| 81 | + | &i.FaviconUrl, | |
| 82 | + | &i.VisitCount, | |
| 83 | + | &i.LastVisited, | |
| 84 | + | &i.CreatedAt, | |
| 85 | + | ); err != nil { | |
| 86 | + | return nil, err | |
| 87 | + | } | |
| 88 | + | items = append(items, i) | |
| 89 | + | } | |
| 90 | + | if err := rows.Close(); err != nil { | |
| 91 | + | return nil, err | |
| 92 | + | } | |
| 93 | + | if err := rows.Err(); err != nil { | |
| 94 | + | return nil, err | |
| 95 | + | } | |
| 96 | + | return items, nil | |
| 97 | + | } | |
| 98 | + | ||
| 99 | + | const IncrementVisitCount = `-- name: IncrementVisitCount :exec | |
| 100 | + | UPDATE history | |
| 101 | + | SET visit_count = visit_count + 1, last_visited = CURRENT_TIMESTAMP | |
| 102 | + | WHERE url = ? | |
| 103 | + | ` | |
| 104 | + | ||
| 105 | + | func (q *Queries) IncrementVisitCount(ctx context.Context, url string) error { | |
| 106 | + | _, err := q.db.ExecContext(ctx, IncrementVisitCount, url) | |
| 107 | + | return err | |
| 108 | + | } | |
| 109 | + | ||
| 110 | + | const SearchHistory = `-- name: SearchHistory :many | |
| 111 | + | SELECT id, url, title, favicon_url, visit_count, last_visited, created_at FROM history | |
| 112 | + | WHERE url LIKE '%' || ? || '%' OR title LIKE '%' || ? || '%' | |
| 113 | + | ORDER BY visit_count DESC, last_visited DESC | |
| 114 | + | LIMIT ? | |
| 115 | + | ` | |
| 116 | + | ||
| 117 | + | type SearchHistoryParams struct { | |
| 118 | + | Column1 sql.NullString `json:"column_1"` | |
| 119 | + | Column2 sql.NullString `json:"column_2"` | |
| 120 | + | Limit int64 `json:"limit"` | |
| 121 | + | } | |
| 122 | + | ||
| 123 | + | func (q *Queries) SearchHistory(ctx context.Context, arg SearchHistoryParams) ([]History, error) { | |
| 124 | + | rows, err := q.db.QueryContext(ctx, SearchHistory, arg.Column1, arg.Column2, arg.Limit) | |
| 125 | + | if err != nil { | |
| 126 | + | return nil, err | |
| 127 | + | } | |
| 128 | + | defer rows.Close() | |
| 129 | + | items := []History{} | |
| 130 | + | for rows.Next() { | |
| 131 | + | var i History | |
| 132 | + | if err := rows.Scan( | |
| 133 | + | &i.ID, | |
| 134 | + | &i.Url, | |
| 135 | + | &i.Title, | |
| 136 | + | &i.FaviconUrl, | |
| 137 | + | &i.VisitCount, | |
| 138 | + | &i.LastVisited, | |
| 139 | + | &i.CreatedAt, | |
| 140 | + | ); err != nil { | |
| 141 | + | return nil, err | |
| 142 | + | } | |
| 143 | + | items = append(items, i) | |
| 144 | + | } | |
| 145 | + | if err := rows.Close(); err != nil { | |
| 146 | + | return nil, err | |
| 147 | + | } | |
| 148 | + | if err := rows.Err(); err != nil { | |
| 149 | + | return nil, err | |
| 150 | + | } | |
| 151 | + | return items, nil | |
| 152 | + | } | |
| 153 | + | ||
| 154 | + | const UpsertHistory = `-- name: UpsertHistory :exec | |
| 155 | + | INSERT INTO history (url, title, favicon_url) | |
| 156 | + | VALUES (?, ?, ?) | |
| 157 | + | ON CONFLICT(url) | |
| 158 | + | DO UPDATE SET | |
| 159 | + | visit_count = visit_count + 1, | |
| 160 | + | last_visited = CURRENT_TIMESTAMP, | |
| 161 | + | title = COALESCE(EXCLUDED.title, history.title), | |
| 162 | + | favicon_url = COALESCE(EXCLUDED.favicon_url, history.favicon_url) | |
| 163 | + | ` | |
| 164 | + | ||
| 165 | + | type UpsertHistoryParams struct { | |
| 166 | + | Url string `json:"url"` | |
| 167 | + | Title sql.NullString `json:"title"` | |
| 168 | + | FaviconUrl sql.NullString `json:"favicon_url"` | |
| 169 | + | } | |
| 170 | + | ||
| 171 | + | func (q *Queries) UpsertHistory(ctx context.Context, arg UpsertHistoryParams) error { | |
| 172 | + | _, err := q.db.ExecContext(ctx, UpsertHistory, arg.Url, arg.Title, arg.FaviconUrl) | |
| 173 | + | return err | |
| 174 | + | } | |
| @@ -0,0 +1,174 @@ | |||||
| 1 | + | // Code generated by sqlc. DO NOT EDIT. | |||
| 2 | + | // versions: | |||
| 3 | + | // sqlc v1.30.0 | |||
| 4 | + | // source: history.sql | |||
| 5 | + | ||||
| 6 | + | package sqlc | |||
| 7 | + | ||||
| 8 | + | import ( | |||
| 9 | + | "context" | |||
| 10 | + | "database/sql" | |||
| 11 | + | ) | |||
| 12 | + | ||||
| 13 | + | const DeleteAllHistory = `-- name: DeleteAllHistory :exec | |||
| 14 | + | DELETE FROM history | |||
| 15 | + | ` | |||
| 16 | + | ||||
| 17 | + | func (q *Queries) DeleteAllHistory(ctx context.Context) error { | |||
| 18 | + | _, err := q.db.ExecContext(ctx, DeleteAllHistory) | |||
| 19 | + | return err | |||
| 20 | + | } | |||
| 21 | + | ||||
| 22 | + | const DeleteHistoryByID = `-- name: DeleteHistoryByID :exec | |||
| 23 | + | DELETE FROM history WHERE id = ? | |||
| 24 | + | ` | |||
| 25 | + | ||||
| 26 | + | func (q *Queries) DeleteHistoryByID(ctx context.Context, id int64) error { | |||
| 27 | + | _, err := q.db.ExecContext(ctx, DeleteHistoryByID, id) | |||
| 28 | + | return err | |||
| 29 | + | } | |||
| 30 | + | ||||
| 31 | + | const DeleteHistoryOlderThan = `-- name: DeleteHistoryOlderThan :exec | |||
| 32 | + | DELETE FROM history WHERE last_visited < ? | |||
| 33 | + | ` | |||
| 34 | + | ||||
| 35 | + | func (q *Queries) DeleteHistoryOlderThan(ctx context.Context, lastVisited sql.NullTime) error { | |||
| 36 | + | _, err := q.db.ExecContext(ctx, DeleteHistoryOlderThan, lastVisited) | |||
| 37 | + | return err | |||
| 38 | + | } | |||
| 39 | + | ||||
| 40 | + | const GetHistoryByURL = `-- name: GetHistoryByURL :one | |||
| 41 | + | SELECT id, url, title, favicon_url, visit_count, last_visited, created_at FROM history WHERE url = ? LIMIT 1 | |||
| 42 | + | ` | |||
| 43 | + | ||||
| 44 | + | func (q *Queries) GetHistoryByURL(ctx context.Context, url string) (History, error) { | |||
| 45 | + | row := q.db.QueryRowContext(ctx, GetHistoryByURL, url) | |||
| 46 | + | var i History | |||
| 47 | + | err := row.Scan( | |||
| 48 | + | &i.ID, | |||
| 49 | + | &i.Url, | |||
| 50 | + | &i.Title, | |||
| 51 | + | &i.FaviconUrl, | |||
| 52 | + | &i.VisitCount, | |||
| 53 | + | &i.LastVisited, | |||
| 54 | + | &i.CreatedAt, | |||
| 55 | + | ) | |||
| 56 | + | return i, err | |||
| 57 | + | } | |||
| 58 | + | ||||
| 59 | + | const GetRecentHistory = `-- name: GetRecentHistory :many | |||
| 60 | + | SELECT id, url, title, favicon_url, visit_count, last_visited, created_at FROM history ORDER BY last_visited DESC LIMIT ? OFFSET ? | |||
| 61 | + | ` | |||
| 62 | + | ||||
| 63 | + | type GetRecentHistoryParams struct { | |||
| 64 | + | Limit int64 `json:"limit"` | |||
| 65 | + | Offset int64 `json:"offset"` | |||
| 66 | + | } | |||
| 67 | + | ||||
| 68 | + | func (q *Queries) GetRecentHistory(ctx context.Context, arg GetRecentHistoryParams) ([]History, error) { | |||
| 69 | + | rows, err := q.db.QueryContext(ctx, GetRecentHistory, arg.Limit, arg.Offset) | |||
| 70 | + | if err != nil { | |||
| 71 | + | return nil, err | |||
| 72 | + | } | |||
| 73 | + | defer rows.Close() | |||
| 74 | + | items := []History{} | |||
| 75 | + | for rows.Next() { | |||
| 76 | + | var i History | |||
| 77 | + | if err := rows.Scan( | |||
| 78 | + | &i.ID, | |||
| 79 | + | &i.Url, | |||
| 80 | + | &i.Title, | |||
| 81 | + | &i.FaviconUrl, | |||
| 82 | + | &i.VisitCount, | |||
| 83 | + | &i.LastVisited, | |||
| 84 | + | &i.CreatedAt, | |||
| 85 | + | ); err != nil { | |||
| 86 | + | return nil, err | |||
| 87 | + | } | |||
| 88 | + | items = append(items, i) | |||
| 89 | + | } | |||
| 90 | + | if err := rows.Close(); err != nil { | |||
| 91 | + | return nil, err | |||
| 92 | + | } | |||
| 93 | + | if err := rows.Err(); err != nil { | |||
| 94 | + | return nil, err | |||
| 95 | + | } | |||
| 96 | + | return items, nil | |||
| 97 | + | } | |||
| 98 | + | ||||
| 99 | + | const IncrementVisitCount = `-- name: IncrementVisitCount :exec | |||
| 100 | + | UPDATE history | |||
| 101 | + | SET visit_count = visit_count + 1, last_visited = CURRENT_TIMESTAMP | |||
| 102 | + | WHERE url = ? | |||
| 103 | + | ` | |||
| 104 | + | ||||
| 105 | + | func (q *Queries) IncrementVisitCount(ctx context.Context, url string) error { | |||
| 106 | + | _, err := q.db.ExecContext(ctx, IncrementVisitCount, url) | |||
| 107 | + | return err | |||
| 108 | + | } | |||
| 109 | + | ||||
| 110 | + | const SearchHistory = `-- name: SearchHistory :many | |||
| 111 | + | SELECT id, url, title, favicon_url, visit_count, last_visited, created_at FROM history | |||
| 112 | + | WHERE url LIKE '%' || ? || '%' OR title LIKE '%' || ? || '%' | |||
| 113 | + | ORDER BY visit_count DESC, last_visited DESC | |||
| 114 | + | LIMIT ? | |||
| 115 | + | ` | |||
| 116 | + | ||||
| 117 | + | type SearchHistoryParams struct { | |||
| 118 | + | Column1 sql.NullString `json:"column_1"` | |||
| 119 | + | Column2 sql.NullString `json:"column_2"` | |||
| 120 | + | Limit int64 `json:"limit"` | |||
| 121 | + | } | |||
| 122 | + | ||||
| 123 | + | func (q *Queries) SearchHistory(ctx context.Context, arg SearchHistoryParams) ([]History, error) { | |||
| 124 | + | rows, err := q.db.QueryContext(ctx, SearchHistory, arg.Column1, arg.Column2, arg.Limit) | |||
| 125 | + | if err != nil { | |||
| 126 | + | return nil, err | |||
| 127 | + | } | |||
| 128 | + | defer rows.Close() | |||
| 129 | + | items := []History{} | |||
| 130 | + | for rows.Next() { | |||
| 131 | + | var i History | |||
| 132 | + | if err := rows.Scan( | |||
| 133 | + | &i.ID, | |||
| 134 | + | &i.Url, | |||
| 135 | + | &i.Title, | |||
| 136 | + | &i.FaviconUrl, | |||
| 137 | + | &i.VisitCount, | |||
| 138 | + | &i.LastVisited, | |||
| 139 | + | &i.CreatedAt, | |||
| 140 | + | ); err != nil { | |||
| 141 | + | return nil, err | |||
| 142 | + | } | |||
| 143 | + | items = append(items, i) | |||
| 144 | + | } | |||
| 145 | + | if err := rows.Close(); err != nil { | |||
| 146 | + | return nil, err | |||
| 147 | + | } | |||
| 148 | + | if err := rows.Err(); err != nil { | |||
| 149 | + | return nil, err | |||
| 150 | + | } | |||
| 151 | + | return items, nil | |||
| 152 | + | } | |||
| 153 | + | ||||
| 154 | + | const UpsertHistory = `-- name: UpsertHistory :exec | |||
| 155 | + | INSERT INTO history (url, title, favicon_url) | |||
| 156 | + | VALUES (?, ?, ?) | |||
| 157 | + | ON CONFLICT(url) | |||
| 158 | + | DO UPDATE SET | |||
| 159 | + | visit_count = visit_count + 1, | |||
| 160 | + | last_visited = CURRENT_TIMESTAMP, | |||
| 161 | + | title = COALESCE(EXCLUDED.title, history.title), | |||
| 162 | + | favicon_url = COALESCE(EXCLUDED.favicon_url, history.favicon_url) | |||
| 163 | + | ` | |||
| 164 | + | ||||
| 165 | + | type UpsertHistoryParams struct { | |||
| 166 | + | Url string `json:"url"` | |||
| 167 | + | Title sql.NullString `json:"title"` | |||
| 168 | + | FaviconUrl sql.NullString `json:"favicon_url"` | |||
| 169 | + | } | |||
| 170 | + | ||||
| 171 | + | func (q *Queries) UpsertHistory(ctx context.Context, arg UpsertHistoryParams) error { | |||
| 172 | + | _, err := q.db.ExecContext(ctx, UpsertHistory, arg.Url, arg.Title, arg.FaviconUrl) | |||
| 173 | + | return err | |||
| 174 | + | } | |||
A internal/infrastructure/persistence/sqlite/sqlc/models.go +69 -0
| @@ -0,0 +1,69 @@ | |||
| 1 | + | // Code generated by sqlc. DO NOT EDIT. | |
| 2 | + | // versions: | |
| 3 | + | // sqlc v1.30.0 | |
| 4 | + | ||
| 5 | + | package sqlc | |
| 6 | + | ||
| 7 | + | import ( | |
| 8 | + | "database/sql" | |
| 9 | + | ) | |
| 10 | + | ||
| 11 | + | type ContentWhitelist struct { | |
| 12 | + | ID int64 `json:"id"` | |
| 13 | + | Domain string `json:"domain"` | |
| 14 | + | CreatedAt sql.NullTime `json:"created_at"` | |
| 15 | + | } | |
| 16 | + | ||
| 17 | + | type Favorite struct { | |
| 18 | + | ID int64 `json:"id"` | |
| 19 | + | Url string `json:"url"` | |
| 20 | + | Title sql.NullString `json:"title"` | |
| 21 | + | FaviconUrl sql.NullString `json:"favicon_url"` | |
| 22 | + | FolderID sql.NullInt64 `json:"folder_id"` | |
| 23 | + | ShortcutKey sql.NullInt64 `json:"shortcut_key"` | |
| 24 | + | Position int64 `json:"position"` | |
| 25 | + | CreatedAt sql.NullTime `json:"created_at"` | |
| 26 | + | UpdatedAt sql.NullTime `json:"updated_at"` | |
| 27 | + | } | |
| 28 | + | ||
| 29 | + | type FavoriteFolder struct { | |
| 30 | + | ID int64 `json:"id"` | |
| 31 | + | Name string `json:"name"` | |
| 32 | + | Icon sql.NullString `json:"icon"` | |
| 33 | + | ParentID sql.NullInt64 `json:"parent_id"` | |
| 34 | + | Position int64 `json:"position"` | |
| 35 | + | CreatedAt sql.NullTime `json:"created_at"` | |
| 36 | + | } | |
| 37 | + | ||
| 38 | + | type FavoriteTag struct { | |
| 39 | + | ID int64 `json:"id"` | |
| 40 | + | Name string `json:"name"` | |
| 41 | + | Color string `json:"color"` | |
| 42 | + | CreatedAt sql.NullTime `json:"created_at"` | |
| 43 | + | } | |
| 44 | + | ||
| 45 | + | type FavoriteTagAssignment struct { | |
| 46 | + | FavoriteID int64 `json:"favorite_id"` | |
| 47 | + | TagID int64 `json:"tag_id"` | |
| 48 | + | } | |
| 49 | + | ||
| 50 | + | type History struct { | |
| 51 | + | ID int64 `json:"id"` | |
| 52 | + | Url string `json:"url"` | |
| 53 | + | Title sql.NullString `json:"title"` | |
| 54 | + | FaviconUrl sql.NullString `json:"favicon_url"` | |
| 55 | + | VisitCount sql.NullInt64 `json:"visit_count"` | |
| 56 | + | LastVisited sql.NullTime `json:"last_visited"` | |
| 57 | + | CreatedAt sql.NullTime `json:"created_at"` | |
| 58 | + | } | |
| 59 | + | ||
| 60 | + | type HistoryFt struct { | |
| 61 | + | Url string `json:"url"` | |
| 62 | + | Title string `json:"title"` | |
| 63 | + | } | |
| 64 | + | ||
| 65 | + | type ZoomLevel struct { | |
| 66 | + | Domain string `json:"domain"` | |
| 67 | + | ZoomFactor float64 `json:"zoom_factor"` | |
| 68 | + | UpdatedAt sql.NullTime `json:"updated_at"` | |
| 69 | + | } | |
| @@ -0,0 +1,69 @@ | |||||
| 1 | + | // Code generated by sqlc. DO NOT EDIT. | |||
| 2 | + | // versions: | |||
| 3 | + | // sqlc v1.30.0 | |||
| 4 | + | ||||
| 5 | + | package sqlc | |||
| 6 | + | ||||
| 7 | + | import ( | |||
| 8 | + | "database/sql" | |||
| 9 | + | ) | |||
| 10 | + | ||||
| 11 | + | type ContentWhitelist struct { | |||
| 12 | + | ID int64 `json:"id"` | |||
| 13 | + | Domain string `json:"domain"` | |||
| 14 | + | CreatedAt sql.NullTime `json:"created_at"` | |||
| 15 | + | } | |||
| 16 | + | ||||
| 17 | + | type Favorite struct { | |||
| 18 | + | ID int64 `json:"id"` | |||
| 19 | + | Url string `json:"url"` | |||
| 20 | + | Title sql.NullString `json:"title"` | |||
| 21 | + | FaviconUrl sql.NullString `json:"favicon_url"` | |||
| 22 | + | FolderID sql.NullInt64 `json:"folder_id"` | |||
| 23 | + | ShortcutKey sql.NullInt64 `json:"shortcut_key"` | |||
| 24 | + | Position int64 `json:"position"` | |||
| 25 | + | CreatedAt sql.NullTime `json:"created_at"` | |||
| 26 | + | UpdatedAt sql.NullTime `json:"updated_at"` | |||
| 27 | + | } | |||
| 28 | + | ||||
| 29 | + | type FavoriteFolder struct { | |||
| 30 | + | ID int64 `json:"id"` | |||
| 31 | + | Name string `json:"name"` | |||
| 32 | + | Icon sql.NullString `json:"icon"` | |||
| 33 | + | ParentID sql.NullInt64 `json:"parent_id"` | |||
| 34 | + | Position int64 `json:"position"` | |||
| 35 | + | CreatedAt sql.NullTime `json:"created_at"` | |||
| 36 | + | } | |||
| 37 | + | ||||
| 38 | + | type FavoriteTag struct { | |||
| 39 | + | ID int64 `json:"id"` | |||
| 40 | + | Name string `json:"name"` | |||
| 41 | + | Color string `json:"color"` | |||
| 42 | + | CreatedAt sql.NullTime `json:"created_at"` | |||
| 43 | + | } | |||
| 44 | + | ||||
| 45 | + | type FavoriteTagAssignment struct { | |||
| 46 | + | FavoriteID int64 `json:"favorite_id"` | |||
| 47 | + | TagID int64 `json:"tag_id"` | |||
| 48 | + | } | |||
| 49 | + | ||||
| 50 | + | type History struct { | |||
| 51 | + | ID int64 `json:"id"` | |||
| 52 | + | Url string `json:"url"` | |||
| 53 | + | Title sql.NullString `json:"title"` | |||
| 54 | + | FaviconUrl sql.NullString `json:"favicon_url"` | |||
| 55 | + | VisitCount sql.NullInt64 `json:"visit_count"` | |||
| 56 | + | LastVisited sql.NullTime `json:"last_visited"` | |||
| 57 | + | CreatedAt sql.NullTime `json:"created_at"` | |||
| 58 | + | } | |||
| 59 | + | ||||
| 60 | + | type HistoryFt struct { | |||
| 61 | + | Url string `json:"url"` | |||
| 62 | + | Title string `json:"title"` | |||
| 63 | + | } | |||
| 64 | + | ||||
| 65 | + | type ZoomLevel struct { | |||
| 66 | + | Domain string `json:"domain"` | |||
| 67 | + | ZoomFactor float64 `json:"zoom_factor"` | |||
| 68 | + | UpdatedAt sql.NullTime `json:"updated_at"` | |||
| 69 | + | } | |||
A internal/infrastructure/persistence/sqlite/sqlc/querier.go +60 -0
| @@ -0,0 +1,60 @@ | |||
| 1 | + | // Code generated by sqlc. DO NOT EDIT. | |
| 2 | + | // versions: | |
| 3 | + | // sqlc v1.30.0 | |
| 4 | + | ||
| 5 | + | package sqlc | |
| 6 | + | ||
| 7 | + | import ( | |
| 8 | + | "context" | |
| 9 | + | "database/sql" | |
| 10 | + | ) | |
| 11 | + | ||
| 12 | + | type Querier interface { | |
| 13 | + | AddToWhitelist(ctx context.Context, domain string) error | |
| 14 | + | AssignTagToFavorite(ctx context.Context, arg AssignTagToFavoriteParams) error | |
| 15 | + | CreateFavorite(ctx context.Context, arg CreateFavoriteParams) (Favorite, error) | |
| 16 | + | CreateFolder(ctx context.Context, arg CreateFolderParams) (FavoriteFolder, error) | |
| 17 | + | CreateTag(ctx context.Context, arg CreateTagParams) (FavoriteTag, error) | |
| 18 | + | DeleteAllHistory(ctx context.Context) error | |
| 19 | + | DeleteFavorite(ctx context.Context, id int64) error | |
| 20 | + | DeleteFolder(ctx context.Context, id int64) error | |
| 21 | + | DeleteHistoryByID(ctx context.Context, id int64) error | |
| 22 | + | DeleteHistoryOlderThan(ctx context.Context, lastVisited sql.NullTime) error | |
| 23 | + | DeleteTag(ctx context.Context, id int64) error | |
| 24 | + | DeleteZoomLevel(ctx context.Context, domain string) error | |
| 25 | + | GetAllFavorites(ctx context.Context) ([]Favorite, error) | |
| 26 | + | GetAllFolders(ctx context.Context) ([]FavoriteFolder, error) | |
| 27 | + | GetAllTags(ctx context.Context) ([]FavoriteTag, error) | |
| 28 | + | GetAllWhitelistedDomains(ctx context.Context) ([]string, error) | |
| 29 | + | GetChildFolders(ctx context.Context, parentID sql.NullInt64) ([]FavoriteFolder, error) | |
| 30 | + | GetFavoriteByID(ctx context.Context, id int64) (Favorite, error) | |
| 31 | + | GetFavoriteByShortcut(ctx context.Context, shortcutKey sql.NullInt64) (Favorite, error) | |
| 32 | + | GetFavoriteByURL(ctx context.Context, url string) (Favorite, error) | |
| 33 | + | GetFavoritesByFolder(ctx context.Context, folderID sql.NullInt64) ([]Favorite, error) | |
| 34 | + | GetFavoritesWithoutFolder(ctx context.Context) ([]Favorite, error) | |
| 35 | + | GetFolderByID(ctx context.Context, id int64) (FavoriteFolder, error) | |
| 36 | + | GetHistoryByURL(ctx context.Context, url string) (History, error) | |
| 37 | + | GetRecentHistory(ctx context.Context, arg GetRecentHistoryParams) ([]History, error) | |
| 38 | + | GetRootFolders(ctx context.Context) ([]FavoriteFolder, error) | |
| 39 | + | GetTagByID(ctx context.Context, id int64) (FavoriteTag, error) | |
| 40 | + | GetTagByName(ctx context.Context, name string) (FavoriteTag, error) | |
| 41 | + | GetTagsForFavorite(ctx context.Context, favoriteID int64) ([]FavoriteTag, error) | |
| 42 | + | GetZoomLevel(ctx context.Context, domain string) (ZoomLevel, error) | |
| 43 | + | IncrementVisitCount(ctx context.Context, url string) error | |
| 44 | + | IsWhitelisted(ctx context.Context, domain string) (int64, error) | |
| 45 | + | ListZoomLevels(ctx context.Context) ([]ZoomLevel, error) | |
| 46 | + | RemoveFromWhitelist(ctx context.Context, domain string) error | |
| 47 | + | RemoveTagFromFavorite(ctx context.Context, arg RemoveTagFromFavoriteParams) error | |
| 48 | + | SearchHistory(ctx context.Context, arg SearchHistoryParams) ([]History, error) | |
| 49 | + | SetFavoriteFolder(ctx context.Context, arg SetFavoriteFolderParams) error | |
| 50 | + | SetFavoriteShortcut(ctx context.Context, arg SetFavoriteShortcutParams) error | |
| 51 | + | SetZoomLevel(ctx context.Context, arg SetZoomLevelParams) error | |
| 52 | + | UpdateFavorite(ctx context.Context, arg UpdateFavoriteParams) error | |
| 53 | + | UpdateFavoritePosition(ctx context.Context, arg UpdateFavoritePositionParams) error | |
| 54 | + | UpdateFolder(ctx context.Context, arg UpdateFolderParams) error | |
| 55 | + | UpdateFolderPosition(ctx context.Context, arg UpdateFolderPositionParams) error | |
| 56 | + | UpdateTag(ctx context.Context, arg UpdateTagParams) error | |
| 57 | + | UpsertHistory(ctx context.Context, arg UpsertHistoryParams) error | |
| 58 | + | } | |
| 59 | + | ||
| 60 | + | var _ Querier = (*Queries)(nil) | |
| @@ -0,0 +1,60 @@ | |||||
| 1 | + | // Code generated by sqlc. DO NOT EDIT. | |||
| 2 | + | // versions: | |||
| 3 | + | // sqlc v1.30.0 | |||
| 4 | + | ||||
| 5 | + | package sqlc | |||
| 6 | + | ||||
| 7 | + | import ( | |||
| 8 | + | "context" | |||
| 9 | + | "database/sql" | |||
| 10 | + | ) | |||
| 11 | + | ||||
| 12 | + | type Querier interface { | |||
| 13 | + | AddToWhitelist(ctx context.Context, domain string) error | |||
| 14 | + | AssignTagToFavorite(ctx context.Context, arg AssignTagToFavoriteParams) error | |||
| 15 | + | CreateFavorite(ctx context.Context, arg CreateFavoriteParams) (Favorite, error) | |||
| 16 | + | CreateFolder(ctx context.Context, arg CreateFolderParams) (FavoriteFolder, error) | |||
| 17 | + | CreateTag(ctx context.Context, arg CreateTagParams) (FavoriteTag, error) | |||
| 18 | + | DeleteAllHistory(ctx context.Context) error | |||
| 19 | + | DeleteFavorite(ctx context.Context, id int64) error | |||
| 20 | + | DeleteFolder(ctx context.Context, id int64) error | |||
| 21 | + | DeleteHistoryByID(ctx context.Context, id int64) error | |||
| 22 | + | DeleteHistoryOlderThan(ctx context.Context, lastVisited sql.NullTime) error | |||
| 23 | + | DeleteTag(ctx context.Context, id int64) error | |||
| 24 | + | DeleteZoomLevel(ctx context.Context, domain string) error | |||
| 25 | + | GetAllFavorites(ctx context.Context) ([]Favorite, error) | |||
| 26 | + | GetAllFolders(ctx context.Context) ([]FavoriteFolder, error) | |||
| 27 | + | GetAllTags(ctx context.Context) ([]FavoriteTag, error) | |||
| 28 | + | GetAllWhitelistedDomains(ctx context.Context) ([]string, error) | |||
| 29 | + | GetChildFolders(ctx context.Context, parentID sql.NullInt64) ([]FavoriteFolder, error) | |||
| 30 | + | GetFavoriteByID(ctx context.Context, id int64) (Favorite, error) | |||
| 31 | + | GetFavoriteByShortcut(ctx context.Context, shortcutKey sql.NullInt64) (Favorite, error) | |||
| 32 | + | GetFavoriteByURL(ctx context.Context, url string) (Favorite, error) | |||
| 33 | + | GetFavoritesByFolder(ctx context.Context, folderID sql.NullInt64) ([]Favorite, error) | |||
| 34 | + | GetFavoritesWithoutFolder(ctx context.Context) ([]Favorite, error) | |||
| 35 | + | GetFolderByID(ctx context.Context, id int64) (FavoriteFolder, error) | |||
| 36 | + | GetHistoryByURL(ctx context.Context, url string) (History, error) | |||
| 37 | + | GetRecentHistory(ctx context.Context, arg GetRecentHistoryParams) ([]History, error) | |||
| 38 | + | GetRootFolders(ctx context.Context) ([]FavoriteFolder, error) | |||
| 39 | + | GetTagByID(ctx context.Context, id int64) (FavoriteTag, error) | |||
| 40 | + | GetTagByName(ctx context.Context, name string) (FavoriteTag, error) | |||
| 41 | + | GetTagsForFavorite(ctx context.Context, favoriteID int64) ([]FavoriteTag, error) | |||
| 42 | + | GetZoomLevel(ctx context.Context, domain string) (ZoomLevel, error) | |||
| 43 | + | IncrementVisitCount(ctx context.Context, url string) error | |||
| 44 | + | IsWhitelisted(ctx context.Context, domain string) (int64, error) | |||
| 45 | + | ListZoomLevels(ctx context.Context) ([]ZoomLevel, error) | |||
| 46 | + | RemoveFromWhitelist(ctx context.Context, domain string) error | |||
| 47 | + | RemoveTagFromFavorite(ctx context.Context, arg RemoveTagFromFavoriteParams) error | |||
| 48 | + | SearchHistory(ctx context.Context, arg SearchHistoryParams) ([]History, error) | |||
| 49 | + | SetFavoriteFolder(ctx context.Context, arg SetFavoriteFolderParams) error | |||
| 50 | + | SetFavoriteShortcut(ctx context.Context, arg SetFavoriteShortcutParams) error | |||
| 51 | + | SetZoomLevel(ctx context.Context, arg SetZoomLevelParams) error | |||
| 52 | + | UpdateFavorite(ctx context.Context, arg UpdateFavoriteParams) error | |||
| 53 | + | UpdateFavoritePosition(ctx context.Context, arg UpdateFavoritePositionParams) error | |||
| 54 | + | UpdateFolder(ctx context.Context, arg UpdateFolderParams) error | |||
| 55 | + | UpdateFolderPosition(ctx context.Context, arg UpdateFolderPositionParams) error | |||
| 56 | + | UpdateTag(ctx context.Context, arg UpdateTagParams) error | |||
| 57 | + | UpsertHistory(ctx context.Context, arg UpsertHistoryParams) error | |||
| 58 | + | } | |||
| 59 | + | ||||
| 60 | + | var _ Querier = (*Queries)(nil) | |||
A internal/infrastructure/persistence/sqlite/sqlc/tags.sql.go +182 -0
| @@ -0,0 +1,182 @@ | |||
| 1 | + | // Code generated by sqlc. DO NOT EDIT. | |
| 2 | + | // versions: | |
| 3 | + | // sqlc v1.30.0 | |
| 4 | + | // source: tags.sql | |
| 5 | + | ||
| 6 | + | package sqlc | |
| 7 | + | ||
| 8 | + | import ( | |
| 9 | + | "context" | |
| 10 | + | ) | |
| 11 | + | ||
| 12 | + | const AssignTagToFavorite = `-- name: AssignTagToFavorite :exec | |
| 13 | + | INSERT OR IGNORE INTO favorite_tag_assignments (favorite_id, tag_id) VALUES (?, ?) | |
| 14 | + | ` | |
| 15 | + | ||
| 16 | + | type AssignTagToFavoriteParams struct { | |
| 17 | + | FavoriteID int64 `json:"favorite_id"` | |
| 18 | + | TagID int64 `json:"tag_id"` | |
| 19 | + | } | |
| 20 | + | ||
| 21 | + | func (q *Queries) AssignTagToFavorite(ctx context.Context, arg AssignTagToFavoriteParams) error { | |
| 22 | + | _, err := q.db.ExecContext(ctx, AssignTagToFavorite, arg.FavoriteID, arg.TagID) | |
| 23 | + | return err | |
| 24 | + | } | |
| 25 | + | ||
| 26 | + | const CreateTag = `-- name: CreateTag :one | |
| 27 | + | INSERT INTO favorite_tags (name, color) VALUES (?, ?) RETURNING id, name, color, created_at | |
| 28 | + | ` | |
| 29 | + | ||
| 30 | + | type CreateTagParams struct { | |
| 31 | + | Name string `json:"name"` | |
| 32 | + | Color string `json:"color"` | |
| 33 | + | } | |
| 34 | + | ||
| 35 | + | func (q *Queries) CreateTag(ctx context.Context, arg CreateTagParams) (FavoriteTag, error) { | |
| 36 | + | row := q.db.QueryRowContext(ctx, CreateTag, arg.Name, arg.Color) | |
| 37 | + | var i FavoriteTag | |
| 38 | + | err := row.Scan( | |
| 39 | + | &i.ID, | |
| 40 | + | &i.Name, | |
| 41 | + | &i.Color, | |
| 42 | + | &i.CreatedAt, | |
| 43 | + | ) | |
| 44 | + | return i, err | |
| 45 | + | } | |
| 46 | + | ||
| 47 | + | const DeleteTag = `-- name: DeleteTag :exec | |
| 48 | + | DELETE FROM favorite_tags WHERE id = ? | |
| 49 | + | ` | |
| 50 | + | ||
| 51 | + | func (q *Queries) DeleteTag(ctx context.Context, id int64) error { | |
| 52 | + | _, err := q.db.ExecContext(ctx, DeleteTag, id) | |
| 53 | + | return err | |
| 54 | + | } | |
| 55 | + | ||
| 56 | + | const GetAllTags = `-- name: GetAllTags :many | |
| 57 | + | SELECT id, name, color, created_at FROM favorite_tags ORDER BY name ASC | |
| 58 | + | ` | |
| 59 | + | ||
| 60 | + | func (q *Queries) GetAllTags(ctx context.Context) ([]FavoriteTag, error) { | |
| 61 | + | rows, err := q.db.QueryContext(ctx, GetAllTags) | |
| 62 | + | if err != nil { | |
| 63 | + | return nil, err | |
| 64 | + | } | |
| 65 | + | defer rows.Close() | |
| 66 | + | items := []FavoriteTag{} | |
| 67 | + | for rows.Next() { | |
| 68 | + | var i FavoriteTag | |
| 69 | + | if err := rows.Scan( | |
| 70 | + | &i.ID, | |
| 71 | + | &i.Name, | |
| 72 | + | &i.Color, | |
| 73 | + | &i.CreatedAt, | |
| 74 | + | ); err != nil { | |
| 75 | + | return nil, err | |
| 76 | + | } | |
| 77 | + | items = append(items, i) | |
| 78 | + | } | |
| 79 | + | if err := rows.Close(); err != nil { | |
| 80 | + | return nil, err | |
| 81 | + | } | |
| 82 | + | if err := rows.Err(); err != nil { | |
| 83 | + | return nil, err | |
| 84 | + | } | |
| 85 | + | return items, nil | |
| 86 | + | } | |
| 87 | + | ||
| 88 | + | const GetTagByID = `-- name: GetTagByID :one | |
| 89 | + | SELECT id, name, color, created_at FROM favorite_tags WHERE id = ? LIMIT 1 | |
| 90 | + | ` | |
| 91 | + | ||
| 92 | + | func (q *Queries) GetTagByID(ctx context.Context, id int64) (FavoriteTag, error) { | |
| 93 | + | row := q.db.QueryRowContext(ctx, GetTagByID, id) | |
| 94 | + | var i FavoriteTag | |
| 95 | + | err := row.Scan( | |
| 96 | + | &i.ID, | |
| 97 | + | &i.Name, | |
| 98 | + | &i.Color, | |
| 99 | + | &i.CreatedAt, | |
| 100 | + | ) | |
| 101 | + | return i, err | |
| 102 | + | } | |
| 103 | + | ||
| 104 | + | const GetTagByName = `-- name: GetTagByName :one | |
| 105 | + | SELECT id, name, color, created_at FROM favorite_tags WHERE name = ? LIMIT 1 | |
| 106 | + | ` | |
| 107 | + | ||
| 108 | + | func (q *Queries) GetTagByName(ctx context.Context, name string) (FavoriteTag, error) { | |
| 109 | + | row := q.db.QueryRowContext(ctx, GetTagByName, name) | |
| 110 | + | var i FavoriteTag | |
| 111 | + | err := row.Scan( | |
| 112 | + | &i.ID, | |
| 113 | + | &i.Name, | |
| 114 | + | &i.Color, | |
| 115 | + | &i.CreatedAt, | |
| 116 | + | ) | |
| 117 | + | return i, err | |
| 118 | + | } | |
| 119 | + | ||
| 120 | + | const GetTagsForFavorite = `-- name: GetTagsForFavorite :many | |
| 121 | + | SELECT t.id, t.name, t.color, t.created_at FROM favorite_tags t | |
| 122 | + | INNER JOIN favorite_tag_assignments fta ON t.id = fta.tag_id | |
| 123 | + | WHERE fta.favorite_id = ? | |
| 124 | + | ORDER BY t.name ASC | |
| 125 | + | ` | |
| 126 | + | ||
| 127 | + | func (q *Queries) GetTagsForFavorite(ctx context.Context, favoriteID int64) ([]FavoriteTag, error) { | |
| 128 | + | rows, err := q.db.QueryContext(ctx, GetTagsForFavorite, favoriteID) | |
| 129 | + | if err != nil { | |
| 130 | + | return nil, err | |
| 131 | + | } | |
| 132 | + | defer rows.Close() | |
| 133 | + | items := []FavoriteTag{} | |
| 134 | + | for rows.Next() { | |
| 135 | + | var i FavoriteTag | |
| 136 | + | if err := rows.Scan( | |
| 137 | + | &i.ID, | |
| 138 | + | &i.Name, | |
| 139 | + | &i.Color, | |
| 140 | + | &i.CreatedAt, | |
| 141 | + | ); err != nil { | |
| 142 | + | return nil, err | |
| 143 | + | } | |
| 144 | + | items = append(items, i) | |
| 145 | + | } | |
| 146 | + | if err := rows.Close(); err != nil { | |
| 147 | + | return nil, err | |
| 148 | + | } | |
| 149 | + | if err := rows.Err(); err != nil { | |
| 150 | + | return nil, err | |
| 151 | + | } | |
| 152 | + | return items, nil | |
| 153 | + | } | |
| 154 | + | ||
| 155 | + | const RemoveTagFromFavorite = `-- name: RemoveTagFromFavorite :exec | |
| 156 | + | DELETE FROM favorite_tag_assignments WHERE favorite_id = ? AND tag_id = ? | |
| 157 | + | ` | |
| 158 | + | ||
| 159 | + | type RemoveTagFromFavoriteParams struct { | |
| 160 | + | FavoriteID int64 `json:"favorite_id"` | |
| 161 | + | TagID int64 `json:"tag_id"` | |
| 162 | + | } | |
| 163 | + | ||
| 164 | + | func (q *Queries) RemoveTagFromFavorite(ctx context.Context, arg RemoveTagFromFavoriteParams) error { | |
| 165 | + | _, err := q.db.ExecContext(ctx, RemoveTagFromFavorite, arg.FavoriteID, arg.TagID) | |
| 166 | + | return err | |
| 167 | + | } | |
| 168 | + | ||
| 169 | + | const UpdateTag = `-- name: UpdateTag :exec | |
| 170 | + | UPDATE favorite_tags SET name = ?, color = ? WHERE id = ? | |
| 171 | + | ` | |
| 172 | + | ||
| 173 | + | type UpdateTagParams struct { | |
| 174 | + | Name string `json:"name"` | |
| 175 | + | Color string `json:"color"` | |
| 176 | + | ID int64 `json:"id"` | |
| 177 | + | } | |
| 178 | + | ||
| 179 | + | func (q *Queries) UpdateTag(ctx context.Context, arg UpdateTagParams) error { | |
| 180 | + | _, err := q.db.ExecContext(ctx, UpdateTag, arg.Name, arg.Color, arg.ID) | |
| 181 | + | return err | |
| 182 | + | } | |
| @@ -0,0 +1,182 @@ | |||||
| 1 | + | // Code generated by sqlc. DO NOT EDIT. | |||
| 2 | + | // versions: | |||
| 3 | + | // sqlc v1.30.0 | |||
| 4 | + | // source: tags.sql | |||
| 5 | + | ||||
| 6 | + | package sqlc | |||
| 7 | + | ||||
| 8 | + | import ( | |||
| 9 | + | "context" | |||
| 10 | + | ) | |||
| 11 | + | ||||
| 12 | + | const AssignTagToFavorite = `-- name: AssignTagToFavorite :exec | |||
| 13 | + | INSERT OR IGNORE INTO favorite_tag_assignments (favorite_id, tag_id) VALUES (?, ?) | |||
| 14 | + | ` | |||
| 15 | + | ||||
| 16 | + | type AssignTagToFavoriteParams struct { | |||
| 17 | + | FavoriteID int64 `json:"favorite_id"` | |||
| 18 | + | TagID int64 `json:"tag_id"` | |||
| 19 | + | } | |||
| 20 | + | ||||
| 21 | + | func (q *Queries) AssignTagToFavorite(ctx context.Context, arg AssignTagToFavoriteParams) error { | |||
| 22 | + | _, err := q.db.ExecContext(ctx, AssignTagToFavorite, arg.FavoriteID, arg.TagID) | |||
| 23 | + | return err | |||
| 24 | + | } | |||
| 25 | + | ||||
| 26 | + | const CreateTag = `-- name: CreateTag :one | |||
| 27 | + | INSERT INTO favorite_tags (name, color) VALUES (?, ?) RETURNING id, name, color, created_at | |||
| 28 | + | ` | |||
| 29 | + | ||||
| 30 | + | type CreateTagParams struct { | |||
| 31 | + | Name string `json:"name"` | |||
| 32 | + | Color string `json:"color"` | |||
| 33 | + | } | |||
| 34 | + | ||||
| 35 | + | func (q *Queries) CreateTag(ctx context.Context, arg CreateTagParams) (FavoriteTag, error) { | |||
| 36 | + | row := q.db.QueryRowContext(ctx, CreateTag, arg.Name, arg.Color) | |||
| 37 | + | var i FavoriteTag | |||
| 38 | + | err := row.Scan( | |||
| 39 | + | &i.ID, | |||
| 40 | + | &i.Name, | |||
| 41 | + | &i.Color, | |||
| 42 | + | &i.CreatedAt, | |||
| 43 | + | ) | |||
| 44 | + | return i, err | |||
| 45 | + | } | |||
| 46 | + | ||||
| 47 | + | const DeleteTag = `-- name: DeleteTag :exec | |||
| 48 | + | DELETE FROM favorite_tags WHERE id = ? | |||
| 49 | + | ` | |||
| 50 | + | ||||
| 51 | + | func (q *Queries) DeleteTag(ctx context.Context, id int64) error { | |||
| 52 | + | _, err := q.db.ExecContext(ctx, DeleteTag, id) | |||
| 53 | + | return err | |||
| 54 | + | } | |||
| 55 | + | ||||
| 56 | + | const GetAllTags = `-- name: GetAllTags :many | |||
| 57 | + | SELECT id, name, color, created_at FROM favorite_tags ORDER BY name ASC | |||
| 58 | + | ` | |||
| 59 | + | ||||
| 60 | + | func (q *Queries) GetAllTags(ctx context.Context) ([]FavoriteTag, error) { | |||
| 61 | + | rows, err := q.db.QueryContext(ctx, GetAllTags) | |||
| 62 | + | if err != nil { | |||
| 63 | + | return nil, err | |||
| 64 | + | } | |||
| 65 | + | defer rows.Close() | |||
| 66 | + | items := []FavoriteTag{} | |||
| 67 | + | for rows.Next() { | |||
| 68 | + | var i FavoriteTag | |||
| 69 | + | if err := rows.Scan( | |||
| 70 | + | &i.ID, | |||
| 71 | + | &i.Name, | |||
| 72 | + | &i.Color, | |||
| 73 | + | &i.CreatedAt, | |||
| 74 | + | ); err != nil { | |||
| 75 | + | return nil, err | |||
| 76 | + | } | |||
| 77 | + | items = append(items, i) | |||
| 78 | + | } | |||
| 79 | + | if err := rows.Close(); err != nil { | |||
| 80 | + | return nil, err | |||
| 81 | + | } | |||
| 82 | + | if err := rows.Err(); err != nil { | |||
| 83 | + | return nil, err | |||
| 84 | + | } | |||
| 85 | + | return items, nil | |||
| 86 | + | } | |||
| 87 | + | ||||
| 88 | + | const GetTagByID = `-- name: GetTagByID :one | |||
| 89 | + | SELECT id, name, color, created_at FROM favorite_tags WHERE id = ? LIMIT 1 | |||
| 90 | + | ` | |||
| 91 | + | ||||
| 92 | + | func (q *Queries) GetTagByID(ctx context.Context, id int64) (FavoriteTag, error) { | |||
| 93 | + | row := q.db.QueryRowContext(ctx, GetTagByID, id) | |||
| 94 | + | var i FavoriteTag | |||
| 95 | + | err := row.Scan( | |||
| 96 | + | &i.ID, | |||
| 97 | + | &i.Name, | |||
| 98 | + | &i.Color, | |||
| 99 | + | &i.CreatedAt, | |||
| 100 | + | ) | |||
| 101 | + | return i, err | |||
| 102 | + | } | |||
| 103 | + | ||||
| 104 | + | const GetTagByName = `-- name: GetTagByName :one | |||
| 105 | + | SELECT id, name, color, created_at FROM favorite_tags WHERE name = ? LIMIT 1 | |||
| 106 | + | ` | |||
| 107 | + | ||||
| 108 | + | func (q *Queries) GetTagByName(ctx context.Context, name string) (FavoriteTag, error) { | |||
| 109 | + | row := q.db.QueryRowContext(ctx, GetTagByName, name) | |||
| 110 | + | var i FavoriteTag | |||
| 111 | + | err := row.Scan( | |||
| 112 | + | &i.ID, | |||
| 113 | + | &i.Name, | |||
| 114 | + | &i.Color, | |||
| 115 | + | &i.CreatedAt, | |||
| 116 | + | ) | |||
| 117 | + | return i, err | |||
| 118 | + | } | |||
| 119 | + | ||||
| 120 | + | const GetTagsForFavorite = `-- name: GetTagsForFavorite :many | |||
| 121 | + | SELECT t.id, t.name, t.color, t.created_at FROM favorite_tags t | |||
| 122 | + | INNER JOIN favorite_tag_assignments fta ON t.id = fta.tag_id | |||
| 123 | + | WHERE fta.favorite_id = ? | |||
| 124 | + | ORDER BY t.name ASC | |||
| 125 | + | ` | |||
| 126 | + | ||||
| 127 | + | func (q *Queries) GetTagsForFavorite(ctx context.Context, favoriteID int64) ([]FavoriteTag, error) { | |||
| 128 | + | rows, err := q.db.QueryContext(ctx, GetTagsForFavorite, favoriteID) | |||
| 129 | + | if err != nil { | |||
| 130 | + | return nil, err | |||
| 131 | + | } | |||
| 132 | + | defer rows.Close() | |||
| 133 | + | items := []FavoriteTag{} | |||
| 134 | + | for rows.Next() { | |||
| 135 | + | var i FavoriteTag | |||
| 136 | + | if err := rows.Scan( | |||
| 137 | + | &i.ID, | |||
| 138 | + | &i.Name, | |||
| 139 | + | &i.Color, | |||
| 140 | + | &i.CreatedAt, | |||
| 141 | + | ); err != nil { | |||
| 142 | + | return nil, err | |||
| 143 | + | } | |||
| 144 | + | items = append(items, i) | |||
| 145 | + | } | |||
| 146 | + | if err := rows.Close(); err != nil { | |||
| 147 | + | return nil, err | |||
| 148 | + | } | |||
| 149 | + | if err := rows.Err(); err != nil { | |||
| 150 | + | return nil, err | |||
| 151 | + | } | |||
| 152 | + | return items, nil | |||
| 153 | + | } | |||
| 154 | + | ||||
| 155 | + | const RemoveTagFromFavorite = `-- name: RemoveTagFromFavorite :exec | |||
| 156 | + | DELETE FROM favorite_tag_assignments WHERE favorite_id = ? AND tag_id = ? | |||
| 157 | + | ` | |||
| 158 | + | ||||
| 159 | + | type RemoveTagFromFavoriteParams struct { | |||
| 160 | + | FavoriteID int64 `json:"favorite_id"` | |||
| 161 | + | TagID int64 `json:"tag_id"` | |||
| 162 | + | } | |||
| 163 | + | ||||
| 164 | + | func (q *Queries) RemoveTagFromFavorite(ctx context.Context, arg RemoveTagFromFavoriteParams) error { | |||
| 165 | + | _, err := q.db.ExecContext(ctx, RemoveTagFromFavorite, arg.FavoriteID, arg.TagID) | |||
| 166 | + | return err | |||
| 167 | + | } | |||
| 168 | + | ||||
| 169 | + | const UpdateTag = `-- name: UpdateTag :exec | |||
| 170 | + | UPDATE favorite_tags SET name = ?, color = ? WHERE id = ? | |||
| 171 | + | ` | |||
| 172 | + | ||||
| 173 | + | type UpdateTagParams struct { | |||
| 174 | + | Name string `json:"name"` | |||
| 175 | + | Color string `json:"color"` | |||
| 176 | + | ID int64 `json:"id"` | |||
| 177 | + | } | |||
| 178 | + | ||||
| 179 | + | func (q *Queries) UpdateTag(ctx context.Context, arg UpdateTagParams) error { | |||
| 180 | + | _, err := q.db.ExecContext(ctx, UpdateTag, arg.Name, arg.Color, arg.ID) | |||
| 181 | + | return err | |||
| 182 | + | } | |||
A internal/infrastructure/persistence/sqlite/sqlc/whitelist.sql.go +66 -0
| @@ -0,0 +1,66 @@ | |||
| 1 | + | // Code generated by sqlc. DO NOT EDIT. | |
| 2 | + | // versions: | |
| 3 | + | // sqlc v1.30.0 | |
| 4 | + | // source: whitelist.sql | |
| 5 | + | ||
| 6 | + | package sqlc | |
| 7 | + | ||
| 8 | + | import ( | |
| 9 | + | "context" | |
| 10 | + | ) | |
| 11 | + | ||
| 12 | + | const AddToWhitelist = `-- name: AddToWhitelist :exec | |
| 13 | + | INSERT OR IGNORE INTO content_whitelist (domain, created_at) VALUES (?, CURRENT_TIMESTAMP) | |
| 14 | + | ` | |
| 15 | + | ||
| 16 | + | func (q *Queries) AddToWhitelist(ctx context.Context, domain string) error { | |
| 17 | + | _, err := q.db.ExecContext(ctx, AddToWhitelist, domain) | |
| 18 | + | return err | |
| 19 | + | } | |
| 20 | + | ||
| 21 | + | const GetAllWhitelistedDomains = `-- name: GetAllWhitelistedDomains :many | |
| 22 | + | SELECT domain FROM content_whitelist ORDER BY domain | |
| 23 | + | ` | |
| 24 | + | ||
| 25 | + | func (q *Queries) GetAllWhitelistedDomains(ctx context.Context) ([]string, error) { | |
| 26 | + | rows, err := q.db.QueryContext(ctx, GetAllWhitelistedDomains) | |
| 27 | + | if err != nil { | |
| 28 | + | return nil, err | |
| 29 | + | } | |
| 30 | + | defer rows.Close() | |
| 31 | + | items := []string{} | |
| 32 | + | for rows.Next() { | |
| 33 | + | var domain string | |
| 34 | + | if err := rows.Scan(&domain); err != nil { | |
| 35 | + | return nil, err | |
| 36 | + | } | |
| 37 | + | items = append(items, domain) | |
| 38 | + | } | |
| 39 | + | if err := rows.Close(); err != nil { | |
| 40 | + | return nil, err | |
| 41 | + | } | |
| 42 | + | if err := rows.Err(); err != nil { | |
| 43 | + | return nil, err | |
| 44 | + | } | |
| 45 | + | return items, nil | |
| 46 | + | } | |
| 47 | + | ||
| 48 | + | const IsWhitelisted = `-- name: IsWhitelisted :one | |
| 49 | + | SELECT COUNT(*) FROM content_whitelist WHERE domain = ? LIMIT 1 | |
| 50 | + | ` | |
| 51 | + | ||
| 52 | + | func (q *Queries) IsWhitelisted(ctx context.Context, domain string) (int64, error) { | |
| 53 | + | row := q.db.QueryRowContext(ctx, IsWhitelisted, domain) | |
| 54 | + | var count int64 | |
| 55 | + | err := row.Scan(&count) | |
| 56 | + | return count, err | |
| 57 | + | } | |
| 58 | + | ||
| 59 | + | const RemoveFromWhitelist = `-- name: RemoveFromWhitelist :exec | |
| 60 | + | DELETE FROM content_whitelist WHERE domain = ? | |
| 61 | + | ` | |
| 62 | + | ||
| 63 | + | func (q *Queries) RemoveFromWhitelist(ctx context.Context, domain string) error { | |
| 64 | + | _, err := q.db.ExecContext(ctx, RemoveFromWhitelist, domain) | |
| 65 | + | return err | |
| 66 | + | } | |
| @@ -0,0 +1,66 @@ | |||||
| 1 | + | // Code generated by sqlc. DO NOT EDIT. | |||
| 2 | + | // versions: | |||
| 3 | + | // sqlc v1.30.0 | |||
| 4 | + | // source: whitelist.sql | |||
| 5 | + | ||||
| 6 | + | package sqlc | |||
| 7 | + | ||||
| 8 | + | import ( | |||
| 9 | + | "context" | |||
| 10 | + | ) | |||
| 11 | + | ||||
| 12 | + | const AddToWhitelist = `-- name: AddToWhitelist :exec | |||
| 13 | + | INSERT OR IGNORE INTO content_whitelist (domain, created_at) VALUES (?, CURRENT_TIMESTAMP) | |||
| 14 | + | ` | |||
| 15 | + | ||||
| 16 | + | func (q *Queries) AddToWhitelist(ctx context.Context, domain string) error { | |||
| 17 | + | _, err := q.db.ExecContext(ctx, AddToWhitelist, domain) | |||
| 18 | + | return err | |||
| 19 | + | } | |||
| 20 | + | ||||
| 21 | + | const GetAllWhitelistedDomains = `-- name: GetAllWhitelistedDomains :many | |||
| 22 | + | SELECT domain FROM content_whitelist ORDER BY domain | |||
| 23 | + | ` | |||
| 24 | + | ||||
| 25 | + | func (q *Queries) GetAllWhitelistedDomains(ctx context.Context) ([]string, error) { | |||
| 26 | + | rows, err := q.db.QueryContext(ctx, GetAllWhitelistedDomains) | |||
| 27 | + | if err != nil { | |||
| 28 | + | return nil, err | |||
| 29 | + | } | |||
| 30 | + | defer rows.Close() | |||
| 31 | + | items := []string{} | |||
| 32 | + | for rows.Next() { | |||
| 33 | + | var domain string | |||
| 34 | + | if err := rows.Scan(&domain); err != nil { | |||
| 35 | + | return nil, err | |||
| 36 | + | } | |||
| 37 | + | items = append(items, domain) | |||
| 38 | + | } | |||
| 39 | + | if err := rows.Close(); err != nil { | |||
| 40 | + | return nil, err | |||
| 41 | + | } | |||
| 42 | + | if err := rows.Err(); err != nil { | |||
| 43 | + | return nil, err | |||
| 44 | + | } | |||
| 45 | + | return items, nil | |||
| 46 | + | } | |||
| 47 | + | ||||
| 48 | + | const IsWhitelisted = `-- name: IsWhitelisted :one | |||
| 49 | + | SELECT COUNT(*) FROM content_whitelist WHERE domain = ? LIMIT 1 | |||
| 50 | + | ` | |||
| 51 | + | ||||
| 52 | + | func (q *Queries) IsWhitelisted(ctx context.Context, domain string) (int64, error) { | |||
| 53 | + | row := q.db.QueryRowContext(ctx, IsWhitelisted, domain) | |||
| 54 | + | var count int64 | |||
| 55 | + | err := row.Scan(&count) | |||
| 56 | + | return count, err | |||
| 57 | + | } | |||
| 58 | + | ||||
| 59 | + | const RemoveFromWhitelist = `-- name: RemoveFromWhitelist :exec | |||
| 60 | + | DELETE FROM content_whitelist WHERE domain = ? | |||
| 61 | + | ` | |||
| 62 | + | ||||
| 63 | + | func (q *Queries) RemoveFromWhitelist(ctx context.Context, domain string) error { | |||
| 64 | + | _, err := q.db.ExecContext(ctx, RemoveFromWhitelist, domain) | |||
| 65 | + | return err | |||
| 66 | + | } | |||
A internal/infrastructure/persistence/sqlite/sqlc/zoom.sql.go +75 -0
| @@ -0,0 +1,75 @@ | |||
| 1 | + | // Code generated by sqlc. DO NOT EDIT. | |
| 2 | + | // versions: | |
| 3 | + | // sqlc v1.30.0 | |
| 4 | + | // source: zoom.sql | |
| 5 | + | ||
| 6 | + | package sqlc | |
| 7 | + | ||
| 8 | + | import ( | |
| 9 | + | "context" | |
| 10 | + | ) | |
| 11 | + | ||
| 12 | + | const DeleteZoomLevel = `-- name: DeleteZoomLevel :exec | |
| 13 | + | DELETE FROM zoom_levels WHERE domain = ? | |
| 14 | + | ` | |
| 15 | + | ||
| 16 | + | func (q *Queries) DeleteZoomLevel(ctx context.Context, domain string) error { | |
| 17 | + | _, err := q.db.ExecContext(ctx, DeleteZoomLevel, domain) | |
| 18 | + | return err | |
| 19 | + | } | |
| 20 | + | ||
| 21 | + | const GetZoomLevel = `-- name: GetZoomLevel :one | |
| 22 | + | SELECT domain, zoom_factor, updated_at FROM zoom_levels WHERE domain = ? LIMIT 1 | |
| 23 | + | ` | |
| 24 | + | ||
| 25 | + | func (q *Queries) GetZoomLevel(ctx context.Context, domain string) (ZoomLevel, error) { | |
| 26 | + | row := q.db.QueryRowContext(ctx, GetZoomLevel, domain) | |
| 27 | + | var i ZoomLevel | |
| 28 | + | err := row.Scan(&i.Domain, &i.ZoomFactor, &i.UpdatedAt) | |
| 29 | + | return i, err | |
| 30 | + | } | |
| 31 | + | ||
| 32 | + | const ListZoomLevels = `-- name: ListZoomLevels :many | |
| 33 | + | SELECT domain, zoom_factor, updated_at FROM zoom_levels ORDER BY updated_at DESC | |
| 34 | + | ` | |
| 35 | + | ||
| 36 | + | func (q *Queries) ListZoomLevels(ctx context.Context) ([]ZoomLevel, error) { | |
| 37 | + | rows, err := q.db.QueryContext(ctx, ListZoomLevels) | |
| 38 | + | if err != nil { | |
| 39 | + | return nil, err | |
| 40 | + | } | |
| 41 | + | defer rows.Close() | |
| 42 | + | items := []ZoomLevel{} | |
| 43 | + | for rows.Next() { | |
| 44 | + | var i ZoomLevel | |
| 45 | + | if err := rows.Scan(&i.Domain, &i.ZoomFactor, &i.UpdatedAt); err != nil { | |
| 46 | + | return nil, err | |
| 47 | + | } | |
| 48 | + | items = append(items, i) | |
| 49 | + | } | |
| 50 | + | if err := rows.Close(); err != nil { | |
| 51 | + | return nil, err | |
| 52 | + | } | |
| 53 | + | if err := rows.Err(); err != nil { | |
| 54 | + | return nil, err | |
| 55 | + | } | |
| 56 | + | return items, nil | |
| 57 | + | } | |
| 58 | + | ||
| 59 | + | const SetZoomLevel = `-- name: SetZoomLevel :exec | |
| 60 | + | INSERT INTO zoom_levels (domain, zoom_factor, updated_at) | |
| 61 | + | VALUES (?, ?, CURRENT_TIMESTAMP) | |
| 62 | + | ON CONFLICT(domain) DO UPDATE SET | |
| 63 | + | zoom_factor = excluded.zoom_factor, | |
| 64 | + | updated_at = CURRENT_TIMESTAMP | |
| 65 | + | ` | |
| 66 | + | ||
| 67 | + | type SetZoomLevelParams struct { | |
| 68 | + | Domain string `json:"domain"` | |
| 69 | + | ZoomFactor float64 `json:"zoom_factor"` | |
| 70 | + | } | |
| 71 | + | ||
| 72 | + | func (q *Queries) SetZoomLevel(ctx context.Context, arg SetZoomLevelParams) error { | |
| 73 | + | _, err := q.db.ExecContext(ctx, SetZoomLevel, arg.Domain, arg.ZoomFactor) | |
| 74 | + | return err | |
| 75 | + | } | |
| @@ -0,0 +1,75 @@ | |||||
| 1 | + | // Code generated by sqlc. DO NOT EDIT. | |||
| 2 | + | // versions: | |||
| 3 | + | // sqlc v1.30.0 | |||
| 4 | + | // source: zoom.sql | |||
| 5 | + | ||||
| 6 | + | package sqlc | |||
| 7 | + | ||||
| 8 | + | import ( | |||
| 9 | + | "context" | |||
| 10 | + | ) | |||
| 11 | + | ||||
| 12 | + | const DeleteZoomLevel = `-- name: DeleteZoomLevel :exec | |||
| 13 | + | DELETE FROM zoom_levels WHERE domain = ? | |||
| 14 | + | ` | |||
| 15 | + | ||||
| 16 | + | func (q *Queries) DeleteZoomLevel(ctx context.Context, domain string) error { | |||
| 17 | + | _, err := q.db.ExecContext(ctx, DeleteZoomLevel, domain) | |||
| 18 | + | return err | |||
| 19 | + | } | |||
| 20 | + | ||||
| 21 | + | const GetZoomLevel = `-- name: GetZoomLevel :one | |||
| 22 | + | SELECT domain, zoom_factor, updated_at FROM zoom_levels WHERE domain = ? LIMIT 1 | |||
| 23 | + | ` | |||
| 24 | + | ||||
| 25 | + | func (q *Queries) GetZoomLevel(ctx context.Context, domain string) (ZoomLevel, error) { | |||
| 26 | + | row := q.db.QueryRowContext(ctx, GetZoomLevel, domain) | |||
| 27 | + | var i ZoomLevel | |||
| 28 | + | err := row.Scan(&i.Domain, &i.ZoomFactor, &i.UpdatedAt) | |||
| 29 | + | return i, err | |||
| 30 | + | } | |||
| 31 | + | ||||
| 32 | + | const ListZoomLevels = `-- name: ListZoomLevels :many | |||
| 33 | + | SELECT domain, zoom_factor, updated_at FROM zoom_levels ORDER BY updated_at DESC | |||
| 34 | + | ` | |||
| 35 | + | ||||
| 36 | + | func (q *Queries) ListZoomLevels(ctx context.Context) ([]ZoomLevel, error) { | |||
| 37 | + | rows, err := q.db.QueryContext(ctx, ListZoomLevels) | |||
| 38 | + | if err != nil { | |||
| 39 | + | return nil, err | |||
| 40 | + | } | |||
| 41 | + | defer rows.Close() | |||
| 42 | + | items := []ZoomLevel{} | |||
| 43 | + | for rows.Next() { | |||
| 44 | + | var i ZoomLevel | |||
| 45 | + | if err := rows.Scan(&i.Domain, &i.ZoomFactor, &i.UpdatedAt); err != nil { | |||
| 46 | + | return nil, err | |||
| 47 | + | } | |||
| 48 | + | items = append(items, i) | |||
| 49 | + | } | |||
| 50 | + | if err := rows.Close(); err != nil { | |||
| 51 | + | return nil, err | |||
| 52 | + | } | |||
| 53 | + | if err := rows.Err(); err != nil { | |||
| 54 | + | return nil, err | |||
| 55 | + | } | |||
| 56 | + | return items, nil | |||
| 57 | + | } | |||
| 58 | + | ||||
| 59 | + | const SetZoomLevel = `-- name: SetZoomLevel :exec | |||
| 60 | + | INSERT INTO zoom_levels (domain, zoom_factor, updated_at) | |||
| 61 | + | VALUES (?, ?, CURRENT_TIMESTAMP) | |||
| 62 | + | ON CONFLICT(domain) DO UPDATE SET | |||
| 63 | + | zoom_factor = excluded.zoom_factor, | |||
| 64 | + | updated_at = CURRENT_TIMESTAMP | |||
| 65 | + | ` | |||
| 66 | + | ||||
| 67 | + | type SetZoomLevelParams struct { | |||
| 68 | + | Domain string `json:"domain"` | |||
| 69 | + | ZoomFactor float64 `json:"zoom_factor"` | |||
| 70 | + | } | |||
| 71 | + | ||||
| 72 | + | func (q *Queries) SetZoomLevel(ctx context.Context, arg SetZoomLevelParams) error { | |||
| 73 | + | _, err := q.db.ExecContext(ctx, SetZoomLevel, arg.Domain, arg.ZoomFactor) | |||
| 74 | + | return err | |||
| 75 | + | } | |||
A internal/infrastructure/persistence/sqlite/zoom_repo.go +69 -0
| @@ -0,0 +1,69 @@ | |||
| 1 | + | package sqlite | |
| 2 | + | ||
| 3 | + | import ( | |
| 4 | + | "context" | |
| 5 | + | "database/sql" | |
| 6 | + | ||
| 7 | + | "github.com/bnema/dumber/internal/domain/entity" | |
| 8 | + | "github.com/bnema/dumber/internal/domain/repository" | |
| 9 | + | "github.com/bnema/dumber/internal/infrastructure/persistence/sqlite/sqlc" | |
| 10 | + | "github.com/bnema/dumber/internal/logging" | |
| 11 | + | ) | |
| 12 | + | ||
| 13 | + | type zoomRepo struct { | |
| 14 | + | queries *sqlc.Queries | |
| 15 | + | } | |
| 16 | + | ||
| 17 | + | // NewZoomRepository creates a new SQLite-backed zoom repository. | |
| 18 | + | func NewZoomRepository(db *sql.DB) repository.ZoomRepository { | |
| 19 | + | return &zoomRepo{queries: sqlc.New(db)} | |
| 20 | + | } | |
| 21 | + | ||
| 22 | + | func (r *zoomRepo) Get(ctx context.Context, domain string) (*entity.ZoomLevel, error) { | |
| 23 | + | log := logging.FromContext(ctx) | |
| 24 | + | log.Debug().Str("domain", domain).Msg("getting zoom level") | |
| 25 | + | ||
| 26 | + | row, err := r.queries.GetZoomLevel(ctx, domain) | |
| 27 | + | if err != nil { | |
| 28 | + | if err == sql.ErrNoRows { | |
| 29 | + | return nil, nil | |
| 30 | + | } | |
| 31 | + | return nil, err | |
| 32 | + | } | |
| 33 | + | return zoomFromRow(row), nil | |
| 34 | + | } | |
| 35 | + | ||
| 36 | + | func (r *zoomRepo) Set(ctx context.Context, level *entity.ZoomLevel) error { | |
| 37 | + | log := logging.FromContext(ctx) | |
| 38 | + | log.Debug().Str("domain", level.Domain).Float64("factor", level.ZoomFactor).Msg("setting zoom level") | |
| 39 | + | ||
| 40 | + | return r.queries.SetZoomLevel(ctx, sqlc.SetZoomLevelParams{ | |
| 41 | + | Domain: level.Domain, | |
| 42 | + | ZoomFactor: level.ZoomFactor, | |
| 43 | + | }) | |
| 44 | + | } | |
| 45 | + | ||
| 46 | + | func (r *zoomRepo) Delete(ctx context.Context, domain string) error { | |
| 47 | + | return r.queries.DeleteZoomLevel(ctx, domain) | |
| 48 | + | } | |
| 49 | + | ||
| 50 | + | func (r *zoomRepo) GetAll(ctx context.Context) ([]*entity.ZoomLevel, error) { | |
| 51 | + | rows, err := r.queries.ListZoomLevels(ctx) | |
| 52 | + | if err != nil { | |
| 53 | + | return nil, err | |
| 54 | + | } | |
| 55 | + | ||
| 56 | + | levels := make([]*entity.ZoomLevel, len(rows)) | |
| 57 | + | for i, row := range rows { | |
| 58 | + | levels[i] = zoomFromRow(row) | |
| 59 | + | } | |
| 60 | + | return levels, nil | |
| 61 | + | } | |
| 62 | + | ||
| 63 | + | func zoomFromRow(row sqlc.ZoomLevel) *entity.ZoomLevel { | |
| 64 | + | return &entity.ZoomLevel{ | |
| 65 | + | Domain: row.Domain, | |
| 66 | + | ZoomFactor: row.ZoomFactor, | |
| 67 | + | UpdatedAt: row.UpdatedAt.Time, | |
| 68 | + | } | |
| 69 | + | } | |
| @@ -0,0 +1,69 @@ | |||||
| 1 | + | package sqlite | |||
| 2 | + | ||||
| 3 | + | import ( | |||
| 4 | + | "context" | |||
| 5 | + | "database/sql" | |||
| 6 | + | ||||
| 7 | + | "github.com/bnema/dumber/internal/domain/entity" | |||
| 8 | + | "github.com/bnema/dumber/internal/domain/repository" | |||
| 9 | + | "github.com/bnema/dumber/internal/infrastructure/persistence/sqlite/sqlc" | |||
| 10 | + | "github.com/bnema/dumber/internal/logging" | |||
| 11 | + | ) | |||
| 12 | + | ||||
| 13 | + | type zoomRepo struct { | |||
| 14 | + | queries *sqlc.Queries | |||
| 15 | + | } | |||
| 16 | + | ||||
| 17 | + | // NewZoomRepository creates a new SQLite-backed zoom repository. | |||
| 18 | + | func NewZoomRepository(db *sql.DB) repository.ZoomRepository { | |||
| 19 | + | return &zoomRepo{queries: sqlc.New(db)} | |||
| 20 | + | } | |||
| 21 | + | ||||
| 22 | + | func (r *zoomRepo) Get(ctx context.Context, domain string) (*entity.ZoomLevel, error) { | |||
| 23 | + | log := logging.FromContext(ctx) | |||
| 24 | + | log.Debug().Str("domain", domain).Msg("getting zoom level") | |||
| 25 | + | ||||
| 26 | + | row, err := r.queries.GetZoomLevel(ctx, domain) | |||
| 27 | + | if err != nil { | |||
| 28 | + | if err == sql.ErrNoRows { | |||
| 29 | + | return nil, nil | |||
| 30 | + | } | |||
| 31 | + | return nil, err | |||
| 32 | + | } | |||
| 33 | + | return zoomFromRow(row), nil | |||
| 34 | + | } | |||
| 35 | + | ||||
| 36 | + | func (r *zoomRepo) Set(ctx context.Context, level *entity.ZoomLevel) error { | |||
| 37 | + | log := logging.FromContext(ctx) | |||
| 38 | + | log.Debug().Str("domain", level.Domain).Float64("factor", level.ZoomFactor).Msg("setting zoom level") | |||
| 39 | + | ||||
| 40 | + | return r.queries.SetZoomLevel(ctx, sqlc.SetZoomLevelParams{ | |||
| 41 | + | Domain: level.Domain, | |||
| 42 | + | ZoomFactor: level.ZoomFactor, | |||
| 43 | + | }) | |||
| 44 | + | } | |||
| 45 | + | ||||
| 46 | + | func (r *zoomRepo) Delete(ctx context.Context, domain string) error { | |||
| 47 | + | return r.queries.DeleteZoomLevel(ctx, domain) | |||
| 48 | + | } | |||
| 49 | + | ||||
| 50 | + | func (r *zoomRepo) GetAll(ctx context.Context) ([]*entity.ZoomLevel, error) { | |||
| 51 | + | rows, err := r.queries.ListZoomLevels(ctx) | |||
| 52 | + | if err != nil { | |||
| 53 | + | return nil, err | |||
| 54 | + | } | |||
| 55 | + | ||||
| 56 | + | levels := make([]*entity.ZoomLevel, len(rows)) | |||
| 57 | + | for i, row := range rows { | |||
| 58 | + | levels[i] = zoomFromRow(row) | |||
| 59 | + | } | |||
| 60 | + | return levels, nil | |||
| 61 | + | } | |||
| 62 | + | ||||
| 63 | + | func zoomFromRow(row sqlc.ZoomLevel) *entity.ZoomLevel { | |||
| 64 | + | return &entity.ZoomLevel{ | |||
| 65 | + | Domain: row.Domain, | |||
| 66 | + | ZoomFactor: row.ZoomFactor, | |||
| 67 | + | UpdatedAt: row.UpdatedAt.Time, | |||
| 68 | + | } | |||
| 69 | + | } | |||