From 72277446217471758169754dffac4d53734dbd21 Mon Sep 17 00:00:00 2001 From: James Tombleson Date: Sun, 28 Apr 2024 10:02:57 -0700 Subject: [PATCH] got the sources repo working --- .gitignore | 4 + .../migrations/20240425083756_init.sql | 17 +- .../migrations/20240425092459_seed.sql | 40 +-- internal/domain/const.go | 9 + internal/domain/entity.go | 40 +-- internal/repository/article.go | 11 +- internal/repository/article_test.go | 9 +- internal/repository/common.go | 71 +++++ internal/repository/discordWebHooks.go | 69 +---- internal/repository/source.go | 239 +++++++++++++++++ internal/repository/source_test.go | 246 ++++++++++++++++++ internal/services/database.go | 6 + internal/services/input/rss.go | 2 +- internal/services/input/rss_test.go | 2 +- makefile | 1 - 15 files changed, 650 insertions(+), 116 deletions(-) create mode 100644 internal/domain/const.go create mode 100644 internal/repository/common.go create mode 100644 internal/repository/source.go create mode 100644 internal/repository/source_test.go create mode 100644 internal/services/database.go diff --git a/.gitignore b/.gitignore index 0e7ec86..75d8d2c 100644 --- a/.gitignore +++ b/.gitignore @@ -4,6 +4,9 @@ __debug_bin server .vscode +# hide the swagger files in the repo +docs/ + # Binaries for programs and plugins *.exe *.exe~ @@ -15,6 +18,7 @@ collector # Test binary, built with `go test -c` *.test + # Output of the go coverage tool, specifically when used with LiteIDE *.out diff --git a/internal/database/migrations/20240425083756_init.sql b/internal/database/migrations/20240425083756_init.sql index afc3c5b..8d83028 100644 --- a/internal/database/migrations/20240425083756_init.sql +++ b/internal/database/migrations/20240425083756_init.sql @@ -5,7 +5,7 @@ CREATE TABLE Articles ( ID INTEGER PRIMARY KEY AUTOINCREMENT, CreatedAt DATETIME NOT NULL, UpdatedAt DATETIME NOT NULL, - DeletedAt DATETIME, + DeletedAt DATETIME NOT NULL, SourceId NUMBER NOT NULL, Tags TEXT NOT NULL, Title TEXT NOT NULL, @@ -33,12 +33,12 @@ CREATE Table DiscordWebHooks ( ID INTEGER PRIMARY KEY AUTOINCREMENT, CreatedAt DATETIME NOT NULL, UpdatedAt DATETIME NOT NULL, - DeletedAt DATETIME, + DeletedAt DATETIME NOT NULL, --Name TEXT NOT NULL, -- Defines webhook purpose --Key TEXT, Url TEXT NOT NULL, -- Webhook Url - Server TEXT NOT NULL, -- Defines the server its bound it. Used for refrence - Channel TEXT NOT NULL, -- Defines the channel its bound to. Used for refrence + Server TEXT NOT NULL, -- Defines the server its bound it. Used for reference + Channel TEXT NOT NULL, -- Defines the channel its bound to. Used for reference Enabled BOOLEAN NOT NULL ); @@ -65,12 +65,9 @@ CREATE Table Sources ( ID INTEGER PRIMARY KEY AUTOINCREMENT, CreatedAt DATETIME NOT NULL, UpdatedAt DATETIME NOT NULL, - DeletedAt DATETIME, - Site TEXT NOT NULL, -- Vanity name - Name TEXT NOT NULL, -- Defines the name of the source. IE: dadjokes - Source TEXT NOT NULL, -- Defines the service that will use this reocrd. IE reddit or youtube - Type TEXT NOT NULL, -- Defines what kind of feed this is. feed, user, tag - Value TEXT, + DeletedAt DATETIME NOT NULL, + DisplayName TEXT NOT NULL, -- Vanity name + Source TEXT NOT NULL, -- Defines the service that will use this record. IE reddit or youtube Enabled BOOLEAN NOT NULL, Url TEXT NOT NULL, Tags TEXT NOT NULL diff --git a/internal/database/migrations/20240425092459_seed.sql b/internal/database/migrations/20240425092459_seed.sql index 55c0471..8cef6a7 100644 --- a/internal/database/migrations/20240425092459_seed.sql +++ b/internal/database/migrations/20240425092459_seed.sql @@ -6,34 +6,34 @@ SELECT 'up SQL query'; --CREATE EXTENSION IF NOT EXISTS "uuid-ossp"; -- Final Fantasy XIV Entries -INSERT INTO sources (CreatedAt, UpdatedAt, Site, Name, Source, Type, Value, Enabled, Url, Tags) VALUES -("2024-04-25 18:37:43.852367", "2024-04-25 18:37:43.852367", 'ffxiv', 'Final Fantasy XIV - NA', 'ffxiv', 'scrape', 'a', TRUE, 'https://na.finalfantasyxiv.com/lodestone/', 'ffxiv, final, fantasy, xiv, na, lodestone'); -INSERT INTO sources (CreatedAt, UpdatedAt, Site, Name, Source, Type, Value, Enabled, Url, Tags) VALUES -("2024-04-25 18:37:43.852367", "2024-04-25 18:37:43.852367", 'ffxiv', 'Final Fantasy XIV - JP', 'ffxiv', 'scrape', 'a', FALSE, 'https://jp.finalfantasyxiv.com/lodestone/', 'ffxiv, final, fantasy, xiv, jp, lodestone'); -INSERT INTO sources (CreatedAt, UpdatedAt, Site, Name, Source, Type, Value, Enabled, Url, Tags) VALUES -("2024-04-25 18:37:43.852367", "2024-04-25 18:37:43.852367", 'ffxiv', 'Final Fantasy XIV - EU', 'ffxiv', 'scrape', 'a', FALSE, 'https://eu.finalfantasyxiv.com/lodestone/', 'ffxiv, final, fantasy, xiv, eu, lodestone'); -INSERT INTO sources (CreatedAt, UpdatedAt, Site, Name, Source, Type, Value, Enabled, Url, Tags) VALUES -("2024-04-25 18:37:43.852367", "2024-04-25 18:37:43.852367", 'ffxiv', 'Final Fantasy XIV - FR', 'ffxiv', 'scrape', 'a', FALSE, 'https://fr.finalfantasyxiv.com/lodestone/', 'ffxiv, final, fantasy, xiv, fr, lodestone'); -INSERT INTO sources (CreatedAt, UpdatedAt, Site, Name, Source, Type, Value, Enabled, Url, Tags) VALUES -("2024-04-25 18:37:43.852367", "2024-04-25 18:37:43.852367", 'ffxiv', 'Final Fantasy XIV - DE', 'ffxiv', 'scrape', 'a', FALSE, 'https://de.finalfantasyxiv.com/lodestone/', 'ffxiv, final, fantasy, xiv, de, lodestone'); +INSERT INTO sources (CreatedAt, UpdatedAt, DeletedAt, DisplayName, Source, Enabled, Url, Tags) VALUES +("2024-04-25 18:37:43.852367", "2024-04-25 18:37:43.852367", "0001-01-01 00:00:00", 'ffxiv', 'Final Fantasy XIV - NA', TRUE, 'https://na.finalfantasyxiv.com/lodestone/', 'ffxiv, final, fantasy, xiv, na, lodestone'); +INSERT INTO sources (CreatedAt, UpdatedAt, DeletedAt, DisplayName, Source, Enabled, Url, Tags) VALUES +("2024-04-25 18:37:43.852367", "2024-04-25 18:37:43.852367", "0001-01-01 00:00:00", 'ffxiv', 'Final Fantasy XIV - JP', FALSE, 'https://jp.finalfantasyxiv.com/lodestone/', 'ffxiv, final, fantasy, xiv, jp, lodestone'); +INSERT INTO sources (CreatedAt, UpdatedAt, DeletedAt, DisplayName, Source, Enabled, Url, Tags) VALUES +("2024-04-25 18:37:43.852367", "2024-04-25 18:37:43.852367", "0001-01-01 00:00:00", 'ffxiv', 'Final Fantasy XIV - EU', FALSE, 'https://eu.finalfantasyxiv.com/lodestone/', 'ffxiv, final, fantasy, xiv, eu, lodestone'); +INSERT INTO sources (CreatedAt, UpdatedAt, DeletedAt, DisplayName, Source, Enabled, Url, Tags) VALUES +("2024-04-25 18:37:43.852367", "2024-04-25 18:37:43.852367", "0001-01-01 00:00:00", 'ffxiv', 'Final Fantasy XIV - FR', FALSE, 'https://fr.finalfantasyxiv.com/lodestone/', 'ffxiv, final, fantasy, xiv, fr, lodestone'); +INSERT INTO sources (CreatedAt, UpdatedAt, DeletedAt, DisplayName, Source, Enabled, Url, Tags) VALUES +("2024-04-25 18:37:43.852367", "2024-04-25 18:37:43.852367", "0001-01-01 00:00:00", 'ffxiv', 'Final Fantasy XIV - DE', FALSE, 'https://de.finalfantasyxiv.com/lodestone/', 'ffxiv, final, fantasy, xiv, de, lodestone'); -- Reddit Entries -INSERT INTO sources (CreatedAt, UpdatedAt, Site, Name, Source, Type, Value, Enabled, Url, Tags) VALUES -("2024-04-25 18:37:43.852367", "2024-04-25 18:37:43.852367", 'reddit', 'dadjokes', 'reddit', 'feed', 'a', TRUE, 'https://reddit.com/r/dadjokes', 'reddit, dadjokes'); -INSERT INTO sources (CreatedAt, UpdatedAt, Site, Name, Source, Type, Value, Enabled, Url, Tags) VALUES -("2024-04-25 18:37:43.852367", "2024-04-25 18:37:43.852367", 'reddit', 'steamdeck', 'reddit', 'feed', 'a', TRUE, 'https://reddit.com/r/steamdeck', 'reddit, steam deck, steam, deck'); +INSERT INTO sources (CreatedAt, UpdatedAt, DeletedAt, DisplayName, Source, Enabled, Url, Tags) VALUES +("2024-04-25 18:37:43.852367", "2024-04-25 18:37:43.852367", "0001-01-01 00:00:00", 'reddit', 'dadjokes', TRUE, 'https://reddit.com/r/dadjokes', 'reddit, dadjokes'); +INSERT INTO sources (CreatedAt, UpdatedAt, DeletedAt, DisplayName, Source, Enabled, Url, Tags) VALUES +("2024-04-25 18:37:43.852367", "2024-04-25 18:37:43.852367", "0001-01-01 00:00:00", 'reddit', 'steamdeck', TRUE, 'https://reddit.com/r/steamdeck', 'reddit, steam deck, steam, deck'); -- Youtube Entries -INSERT INTO sources (CreatedAt, UpdatedAt, Site, Name, Source, Type, Value, Enabled, Url, Tags) VALUES -("2024-04-25 18:37:43.852367", "2024-04-25 18:37:43.852367", 'youtube', 'Game Grumps', 'youtube', 'feed', 'a', TRUE, 'https://www.youtube.com/user/GameGrumps', 'youtube, game grumps, game, grumps'); +INSERT INTO sources (CreatedAt, UpdatedAt, DeletedAt, DisplayName, Source, Enabled, Url, Tags) VALUES +("2024-04-25 18:37:43.852367", "2024-04-25 18:37:43.852367", "0001-01-01 00:00:00", 'youtube', 'Game Grumps', TRUE, 'https://www.youtube.com/user/GameGrumps', 'youtube, game grumps, game, grumps'); -- RSS Entries -INSERT INTO sources (CreatedAt, UpdatedAt, Site, Name, Source, Type, Value, Enabled, Url, Tags) VALUES -("2024-04-25 18:37:43.852367", "2024-04-25 18:37:43.852367", 'steampowered', 'steam deck', 'rss', 'feed', 'a', TRUE, 'https://store.steampowered.com/feeds/news/app/1675200/?cc=US&l=english&snr=1_2108_9__2107', 'rss, steampowered, steam, deck, steam deck'); +INSERT INTO sources (CreatedAt, UpdatedAt, DeletedAt, DisplayName, Source, Enabled, Url, Tags) VALUES +("2024-04-25 18:37:43.852367", "2024-04-25 18:37:43.852367", "0001-01-01 00:00:00", 'steampowered', 'steam deck', TRUE, 'https://store.steampowered.com/feeds/news/app/1675200/?cc=US&l=english&snr=1_2108_9__2107', 'rss, steampowered, steam, deck, steam deck'); -- Twitch Entries -INSERT INTO sources (CreatedAt, UpdatedAt, Site, Name, Source, Type, Value, Enabled, Url, Tags) VALUES -("2024-04-25 18:37:43.852367", "2024-04-25 18:37:43.852367", 'twitch', 'Nintendo', 'twitch', 'api', 'a', TRUE, 'https://twitch.tv/nintendo', 'twitch, nintendo'); +INSERT INTO sources (CreatedAt, UpdatedAt, DeletedAt, DisplayName, Source, Enabled, Url, Tags) VALUES +("2024-04-25 18:37:43.852367", "2024-04-25 18:37:43.852367", "0001-01-01 00:00:00", 'twitch', 'Nintendo', TRUE, 'https://twitch.tv/nintendo', 'twitch, nintendo'); -- +goose StatementEnd diff --git a/internal/domain/const.go b/internal/domain/const.go new file mode 100644 index 0000000..f9f1332 --- /dev/null +++ b/internal/domain/const.go @@ -0,0 +1,9 @@ +package domain + +const ( + SourceCollectorRss = "rss" + SourceCollectorFfxiv = "ffxiv" + SourceCollectorTwitch = "twitch" + SourceCollectorYoutube = "youtube" + SourceCollectorReddit = "reddit" +) diff --git a/internal/domain/entity.go b/internal/domain/entity.go index c8e2304..c892476 100644 --- a/internal/domain/entity.go +++ b/internal/domain/entity.go @@ -37,10 +37,10 @@ type DiscordWebHookEntity struct { DeletedAt time.Time //Name string //Key string - Url string - Server string - Channel string - Enabled bool + Url string + Server string + Channel string + Enabled bool } type IconEntity struct { @@ -63,18 +63,26 @@ type SettingEntity struct { } type SourceEntity struct { - ID int64 - CreatedAt time.Time - UpdatedAt time.Time - DeletedAt time.Time - Site string - Name string - Source string - Type string - Value string - Enabled bool - Url string - Tags string + ID int64 + CreatedAt time.Time + UpdatedAt time.Time + DeletedAt time.Time + + // Who will collect from it. Used + // domain.SourceCollector... + Source string + + // Human Readable value to state what is getting collected + DisplayName string + + // Tells the parser where to look for data + Url string + + // Static tags for this defined record + Tags string + + // If the record is disabled, then it will be skipped on processing + Enabled bool } type SubscriptionEntity struct { diff --git a/internal/repository/article.go b/internal/repository/article.go index 4bc164f..1c92b38 100644 --- a/internal/repository/article.go +++ b/internal/repository/article.go @@ -166,8 +166,8 @@ func (ar ArticleRepository) Create(sourceId int64, tags, title, url, thumbnailUr dt := time.Now() queryBuilder := sqlbuilder.NewInsertBuilder() queryBuilder.InsertInto("articles") - queryBuilder.Cols("UpdatedAt", "CreatedAt", "SourceId", "Tags", "Title", "Url", "PubDate", "IsVideo", "ThumbnailUrl", "Description", "AuthorName", "AuthorImageUrl") - queryBuilder.Values(dt, dt, sourceId, tags, title, url, pubDate, isVideo, thumbnailUrl, description, authorName, authorImageUrl) + queryBuilder.Cols("UpdatedAt", "CreatedAt", "DeletedAt", "SourceId", "Tags", "Title", "Url", "PubDate", "IsVideo", "ThumbnailUrl", "Description", "AuthorName", "AuthorImageUrl") + queryBuilder.Values(dt, dt, timeZero, sourceId, tags, title, url, pubDate, isVideo, thumbnailUrl, description, authorName, authorImageUrl) query, args := queryBuilder.Build() _, err := ar.conn.Exec(query, args...) @@ -185,7 +185,7 @@ func (ur ArticleRepository) processRows(rows *sql.Rows) []domain.ArticleEntity { var id int64 var createdAt time.Time var updatedAt time.Time - var deletedAt sql.NullTime + var deletedAt time.Time var sourceId int64 var tags string var title string @@ -210,6 +210,7 @@ func (ur ArticleRepository) processRows(rows *sql.Rows) []domain.ArticleEntity { ID: id, CreatedAt: createdAt, UpdatedAt: updatedAt, + DeletedAt: deletedAt, SourceID: sourceId, Tags: tags, Title: title, @@ -222,10 +223,6 @@ func (ur ArticleRepository) processRows(rows *sql.Rows) []domain.ArticleEntity { AuthorImageUrl: authorImageUrl, } - if deletedAt.Valid { - item.DeletedAt = deletedAt.Time - } - items = append(items, item) } diff --git a/internal/repository/article_test.go b/internal/repository/article_test.go index e5c37c8..ba81544 100644 --- a/internal/repository/article_test.go +++ b/internal/repository/article_test.go @@ -12,6 +12,7 @@ const ( ) func TestCreateArticle(t *testing.T) { + t.Log(time.Time{}) db, err := setupInMemoryDb() if err != nil { t.Log(err) @@ -134,9 +135,10 @@ func TestPullingByPublishDate(t *testing.T) { t.Log("expected two items back") t.FailNow() } - - if items[0].PubDate.Day() != (today.Day() - 2) { - t.Log("expected the record that was 2 days old") + pubDate := items[1].PubDate.Day() + expectedDay := today.Day() - 1 + if pubDate != expectedDay { + t.Log("expected a record that was 2 days old") t.FailNow() } } @@ -144,7 +146,6 @@ func TestPullingByPublishDate(t *testing.T) { //func TestArticleBySource func insertFakeArticles(r repository.ArticleRepository, title string, daysOld int) error { - pubDate := time.Now().AddDate(0,0, daysOld) _, err := r.Create(1, "", title, articleFakeDotCom, "", "testing", "", "", pubDate, false) if err != nil { diff --git a/internal/repository/common.go b/internal/repository/common.go new file mode 100644 index 0000000..004aeef --- /dev/null +++ b/internal/repository/common.go @@ -0,0 +1,71 @@ +package repository + +import ( + "context" + "database/sql" + "time" + + "github.com/huandu/go-sqlbuilder" +) + +var ( + timeZero = time.Time{} +) + +func deleteFromTable(ctx context.Context, conn *sql.DB, tableName string, id int64) (int64, error) { + b := sqlbuilder.NewDeleteBuilder() + b.DeleteFrom(tableName) + b.Where( + b.Equal("Id", id), + ) + query, args := b.Build() + + _, err := conn.ExecContext(ctx, query, args...) + if err != nil { + return 0, err + } + + return 1, nil +} + +func restoreRow(ctx context.Context, conn *sql.DB, tableName string, id int64) (int64, error) { + timeZero := time.Time{} + b := sqlbuilder.NewUpdateBuilder() + b.Update(tableName) + b.Set( + b.Assign("UpdatedAt", time.Now()), + b.Assign("DeletedAt", timeZero), + ) + b.Where( + b.Equal("Id", id), + ) + query, args := b.Build() + + _, err := conn.ExecContext(ctx, query, args...) + if err != nil { + return 0, err + } + + return 1, nil +} + +func softDeleteRow(ctx context.Context, conn *sql.DB, tableName string, id int64) (int64, error) { + now := time.Now() + b := sqlbuilder.NewUpdateBuilder() + b.Update(tableName) + b.Set( + b.Assign("UpdatedAt", now), + b.Assign("DeletedAt", now), + ) + b.Where( + b.Equal("Id", id), + ) + query, args := b.Build() + + _, err := conn.ExecContext(ctx, query, args...) + if err != nil { + return 0, err + } + + return 1, nil +} \ No newline at end of file diff --git a/internal/repository/discordWebHooks.go b/internal/repository/discordWebHooks.go index 37dca24..dead8c4 100644 --- a/internal/repository/discordWebHooks.go +++ b/internal/repository/discordWebHooks.go @@ -23,8 +23,8 @@ func (r discordWebHookRepository) Create(ctx context.Context, url, server, chann dt := time.Now() queryBuilder := sqlbuilder.NewInsertBuilder() queryBuilder.InsertInto("DiscordWebHooks") - queryBuilder.Cols("UpdatedAt", "CreatedAt", "Url", "Server", "Channel", "Enabled") - queryBuilder.Values(dt, dt, url, server, channel, enabled) + queryBuilder.Cols("UpdatedAt", "CreatedAt", "DeletedAt", "Url", "Server", "Channel", "Enabled") + queryBuilder.Values(dt, dt, timeZero, url, server, channel, enabled) query, args := queryBuilder.Build() _, err := r.conn.ExecContext(ctx, query, args...) @@ -42,6 +42,9 @@ func (r discordWebHookRepository) Enable(ctx context.Context, id int64) (int64, b.Assign("Enabled", true), b.Assign("UpdatedAt", time.Now()), ) + b.Where( + b.Equal("Id", id), + ) query, args := b.Build() _, err := r.conn.ExecContext(ctx, query, args...) @@ -59,6 +62,9 @@ func (r discordWebHookRepository) Disable(ctx context.Context, id int64) (int64, b.Assign("Enabled", false), b.Assign("UpdatedAt", time.Now()), ) + b.Where( + b.Equal("Id", id), + ) query, args := b.Build() _, err := r.conn.ExecContext(ctx, query, args...) @@ -70,61 +76,15 @@ func (r discordWebHookRepository) Disable(ctx context.Context, id int64) (int64, } func (r discordWebHookRepository) SoftDelete(ctx context.Context, id int64) (int64, error) { - now := time.Now() - b := sqlbuilder.NewUpdateBuilder() - b.Update("DiscordWebHooks") - b.Set( - b.Assign("UpdatedAt", now), - b.Assign("DeletedAt", now), - ) - b.Where( - b.Equal("Id", id), - ) - query, args := b.Build() - - _, err := r.conn.ExecContext(ctx, query, args...) - if err != nil { - return 0, err - } - - return 1, nil + return softDeleteRow(ctx, r.conn, "DiscordWebHooks", id) } func (r discordWebHookRepository) Restore(ctx context.Context, id int64) (int64, error) { - timeZero := time.Time{} - b := sqlbuilder.NewUpdateBuilder() - b.Update("DiscordWebHooks") - b.Set( - b.Assign("UpdatedAt", time.Now()), - b.Assign("DeletedAt", timeZero), - ) - b.Where( - b.Equal("Id", id), - ) - query, args := b.Build() - - _, err := r.conn.ExecContext(ctx, query, args...) - if err != nil { - return 0, err - } - - return 1, nil + return restoreRow(ctx, r.conn, "DiscordWebHooks", id) } func (r discordWebHookRepository) Delete(ctx context.Context, id int64) (int64, error) { - b := sqlbuilder.NewDeleteBuilder() - b.DeleteFrom("DiscordWebHooks") - b.Where( - b.Equal("Id", id), - ) - query, args := b.Build() - - _, err := r.conn.ExecContext(ctx, query, args...) - if err != nil { - return 0, err - } - - return 1, nil + return deleteFromTable(ctx, r.conn, "DiscordWebHooks", id) } func (r discordWebHookRepository) GetById(ctx context.Context, id int64) (domain.DiscordWebHookEntity, error) { @@ -221,7 +181,7 @@ func (r discordWebHookRepository) processRows(rows *sql.Rows) ([]domain.DiscordW var id int64 var createdAt time.Time var updatedAt time.Time - var deletedAt sql.NullTime + var deletedAt time.Time var url string var server string var channel string @@ -239,16 +199,13 @@ func (r discordWebHookRepository) processRows(rows *sql.Rows) ([]domain.DiscordW ID: id, CreatedAt: createdAt, UpdatedAt: updatedAt, + DeletedAt: deletedAt, Url: url, Server: server, Channel: channel, Enabled: enabled, } - if deletedAt.Valid { - item.DeletedAt = deletedAt.Time - } - items = append(items, item) } diff --git a/internal/repository/source.go b/internal/repository/source.go new file mode 100644 index 0000000..2073768 --- /dev/null +++ b/internal/repository/source.go @@ -0,0 +1,239 @@ +package repository + +import ( + "context" + "database/sql" + "time" + + "git.jamestombleson.com/jtom38/newsbot-api/internal/domain" + "github.com/huandu/go-sqlbuilder" +) + +type sourceRepository struct { + conn *sql.DB +} + +func NewSourceRepository(conn *sql.DB) sourceRepository { + return sourceRepository{ + conn: conn, + } +} + +func (r sourceRepository) Create(ctx context.Context, source, displayName, url, tags string, enabled bool) (int64, error) { + dt := time.Now() + queryBuilder := sqlbuilder.NewInsertBuilder() + queryBuilder.InsertInto("Sources") + queryBuilder.Cols("CreatedAt", "UpdatedAt", "DeletedAt", "DisplayName", "Source", "Url", "Tags", "Enabled") + queryBuilder.Values(dt, dt, timeZero, displayName, source, url, tags, enabled) + query, args := queryBuilder.Build() + + _, err := r.conn.ExecContext(ctx, query, args...) + if err != nil { + return 0, err + } + + return 1, nil +} + +func (r sourceRepository) GetById(ctx context.Context, id int64) (domain.SourceEntity, error) { + b := sqlbuilder.NewSelectBuilder() + b.Select("*") + b.From("Sources").Where( + b.Equal("Id", id), + ) + b.Limit(1) + query, args := b.Build() + + rows, err := r.conn.QueryContext(ctx, query, args...) + if err != nil { + return domain.SourceEntity{}, err + } + + data, err := r.processRows(rows) + if len(data) == 0 { + return domain.SourceEntity{}, err + } + + return data[0], nil +} + +func (r sourceRepository) GetByDisplayName(ctx context.Context, displayName string) (domain.SourceEntity, error) { + b := sqlbuilder.NewSelectBuilder() + b.Select("*") + b.From("Sources").Where( + b.Equal("DisplayName", displayName), + ) + b.Limit(1) + query, args := b.Build() + + rows, err := r.conn.QueryContext(ctx, query, args...) + if err != nil { + return domain.SourceEntity{}, err + } + + data, err := r.processRows(rows) + if len(data) == 0 { + return domain.SourceEntity{}, err + } + + return data[0], nil +} + +func (r sourceRepository) GetBySource(ctx context.Context, source string) (domain.SourceEntity, error) { + b := sqlbuilder.NewSelectBuilder() + b.Select("*") + b.From("Sources").Where( + b.Equal("Source", source), + ) + b.Limit(1) + query, args := b.Build() + + rows, err := r.conn.QueryContext(ctx, query, args...) + if err != nil { + return domain.SourceEntity{}, err + } + + data, err := r.processRows(rows) + if len(data) == 0 { + return domain.SourceEntity{}, err + } + + return data[0], nil +} + +func (r sourceRepository) List(ctx context.Context, page, limit int) ([]domain.SourceEntity, error) { + builder := sqlbuilder.NewSelectBuilder() + builder.Select("*") + builder.From("Sources") + builder.Offset(page * limit) + builder.Limit(limit) + + query, args := builder.Build() + rows, err := r.conn.QueryContext(ctx, query, args...) + if err != nil { + return []domain.SourceEntity{}, err + } + + data, err := r.processRows(rows) + if len(data) == 0 { + return []domain.SourceEntity{}, err + } + + return data, nil +} + +func (r sourceRepository) ListBySource(ctx context.Context, page, limit int, source string) ([]domain.SourceEntity, error) { + builder := sqlbuilder.NewSelectBuilder() + builder.Select("*") + builder.From("Sources") + builder.Where( + builder.Equal("Source", source), + ) + builder.Offset(page * limit) + builder.Limit(limit) + + query, args := builder.Build() + rows, err := r.conn.QueryContext(ctx, query, args...) + if err != nil { + return []domain.SourceEntity{}, err + } + + data, err := r.processRows(rows) + if len(data) == 0 { + return []domain.SourceEntity{}, err + } + + return data, nil +} + +func (r sourceRepository) Enable(ctx context.Context, id int64) (int64, error) { + b := sqlbuilder.NewUpdateBuilder() + b.Update("Sources") + b.Set( + b.Assign("Enabled", true), + b.Assign("UpdatedAt", time.Now()), + ) + b.Where( + b.Equal("Id", id), + ) + query, args := b.Build() + + _, err := r.conn.ExecContext(ctx, query, args...) + if err != nil { + return 0, err + } + + return 1, nil +} + +func (r sourceRepository) Disable(ctx context.Context, id int64) (int64, error) { + b := sqlbuilder.NewUpdateBuilder() + b.Update("Sources") + b.Set( + b.Assign("Enabled", false), + b.Assign("UpdatedAt", time.Now()), + ) + b.Where( + b.Equal("Id", id), + ) + query, args := b.Build() + + _, err := r.conn.ExecContext(ctx, query, args...) + if err != nil { + return 0, err + } + + return 1, nil +} + +func (r sourceRepository) SoftDelete(ctx context.Context, id int64) (int64, error) { + return softDeleteRow(ctx, r.conn, "Sources", id) +} + +func (r sourceRepository) Restore(ctx context.Context, id int64) (int64, error) { + return restoreRow(ctx, r.conn, "Sources", id) +} + +func (r sourceRepository) Delete(ctx context.Context, id int64) (int64, error) { + return deleteFromTable(ctx, r.conn, "Sources", id) +} + +func (r sourceRepository) processRows(rows *sql.Rows) ([]domain.SourceEntity, error) { + items := []domain.SourceEntity{} + + for rows.Next() { + var id int64 + var createdAt time.Time + var updatedAt time.Time + var deletedAt time.Time + var displayName string + var source string + var enabled bool + var url string + var tags string + err := rows.Scan( + &id, &createdAt, &updatedAt, + &deletedAt, &displayName, &source, + &enabled, &url, &tags, + ) + if err != nil { + return items, err + } + + item := domain.SourceEntity{ + ID: id, + CreatedAt: createdAt, + UpdatedAt: updatedAt, + DeletedAt: deletedAt, + DisplayName: displayName, + Source: source, + Enabled: enabled, + Url: url, + Tags: tags, + } + + items = append(items, item) + } + + return items, nil +} diff --git a/internal/repository/source_test.go b/internal/repository/source_test.go new file mode 100644 index 0000000..d262322 --- /dev/null +++ b/internal/repository/source_test.go @@ -0,0 +1,246 @@ +package repository_test + +import ( + "context" + "testing" + + "git.jamestombleson.com/jtom38/newsbot-api/internal/domain" + "git.jamestombleson.com/jtom38/newsbot-api/internal/repository" +) + +func TestSourceCreate(t *testing.T) { + db, err := setupInMemoryDb() + if err != nil { + t.Log(err) + t.FailNow() + } + defer db.Close() + ctx := context.Background() + r := repository.NewSourceRepository(db) + + rows, err := r.Create(ctx, domain.SourceCollectorRss, "Test", "www.badurl.com", "rss, badurl", true) + if err != nil { + t.Log(err) + t.FailNow() + } + + if rows != 1 { + t.Log("failed to create a record, why") + t.FailNow() + } +} + +func TestSourceGetById(t *testing.T) { + db, err := setupInMemoryDb() + if err != nil { + t.Log(err) + t.FailNow() + } + defer db.Close() + ctx := context.Background() + r := repository.NewSourceRepository(db) + + _, err = r.Create(ctx, domain.SourceCollectorRss, "Test", "www.badurl.com", "rss, badurl", true) + if err != nil { + t.Log(err) + t.FailNow() + } + + item, err := r.GetById(ctx, 1) + if err != nil { + t.Log(err) + t.FailNow() + } + if item.ID != 1 { + t.Log("got no record or the wrong one") + t.FailNow() + } +} + +func TestSourceGetByDisplayName(t *testing.T) { + db, err := setupInMemoryDb() + if err != nil { + t.Log(err) + t.FailNow() + } + defer db.Close() + ctx := context.Background() + r := repository.NewSourceRepository(db) + + _, err = r.Create(ctx, domain.SourceCollectorRss, "Test", "www.badurl.com", "rss, badurl", true) + if err != nil { + t.Log(err) + t.FailNow() + } + + item, err := r.GetByDisplayName(ctx, "Test") + if err != nil { + t.Log(err) + t.FailNow() + } + if item.DisplayName != "Test" { + t.Log("got no record or the wrong one") + t.FailNow() + } +} + +func TestSourceGetBySource(t *testing.T) { + db, err := setupInMemoryDb() + if err != nil { + t.Log(err) + t.FailNow() + } + defer db.Close() + ctx := context.Background() + r := repository.NewSourceRepository(db) + + _, err = r.Create(ctx, domain.SourceCollectorRss, "Test", "www.badurl.com", "rss, badurl", true) + if err != nil { + t.Log(err) + t.FailNow() + } + + item, err := r.GetBySource(ctx, domain.SourceCollectorRss) + if err != nil { + t.Log(err) + t.FailNow() + } + if item.Source != domain.SourceCollectorRss { + t.Log("got no record or the wrong one") + t.FailNow() + } +} + +func TestSourceList(t *testing.T) { + db, err := setupInMemoryDb() + if err != nil { + t.Log(err) + t.FailNow() + } + defer db.Close() + ctx := context.Background() + r := repository.NewSourceRepository(db) + + _, _ = r.Create(ctx, domain.SourceCollectorRss, "Test", "www.badurl.com", "rss, badurl", true) + _, _ = r.Create(ctx, domain.SourceCollectorRss, "Test", "www.badurl.com", "rss, badurl", true) + _, _ = r.Create(ctx, domain.SourceCollectorRss, "Test", "www.badurl.com", "rss, badurl", true) + _, _ = r.Create(ctx, domain.SourceCollectorRss, "Test", "www.badurl.com", "rss, badurl", true) + + items, err := r.List(ctx, 0, 4) + if err != nil { + t.Log(err) + t.FailNow() + } + if len(items ) != 4 { + t.Log("something bad happened here") + t.FailNow() + } +} + +func TestSourceListBySource(t *testing.T) { + db, err := setupInMemoryDb() + if err != nil { + t.Log(err) + t.FailNow() + } + defer db.Close() + ctx := context.Background() + r := repository.NewSourceRepository(db) + + _, _ = r.Create(ctx, domain.SourceCollectorRss, "Test", "www.badurl.com", "rss, badurl", true) + _, _ = r.Create(ctx, domain.SourceCollectorRss, "Test", "www.badurl.com", "rss, badurl", true) + _, _ = r.Create(ctx, domain.SourceCollectorRss, "Test", "www.badurl.com", "rss, badurl", true) + _, _ = r.Create(ctx, domain.SourceCollectorRss, "Test", "www.badurl.com", "rss, badurl", true) + + items, err := r.ListBySource(ctx, 0, 4, domain.SourceCollectorRss) + if err != nil { + t.Log(err) + t.FailNow() + } + if len(items ) != 4 { + t.Log("something bad happened here") + t.FailNow() + } +} + +func TestSourcesEnableRecord(t *testing.T) { + // This depends on the seed migration + db, err := setupInMemoryDb() + if err != nil { + t.Log(err) + t.FailNow() + } + defer db.Close() + + ctx := context.Background() + + r := repository.NewSourceRepository(db) + _, _ = r.Create(ctx, domain.SourceCollectorRss, "Test", "www.badurl.com", "rss, badurl", false) + item, err := r.GetByDisplayName(ctx, "Test") + if err != nil { + t.Log(err) + t.FailNow() + } + + if item.Enabled != false { + t.Log("the initial record was created wrong") + t.FailNow() + } + + _, err = r.Enable(ctx, item.ID) + if err != nil { + t.Log(err) + t.FailNow() + } + + updated, err := r.GetById(ctx, item.ID) + if err != nil { + t.Log(err) + t.FailNow() + } + + if item.Enabled == updated.Enabled { + t.Log("failed to update the enabled value") + t.FailNow() + } +} + +func TestSourcesDisableRecord(t *testing.T) { + db, err := setupInMemoryDb() + if err != nil { + t.Log(err) + t.FailNow() + } + defer db.Close() + + ctx := context.Background() + r := repository.NewSourceRepository(db) + _, _ = r.Create(ctx, domain.SourceCollectorRss, "Test", "www.badurl.com", "rss, badurl", true) + item, err := r.GetByDisplayName(ctx, "Test") + if err != nil { + t.Log(err) + t.FailNow() + } + + if item.Enabled != true { + t.Log("the initial record was created wrong") + t.FailNow() + } + + _, err = r.Disable(ctx, 1) + if err != nil { + t.Log(err) + t.FailNow() + } + + updated, err := r.GetById(ctx, 1) + if err != nil { + t.Log(err) + t.FailNow() + } + + if item.Enabled == updated.Enabled { + t.Log("failed to update the enabled value") + t.FailNow() + } +} \ No newline at end of file diff --git a/internal/services/database.go b/internal/services/database.go new file mode 100644 index 0000000..80eb9ab --- /dev/null +++ b/internal/services/database.go @@ -0,0 +1,6 @@ +package services + +type RepositoryService struct { + +} + diff --git a/internal/services/input/rss.go b/internal/services/input/rss.go index a803267..3d2833b 100644 --- a/internal/services/input/rss.go +++ b/internal/services/input/rss.go @@ -26,7 +26,7 @@ func NewRssClient(sourceRecord domain.SourceEntity) rssClient { //} func (rc rssClient) getCacheGroup() string { - return fmt.Sprintf("rss-%v", rc.SourceRecord.Name) + return fmt.Sprintf("rss-%v", rc.SourceRecord.DisplayName) } func (rc rssClient) GetContent() error { diff --git a/internal/services/input/rss_test.go b/internal/services/input/rss_test.go index e27b510..728bd85 100644 --- a/internal/services/input/rss_test.go +++ b/internal/services/input/rss_test.go @@ -9,7 +9,7 @@ import ( var rssRecord = domain.SourceEntity{ ID: 1, - Name: "ArsTechnica", + DisplayName: "ArsTechnica", Url: "https://feeds.arstechnica.com/arstechnica/index", } diff --git a/makefile b/makefile index 7a62368..baec9f5 100644 --- a/makefile +++ b/makefile @@ -3,7 +3,6 @@ help: ## Shows this help command @egrep -h '\s##\s' $(MAKEFILE_LIST) | sort | awk 'BEGIN {FS = ":.*?## "}; {printf "\033[36m%-20s\033[0m %s\n", $$1, $$2}' build: ## builds the application with the current go runtime - sqlc generate ~/go/bin/swag f ~/go/bin/swag i go build .