From ba33d18525ef4bd26bfc81fef6c97ae40571f5b7 Mon Sep 17 00:00:00 2001 From: James Tombleson Date: Sat, 27 Apr 2024 07:44:04 -0700 Subject: [PATCH 01/13] Redefined what can be null and removed some values I am not sure matter anymore --- .../database/migrations/20240425083756_init.sql | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/internal/database/migrations/20240425083756_init.sql b/internal/database/migrations/20240425083756_init.sql index 5da446a..afc3c5b 100644 --- a/internal/database/migrations/20240425083756_init.sql +++ b/internal/database/migrations/20240425083756_init.sql @@ -4,20 +4,20 @@ SELECT 'up SQL query'; CREATE TABLE Articles ( ID INTEGER PRIMARY KEY AUTOINCREMENT, CreatedAt DATETIME NOT NULL, - LastUpdated DATETIME NOT NULL, + UpdatedAt DATETIME NOT NULL, DeletedAt DATETIME, SourceId NUMBER NOT NULL, Tags TEXT NOT NULL, Title TEXT NOT NULL, Url TEXT NOT NULL, PubDate DATETIME NOT NULL, - Video TEXT, - VideoHeight int NOT NULL, - VideoWidth int NOT NULL, - Thumbnail TEXT NOT NULL, + IsVideo TEXT NOT NULL, + --VideoHeight int NOT NULL, + --VideoWidth int NOT NULL, + ThumbnailUrl TEXT NOT NULL, Description TEXT NOT NULL, - AuthorName TEXT, - AuthorImageUrl TEXT + AuthorName TEXT NOT NULL, + AuthorImageUrl TEXT NOT NULL ); CREATE Table DiscordQueue ( -- 2.45.2 From f6cc0a3d93075a7c80113e84a07773155138b939 Mon Sep 17 00:00:00 2001 From: James Tombleson Date: Sat, 27 Apr 2024 07:44:20 -0700 Subject: [PATCH 02/13] entity updated to reflect table --- internal/domain/entity.go | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/internal/domain/entity.go b/internal/domain/entity.go index ab8ddcc..80ce64b 100644 --- a/internal/domain/entity.go +++ b/internal/domain/entity.go @@ -7,13 +7,14 @@ import ( type ArticleEntity struct { ID int64 CreatedAt time.Time - LastUpdated time.Time + UpdatedAt time.Time DeletedAt time.Time SourceID int64 Tags string Title string Url string PubDate time.Time + IsVideo bool Thumbnail string Description string AuthorName string @@ -21,12 +22,12 @@ type ArticleEntity struct { } type DiscordQueueEntity struct { - ID int64 - CreatedAt time.Time - LastUpdated time.Time - DeletedAt time.Time - ArticleId int64 - SourceId int64 + ID int64 + CreatedAt time.Time + UpdatedAt time.Time + DeletedAt time.Time + ArticleId int64 + SourceId int64 } type DiscordWebHookEntity struct { -- 2.45.2 From 3d3b582e82f3743b3b9476dec1df607eb0b39a7c Mon Sep 17 00:00:00 2001 From: James Tombleson Date: Sat, 27 Apr 2024 07:44:41 -0700 Subject: [PATCH 03/13] Articles can be created and working on pulling over the old queries --- internal/repository/article.go | 226 ++++++++++++++++++++++++++++ internal/repository/article_test.go | 118 +++++++++++++++ 2 files changed, 344 insertions(+) create mode 100644 internal/repository/article.go create mode 100644 internal/repository/article_test.go diff --git a/internal/repository/article.go b/internal/repository/article.go new file mode 100644 index 0000000..5b0b2b8 --- /dev/null +++ b/internal/repository/article.go @@ -0,0 +1,226 @@ +package repository + +import ( + "database/sql" + "errors" + "fmt" + "time" + + "git.jamestombleson.com/jtom38/newsbot-api/internal/domain" + "github.com/huandu/go-sqlbuilder" +) + +type ArticleRepository struct { + conn *sql.DB + defaultLimit int + defaultOffset int +} + +func NewArticleRepository(conn *sql.DB) ArticleRepository { + return ArticleRepository{ + conn: conn, + defaultLimit: 50, + defaultOffset: 50, + } +} + +func (ar ArticleRepository) GetById(id int64) (domain.ArticleEntity, error) { + builder := sqlbuilder.NewSelectBuilder() + builder.Select("*") + builder.From("articles").Where( + builder.E("id", id), + ) + builder.Limit(1) + + query, args := builder.Build() + rows, err := ar.conn.Query(query, args...) + if err != nil { + return domain.ArticleEntity{}, err + } + + data := ar.processRows(rows) + if len(data) == 0 { + return domain.ArticleEntity{}, errors.New(ErrUserNotFound) + } + + return data[0], nil +} + +func (ar ArticleRepository) GetByUrl(url string) (domain.ArticleEntity, error) { + builder := sqlbuilder.NewSelectBuilder() + builder.Select("*") + builder.From("articles").Where( + builder.E("url", url), + ) + builder.Limit(1) + + query, args := builder.Build() + rows, err := ar.conn.Query(query, args...) + if err != nil { + return domain.ArticleEntity{}, err + } + + data := ar.processRows(rows) + if len(data) == 0 { + return domain.ArticleEntity{}, errors.New(ErrUserNotFound) + } + + return data[0], nil +} + +func (ar ArticleRepository) List(limit int) ([]domain.ArticleEntity, error) { + builder := sqlbuilder.NewSelectBuilder() + builder.Select("*") + builder.From("articles") + //builder.OrderBy("pubdate") + builder.Limit(limit) + //builder.Offset(50) + + query, args := builder.Build() + rows, err := ar.conn.Query(query, args...) + if err != nil { + return []domain.ArticleEntity{}, err + } + + data := ar.processRows(rows) + if len(data) == 0 { + return []domain.ArticleEntity{}, errors.New(ErrUserNotFound) + } + + return data, nil +} + +func (ar ArticleRepository) ListByPage(page, limit int) ([]domain.ArticleEntity, error) { + builder := sqlbuilder.NewSelectBuilder() + builder.Select("*") + builder.From("articles") + builder.OrderBy("pubdate desc") + builder.Offset(page * limit) + builder.Limit(limit) + + query, args := builder.Build() + rows, err := ar.conn.Query(query, args...) + if err != nil { + return []domain.ArticleEntity{}, err + } + + data := ar.processRows(rows) + if len(data) == 0 { + return []domain.ArticleEntity{}, errors.New(ErrUserNotFound) + } + + return data, nil +} + +func (ar ArticleRepository) ListByPublishDate(limit int) ([]domain.ArticleEntity, error) { + builder := sqlbuilder.NewSelectBuilder() + builder.Select("*") + builder.From("articles") + //builder.OrderBy("pubdate") + builder.Limit(limit) + //builder.Offset(50) + + query, args := builder.Build() + rows, err := ar.conn.Query(query, args...) + if err != nil { + return []domain.ArticleEntity{}, err + } + + data := ar.processRows(rows) + if len(data) == 0 { + return []domain.ArticleEntity{}, errors.New(ErrUserNotFound) + } + return data, nil +} + +func (ar ArticleRepository) ListBySource(sourceName string, limit int) ([]domain.ArticleEntity, error) { + builder := sqlbuilder.NewSelectBuilder() + builder.Select("*") + builder.From("articles") + builder.JoinWithOption("InnerJoin", "sources", "articles.sourceId=sources.Id") + builder.OrderBy("pubdate") + builder.Limit(limit) + builder.Offset(50) + + query, args := builder.Build() + rows, err := ar.conn.Query(query, args...) + if err != nil { + return []domain.ArticleEntity{}, err + } + + data := ar.processRows(rows) + if len(data) == 0 { + return []domain.ArticleEntity{}, errors.New(ErrUserNotFound) + } + return data, nil +} + +func (ar ArticleRepository) Create(sourceId int64, tags, title, url, thumbnailUrl, description, authorName, authorImageUrl string, pubDate time.Time, isVideo bool) (int64, error) { + 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) + query, args := queryBuilder.Build() + + _, err := ar.conn.Exec(query, args...) + if err != nil { + return 0, err + } + + return 1, nil +} + +func (ur ArticleRepository) processRows(rows *sql.Rows) []domain.ArticleEntity { + items := []domain.ArticleEntity{} + + for rows.Next() { + var id int64 + var createdAt time.Time + var updatedAt time.Time + var deletedAt sql.NullTime + var sourceId int64 + var tags string + var title string + var url string + var pubDate time.Time + var isVideo bool + var thumbnail string + var description string + var authorName string + var authorImageUrl string + err := rows.Scan( + &id, &createdAt, &updatedAt, + &deletedAt, &sourceId, &tags, + &title, &url, &pubDate, + &isVideo, &thumbnail, &description, + &authorName, &authorImageUrl) + if err != nil { + fmt.Println(err) + } + + item := domain.ArticleEntity{ + ID: id, + CreatedAt: createdAt, + UpdatedAt: updatedAt, + SourceID: sourceId, + Tags: tags, + Title: title, + Url: url, + PubDate: pubDate, + IsVideo: isVideo, + Thumbnail: thumbnail, + Description: description, + AuthorName: authorName, + AuthorImageUrl: authorImageUrl, + } + + if deletedAt.Valid { + item.DeletedAt = deletedAt.Time + } + + items = append(items, item) + } + + return items +} diff --git a/internal/repository/article_test.go b/internal/repository/article_test.go new file mode 100644 index 0000000..b4d127d --- /dev/null +++ b/internal/repository/article_test.go @@ -0,0 +1,118 @@ +package repository_test + +import ( + "testing" + "time" + + "git.jamestombleson.com/jtom38/newsbot-api/internal/repository" +) + +const ( + articleFakeDotCom = "www.fake.com" +) + +func TestCreateArticle(t *testing.T) { + db, err := setupInMemoryDb() + if err != nil { + t.Log(err) + t.FailNow() + } + defer db.Close() + + r := repository.NewArticleRepository(db) + created, err := r.Create(1, "", "unit test", articleFakeDotCom, "", "testing", "", "", time.Now(), false) + if err != nil { + t.Log(err) + t.FailNow() + } + + if created != 1 { + t.Log("failed to create the record") + t.FailNow() + } +} + +func TestArticleByUrl(t *testing.T) { + db, err := setupInMemoryDb() + if err != nil { + t.Log(err) + t.FailNow() + } + defer db.Close() + r := repository.NewArticleRepository(db) + + err = insertFakeArticles(r, "u1") + if err != nil { + t.Log(err) + t.FailNow() + } + + article, err := r.GetByUrl(articleFakeDotCom) + if err != nil { + t.Log(err) + t.FailNow() + } + + if article.Url != "www.fake.com" { + t.Log("failed to find the requested record") + t.FailNow() + } +} + +func TestPullingMultipleArticlesWithLimit(t *testing.T) { + db, err := setupInMemoryDb() + if err != nil { + t.Log(err) + t.FailNow() + } + defer db.Close() + r := repository.NewArticleRepository(db) + insertFakeArticles(r, "u1") + insertFakeArticles(r, "u2") + insertFakeArticles(r, "u3") + insertFakeArticles(r, "u4") + + items, err := r.List(3) + if err != nil { + t.Log(err) + t.FailNow() + } + + if len(items) != 3 { + t.Log("expected 3 rows back") + t.FailNow() + } +} + +func TestPullingMultipleArticlesWithPaging(t *testing.T) { + db, err := setupInMemoryDb() + if err != nil { + t.Log(err) + t.FailNow() + } + defer db.Close() + r := repository.NewArticleRepository(db) + insertFakeArticles(r, "u1") + insertFakeArticles(r, "u2") + insertFakeArticles(r, "u3") + insertFakeArticles(r, "u4") + + items, err := r.ListByPage(2, 1) + if err != nil { + t.Log(err) + t.FailNow() + } + + if items[0].Title != "u2" { + t.Log("pulled the wrong record with paging") + t.FailNow() + } +} + +func insertFakeArticles(r repository.ArticleRepository, title string) error { + _, err := r.Create(1, "", title, articleFakeDotCom, "", "testing", "", "", time.Now(), false) + if err != nil { + return err + } + return nil +} -- 2.45.2 From 15681d9d3779ad0a23088a83ae9be7a11e011bea Mon Sep 17 00:00:00 2001 From: James Tombleson Date: Sat, 27 Apr 2024 13:11:03 -0700 Subject: [PATCH 04/13] Almost done with DiscordWebHooks repo --- internal/domain/entity.go | 4 +- internal/repository/article.go | 25 +- internal/repository/article_test.go | 60 +++- internal/repository/discordWebHooks.go | 256 +++++++++++++++++ internal/repository/discordWebHooks_test.go | 287 ++++++++++++++++++++ internal/repository/users_test.go | 1 - 6 files changed, 609 insertions(+), 24 deletions(-) create mode 100644 internal/repository/discordWebHooks.go create mode 100644 internal/repository/discordWebHooks_test.go diff --git a/internal/domain/entity.go b/internal/domain/entity.go index 80ce64b..c8e2304 100644 --- a/internal/domain/entity.go +++ b/internal/domain/entity.go @@ -35,8 +35,8 @@ type DiscordWebHookEntity struct { CreatedAt time.Time UpdatedAt time.Time DeletedAt time.Time - Name string - Key string + //Name string + //Key string Url string Server string Channel string diff --git a/internal/repository/article.go b/internal/repository/article.go index 5b0b2b8..4bc164f 100644 --- a/internal/repository/article.go +++ b/internal/repository/article.go @@ -10,6 +10,11 @@ import ( "github.com/huandu/go-sqlbuilder" ) +const ( + ArticleOrderByPublishDateDesc = "pubDate desc" + ArticleOrderByPublishDatAsc = "pubDate asc" +) + type ArticleRepository struct { conn *sql.DB defaultLimit int @@ -68,13 +73,11 @@ func (ar ArticleRepository) GetByUrl(url string) (domain.ArticleEntity, error) { return data[0], nil } -func (ar ArticleRepository) List(limit int) ([]domain.ArticleEntity, error) { +func (ar ArticleRepository) ListTop(limit int) ([]domain.ArticleEntity, error) { builder := sqlbuilder.NewSelectBuilder() builder.Select("*") builder.From("articles") - //builder.OrderBy("pubdate") builder.Limit(limit) - //builder.Offset(50) query, args := builder.Build() rows, err := ar.conn.Query(query, args...) @@ -112,13 +115,15 @@ func (ar ArticleRepository) ListByPage(page, limit int) ([]domain.ArticleEntity, return data, nil } -func (ar ArticleRepository) ListByPublishDate(limit int) ([]domain.ArticleEntity, error) { +func (ar ArticleRepository) ListByPublishDate(page, limit int, orderBy string) ([]domain.ArticleEntity, error) { builder := sqlbuilder.NewSelectBuilder() builder.Select("*") builder.From("articles") - //builder.OrderBy("pubdate") + if orderBy != "" { + builder.OrderBy(orderBy) + } + builder.Offset(page * limit) builder.Limit(limit) - //builder.Offset(50) query, args := builder.Build() rows, err := ar.conn.Query(query, args...) @@ -133,14 +138,16 @@ func (ar ArticleRepository) ListByPublishDate(limit int) ([]domain.ArticleEntity return data, nil } -func (ar ArticleRepository) ListBySource(sourceName string, limit int) ([]domain.ArticleEntity, error) { +func (ar ArticleRepository) ListBySource(page, limit int, orderBy string) ([]domain.ArticleEntity, error) { builder := sqlbuilder.NewSelectBuilder() builder.Select("*") builder.From("articles") builder.JoinWithOption("InnerJoin", "sources", "articles.sourceId=sources.Id") - builder.OrderBy("pubdate") - builder.Limit(limit) + if orderBy != "" { + builder.OrderBy(orderBy) + } builder.Offset(50) + builder.Limit(page * limit) query, args := builder.Build() rows, err := ar.conn.Query(query, args...) diff --git a/internal/repository/article_test.go b/internal/repository/article_test.go index b4d127d..e5c37c8 100644 --- a/internal/repository/article_test.go +++ b/internal/repository/article_test.go @@ -41,7 +41,7 @@ func TestArticleByUrl(t *testing.T) { defer db.Close() r := repository.NewArticleRepository(db) - err = insertFakeArticles(r, "u1") + err = insertFakeArticles(r, "u1", 0) if err != nil { t.Log(err) t.FailNow() @@ -67,12 +67,12 @@ func TestPullingMultipleArticlesWithLimit(t *testing.T) { } defer db.Close() r := repository.NewArticleRepository(db) - insertFakeArticles(r, "u1") - insertFakeArticles(r, "u2") - insertFakeArticles(r, "u3") - insertFakeArticles(r, "u4") + insertFakeArticles(r, "u1", 0) + insertFakeArticles(r, "u2", 0) + insertFakeArticles(r, "u3", 0) + insertFakeArticles(r, "u4", 0) - items, err := r.List(3) + items, err := r.ListTop(3) if err != nil { t.Log(err) t.FailNow() @@ -92,10 +92,10 @@ func TestPullingMultipleArticlesWithPaging(t *testing.T) { } defer db.Close() r := repository.NewArticleRepository(db) - insertFakeArticles(r, "u1") - insertFakeArticles(r, "u2") - insertFakeArticles(r, "u3") - insertFakeArticles(r, "u4") + insertFakeArticles(r, "u1", 0) + insertFakeArticles(r, "u2", 0) + insertFakeArticles(r, "u3", 0) + insertFakeArticles(r, "u4", 0) items, err := r.ListByPage(2, 1) if err != nil { @@ -109,8 +109,44 @@ func TestPullingMultipleArticlesWithPaging(t *testing.T) { } } -func insertFakeArticles(r repository.ArticleRepository, title string) error { - _, err := r.Create(1, "", title, articleFakeDotCom, "", "testing", "", "", time.Now(), false) +func TestPullingByPublishDate(t *testing.T) { + db, err := setupInMemoryDb() + if err != nil { + t.Log(err) + t.FailNow() + } + defer db.Close() + r := repository.NewArticleRepository(db) + + today := time.Now() + + insertFakeArticles(r, "u1", 0) + insertFakeArticles(r, "u1", -1) + insertFakeArticles(r, "u1", -2) + + items, err := r.ListByPublishDate(0, 2, repository.ArticleOrderByPublishDateDesc) + if err != nil { + t.Log(err) + t.FailNow() + } + + if len(items) != 2 { + 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") + t.FailNow() + } +} + +//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 { return err } diff --git a/internal/repository/discordWebHooks.go b/internal/repository/discordWebHooks.go new file mode 100644 index 0000000..37dca24 --- /dev/null +++ b/internal/repository/discordWebHooks.go @@ -0,0 +1,256 @@ +package repository + +import ( + "context" + "database/sql" + "time" + + "git.jamestombleson.com/jtom38/newsbot-api/internal/domain" + "github.com/huandu/go-sqlbuilder" +) + +type discordWebHookRepository struct { + conn *sql.DB +} + +func NewDiscordWebHookRepository(conn *sql.DB) discordWebHookRepository { + return discordWebHookRepository{ + conn: conn, + } +} + +func (r discordWebHookRepository) Create(ctx context.Context, url, server, channel string, enabled bool) (int64, error) { + 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) + query, args := queryBuilder.Build() + + _, err := r.conn.ExecContext(ctx, query, args...) + if err != nil { + return 0, err + } + + return 1, nil +} + +func (r discordWebHookRepository) Enable(ctx context.Context, id int64) (int64, error) { + b := sqlbuilder.NewUpdateBuilder() + b.Update("DiscordWebHooks") + b.Set( + b.Assign("Enabled", true), + b.Assign("UpdatedAt", time.Now()), + ) + query, args := b.Build() + + _, err := r.conn.ExecContext(ctx, query, args...) + if err != nil { + return 0, err + } + + return 1, nil +} + +func (r discordWebHookRepository) Disable(ctx context.Context, id int64) (int64, error) { + b := sqlbuilder.NewUpdateBuilder() + b.Update("DiscordWebHooks") + b.Set( + b.Assign("Enabled", false), + b.Assign("UpdatedAt", time.Now()), + ) + query, args := b.Build() + + _, err := r.conn.ExecContext(ctx, query, args...) + if err != nil { + return 0, err + } + + return 1, nil +} + +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 +} + +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 +} + +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 +} + +func (r discordWebHookRepository) GetById(ctx context.Context, id int64) (domain.DiscordWebHookEntity, error) { + builder := sqlbuilder.NewSelectBuilder() + builder.Select("*") + builder.From("DiscordWebHooks").Where( + builder.E("id", id), + ) + builder.Limit(1) + + query, args := builder.Build() + rows, err := r.conn.QueryContext(ctx, query, args...) + if err != nil { + return domain.DiscordWebHookEntity{}, err + } + + data, err := r.processRows(rows) + if len(data) == 0 { + return domain.DiscordWebHookEntity{}, err + } + + return data[0], nil +} + +func (r discordWebHookRepository) GetByUrl(ctx context.Context, url string) (domain.DiscordWebHookEntity, error) { + builder := sqlbuilder.NewSelectBuilder() + builder.Select("*") + builder.From("DiscordWebHooks").Where( + builder.E("Url", url), + ) + builder.Limit(1) + + query, args := builder.Build() + rows, err := r.conn.QueryContext(ctx, query, args...) + if err != nil { + return domain.DiscordWebHookEntity{}, err + } + + data, err := r.processRows(rows) + if len(data) == 0 { + return domain.DiscordWebHookEntity{}, err + } + + return data[0], nil +} + +func (r discordWebHookRepository) ListByServerName(ctx context.Context, name string) ([]domain.DiscordWebHookEntity, error) { + builder := sqlbuilder.NewSelectBuilder() + builder.Select("*") + builder.From("DiscordWebHooks").Where( + builder.E("Server", name), + ) + + query, args := builder.Build() + rows, err := r.conn.QueryContext(ctx, query, args...) + if err != nil { + return []domain.DiscordWebHookEntity{}, err + } + + data, err := r.processRows(rows) + if len(data) == 0 { + return []domain.DiscordWebHookEntity{}, err + } + + return data, nil +} + +func (r discordWebHookRepository) ListByServerAndChannel(ctx context.Context, server, channel string) ([]domain.DiscordWebHookEntity, error) { + builder := sqlbuilder.NewSelectBuilder() + builder.Select("*") + builder.From("DiscordWebHooks").Where( + builder.Equal("Server", server), + builder.Equal("Channel", channel), + ) + + query, args := builder.Build() + rows, err := r.conn.QueryContext(ctx, query, args...) + if err != nil { + return []domain.DiscordWebHookEntity{}, err + } + + data, err := r.processRows(rows) + if len(data) == 0 { + return []domain.DiscordWebHookEntity{}, err + } + + return data, nil +} + +func (r discordWebHookRepository) processRows(rows *sql.Rows) ([]domain.DiscordWebHookEntity, error) { + items := []domain.DiscordWebHookEntity{} + + for rows.Next() { + var id int64 + var createdAt time.Time + var updatedAt time.Time + var deletedAt sql.NullTime + var url string + var server string + var channel string + var enabled bool + err := rows.Scan( + &id, &createdAt, &updatedAt, + &deletedAt, &url, &server, + &channel, &enabled, + ) + if err != nil { + return items, err + } + + item := domain.DiscordWebHookEntity{ + ID: id, + CreatedAt: createdAt, + UpdatedAt: updatedAt, + Url: url, + Server: server, + Channel: channel, + Enabled: enabled, + } + + if deletedAt.Valid { + item.DeletedAt = deletedAt.Time + } + + items = append(items, item) + } + + return items, nil +} diff --git a/internal/repository/discordWebHooks_test.go b/internal/repository/discordWebHooks_test.go new file mode 100644 index 0000000..122299d --- /dev/null +++ b/internal/repository/discordWebHooks_test.go @@ -0,0 +1,287 @@ +package repository_test + +import ( + "context" + "testing" + "time" + + "git.jamestombleson.com/jtom38/newsbot-api/internal/repository" +) + +func TestCreateDiscordWebHookRecord(t *testing.T) { + db, err := setupInMemoryDb() + if err != nil { + t.Log(err) + t.FailNow() + } + defer db.Close() + + r := repository.NewDiscordWebHookRepository(db) + created, err := r.Create(context.Background(), "www.discord.com/bad/webhook", "Unit Testing", "memes", true) + if err != nil { + t.Log(err) + t.FailNow() + } + + if created != 1 { + t.Log("failed to create the record") + t.FailNow() + } +} + +func TestDiscordWebHookGetById(t *testing.T) { + db, err := setupInMemoryDb() + if err != nil { + t.Log(err) + t.FailNow() + } + defer db.Close() + ctx := context.Background() + r := repository.NewDiscordWebHookRepository(db) + created, err := r.Create(ctx, "www.discord.com/bad/webhook", "Unit Testing", "memes", true) + if err != nil { + t.Log(err) + t.FailNow() + } + + if created != 1 { + t.Log("failed to create the record") + t.FailNow() + } + + item, err := r.GetById(ctx, 1) + if err != nil { + t.Log(err) + t.FailNow() + } + + if item.ID != 1 { + t.Log("got the wrong record back") + t.FailNow() + } +} + +func TestDiscordWebHookGetByUrl(t *testing.T) { + db, err := setupInMemoryDb() + if err != nil { + t.Log(err) + t.FailNow() + } + defer db.Close() + + ctx := context.Background() + r := repository.NewDiscordWebHookRepository(db) + _, _ = r.Create(ctx, "www.discord.com/bad/webhook", "Unit Testing", "memes", true) + item, err := r.GetByUrl(ctx, "www.discord.com/bad/webhook") + if err != nil { + t.Log(err) + t.FailNow() + } + + if item.Url != "www.discord.com/bad/webhook" { + t.Log("got the wrong record back") + t.FailNow() + } +} + +func TestDiscordWebHookListByServerName(t *testing.T) { + db, err := setupInMemoryDb() + if err != nil { + t.Log(err) + t.FailNow() + } + defer db.Close() + + ctx := context.Background() + serverName := "Unit Testing" + r := repository.NewDiscordWebHookRepository(db) + _, _ = r.Create(ctx, "www.discord.com/bad/webhook", serverName, "memes", true) + + item, err := r.ListByServerName(ctx, serverName) + if err != nil { + t.Log(err) + t.FailNow() + } + + if item[0].Server != serverName { + t.Log("got the wrong record back") + t.FailNow() + } +} + +func TestDiscordWebHookListByServerAndChannel(t *testing.T) { + db, err := setupInMemoryDb() + if err != nil { + t.Log(err) + t.FailNow() + } + defer db.Close() + + ctx := context.Background() + serverName := "Unit Testing" + channel := "memes" + r := repository.NewDiscordWebHookRepository(db) + _, _ = r.Create(ctx, "www.discord.com/bad/webhook", serverName, channel, true) + + item, err := r.ListByServerAndChannel(ctx, serverName, channel) + if err != nil { + t.Log(err) + t.FailNow() + } + + if item[0].Server != serverName { + t.Log("got the wrong wrong server back") + t.FailNow() + } + + if item[0].Channel != channel { + t.Log("got the wrong channel back") + t.FailNow() + } +} + +func TestDiscordWebHookEnableRecord(t *testing.T) { + db, err := setupInMemoryDb() + if err != nil { + t.Log(err) + t.FailNow() + } + defer db.Close() + + ctx := context.Background() + serverName := "Unit Testing" + channel := "memes" + r := repository.NewDiscordWebHookRepository(db) + _, _ = r.Create(ctx, "www.discord.com/bad/webhook", serverName, channel, false) + + item, err := r.GetById(ctx, 1) + 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, 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() + } +} + +func TestDiscordWebHookDisableRecord(t *testing.T) { + db, err := setupInMemoryDb() + if err != nil { + t.Log(err) + t.FailNow() + } + defer db.Close() + + ctx := context.Background() + serverName := "Unit Testing" + channel := "memes" + r := repository.NewDiscordWebHookRepository(db) + _, _ = r.Create(ctx, "www.discord.com/bad/webhook", serverName, channel, true) + + item, err := r.GetById(ctx, 1) + 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() + } +} + +func TestDiscordWebHookSoftDelete(t *testing.T) { + db, err := setupInMemoryDb() + if err != nil { + t.Log(err) + t.FailNow() + } + defer db.Close() + + ctx := context.Background() + serverName := "Unit Testing" + channel := "memes" + r := repository.NewDiscordWebHookRepository(db) + _, _ = r.Create(ctx, "www.discord.com/bad/webhook", serverName, channel, true) + _, err = r.SoftDelete(ctx, 1) + if err != nil { + t.Log(err) + t.FailNow() + } + + updated, _ := r.GetById(ctx, 1) + t.Log(updated.DeletedAt) +} + +func TestDiscordWebHookRestore(t *testing.T) { + db, err := setupInMemoryDb() + if err != nil { + t.Log(err) + t.FailNow() + } + defer db.Close() + + ctx := context.Background() + serverName := "Unit Testing" + channel := "memes" + timeZero := time.Time{} + + r := repository.NewDiscordWebHookRepository(db) + _, _ = r.Create(ctx, "www.discord.com/bad/webhook", serverName, channel, true) + item, _ := r.GetById(ctx, 1) + if item.DeletedAt != timeZero { + t.Log("DeletedAt was not zero") + t.FailNow() + } + _, _ = r.SoftDelete(ctx, 1) + softDeleted, _ := r.GetById(ctx, 1) + if softDeleted.ID != 1 { + t.Log("record went boom") + t.FailNow() + } + + _, err = r.Restore(ctx, 1) + if err != nil { + t.Log(err) + t.FailNow() + } + + updated, _ := r.GetById(ctx, 1) + t.Log(updated.DeletedAt) +} diff --git a/internal/repository/users_test.go b/internal/repository/users_test.go index c5d18db..849338e 100644 --- a/internal/repository/users_test.go +++ b/internal/repository/users_test.go @@ -12,7 +12,6 @@ import ( ) func TestCanCreateNewUser(t *testing.T) { - //t.Log(time.Now().String()) db, err := setupInMemoryDb() if err != nil { t.Log(err) -- 2.45.2 From 72277446217471758169754dffac4d53734dbd21 Mon Sep 17 00:00:00 2001 From: James Tombleson Date: Sun, 28 Apr 2024 10:02:57 -0700 Subject: [PATCH 05/13] 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 . -- 2.45.2 From 9586c6a544c116e3f95d619a0f5b28713633912c Mon Sep 17 00:00:00 2001 From: James Tombleson Date: Sun, 28 Apr 2024 11:39:25 -0700 Subject: [PATCH 06/13] repositories now use context and have interfaces exposed --- internal/repository/article.go | 46 +++++++++++++++++--------- internal/repository/article_test.go | 13 ++++---- internal/repository/discordWebHooks.go | 13 ++++++++ internal/repository/refreshTokens.go | 2 +- internal/repository/source.go | 14 ++++++++ internal/repository/users.go | 22 ++++++------ 6 files changed, 76 insertions(+), 34 deletions(-) diff --git a/internal/repository/article.go b/internal/repository/article.go index 1c92b38..0652ba6 100644 --- a/internal/repository/article.go +++ b/internal/repository/article.go @@ -1,6 +1,7 @@ package repository import ( + "context" "database/sql" "errors" "fmt" @@ -12,9 +13,19 @@ import ( const ( ArticleOrderByPublishDateDesc = "pubDate desc" - ArticleOrderByPublishDatAsc = "pubDate asc" + ArticleOrderByPublishDateAsc = "pubDate asc" ) +type ArticlesRepo interface { + GetById(ctx context.Context, id int64) (domain.ArticleEntity, error) + GetByUrl(ctx context.Context, url string) (domain.ArticleEntity, error) + ListTop(ctx context.Context, limit int) ([]domain.ArticleEntity, error) + ListByPage(ctx context.Context, page, limit int) ([]domain.ArticleEntity, error) + ListByPublishDate(ctx context.Context, page, limit int, orderBy string) ([]domain.ArticleEntity, error) + ListBySource(ctx context.Context, page, limit, sourceId int, orderBy string) ([]domain.ArticleEntity, error) + Create(ctx context.Context, sourceId int64, tags, title, url, thumbnailUrl, description, authorName, authorImageUrl string, pubDate time.Time, isVideo bool) (int64, error) +} + type ArticleRepository struct { conn *sql.DB defaultLimit int @@ -29,7 +40,7 @@ func NewArticleRepository(conn *sql.DB) ArticleRepository { } } -func (ar ArticleRepository) GetById(id int64) (domain.ArticleEntity, error) { +func (ar ArticleRepository) GetById(ctx context.Context, id int64) (domain.ArticleEntity, error) { builder := sqlbuilder.NewSelectBuilder() builder.Select("*") builder.From("articles").Where( @@ -38,7 +49,7 @@ func (ar ArticleRepository) GetById(id int64) (domain.ArticleEntity, error) { builder.Limit(1) query, args := builder.Build() - rows, err := ar.conn.Query(query, args...) + rows, err := ar.conn.QueryContext(ctx, query, args...) if err != nil { return domain.ArticleEntity{}, err } @@ -51,7 +62,7 @@ func (ar ArticleRepository) GetById(id int64) (domain.ArticleEntity, error) { return data[0], nil } -func (ar ArticleRepository) GetByUrl(url string) (domain.ArticleEntity, error) { +func (ar ArticleRepository) GetByUrl(ctx context.Context, url string) (domain.ArticleEntity, error) { builder := sqlbuilder.NewSelectBuilder() builder.Select("*") builder.From("articles").Where( @@ -60,7 +71,7 @@ func (ar ArticleRepository) GetByUrl(url string) (domain.ArticleEntity, error) { builder.Limit(1) query, args := builder.Build() - rows, err := ar.conn.Query(query, args...) + rows, err := ar.conn.QueryContext(ctx, query, args...) if err != nil { return domain.ArticleEntity{}, err } @@ -73,14 +84,14 @@ func (ar ArticleRepository) GetByUrl(url string) (domain.ArticleEntity, error) { return data[0], nil } -func (ar ArticleRepository) ListTop(limit int) ([]domain.ArticleEntity, error) { +func (ar ArticleRepository) ListTop(ctx context.Context, limit int) ([]domain.ArticleEntity, error) { builder := sqlbuilder.NewSelectBuilder() builder.Select("*") builder.From("articles") builder.Limit(limit) query, args := builder.Build() - rows, err := ar.conn.Query(query, args...) + rows, err := ar.conn.QueryContext(ctx, query, args...) if err != nil { return []domain.ArticleEntity{}, err } @@ -93,16 +104,16 @@ func (ar ArticleRepository) ListTop(limit int) ([]domain.ArticleEntity, error) { return data, nil } -func (ar ArticleRepository) ListByPage(page, limit int) ([]domain.ArticleEntity, error) { +func (ar ArticleRepository) ListByPage(ctx context.Context, page, limit int) ([]domain.ArticleEntity, error) { builder := sqlbuilder.NewSelectBuilder() builder.Select("*") builder.From("articles") - builder.OrderBy("pubdate desc") + builder.OrderBy(ArticleOrderByPublishDateDesc) builder.Offset(page * limit) builder.Limit(limit) query, args := builder.Build() - rows, err := ar.conn.Query(query, args...) + rows, err := ar.conn.QueryContext(ctx, query, args...) if err != nil { return []domain.ArticleEntity{}, err } @@ -115,7 +126,7 @@ func (ar ArticleRepository) ListByPage(page, limit int) ([]domain.ArticleEntity, return data, nil } -func (ar ArticleRepository) ListByPublishDate(page, limit int, orderBy string) ([]domain.ArticleEntity, error) { +func (ar ArticleRepository) ListByPublishDate(ctx context.Context, page, limit int, orderBy string) ([]domain.ArticleEntity, error) { builder := sqlbuilder.NewSelectBuilder() builder.Select("*") builder.From("articles") @@ -126,7 +137,7 @@ func (ar ArticleRepository) ListByPublishDate(page, limit int, orderBy string) ( builder.Limit(limit) query, args := builder.Build() - rows, err := ar.conn.Query(query, args...) + rows, err := ar.conn.QueryContext(ctx, query, args...) if err != nil { return []domain.ArticleEntity{}, err } @@ -138,7 +149,7 @@ func (ar ArticleRepository) ListByPublishDate(page, limit int, orderBy string) ( return data, nil } -func (ar ArticleRepository) ListBySource(page, limit int, orderBy string) ([]domain.ArticleEntity, error) { +func (ar ArticleRepository) ListBySource(ctx context.Context, page, limit, sourceId int, orderBy string) ([]domain.ArticleEntity, error) { builder := sqlbuilder.NewSelectBuilder() builder.Select("*") builder.From("articles") @@ -146,11 +157,14 @@ func (ar ArticleRepository) ListBySource(page, limit int, orderBy string) ([]dom if orderBy != "" { builder.OrderBy(orderBy) } + builder.Where( + builder.Equal("SourceId", sourceId), + ) builder.Offset(50) builder.Limit(page * limit) query, args := builder.Build() - rows, err := ar.conn.Query(query, args...) + rows, err := ar.conn.QueryContext(ctx, query, args...) if err != nil { return []domain.ArticleEntity{}, err } @@ -162,7 +176,7 @@ func (ar ArticleRepository) ListBySource(page, limit int, orderBy string) ([]dom return data, nil } -func (ar ArticleRepository) Create(sourceId int64, tags, title, url, thumbnailUrl, description, authorName, authorImageUrl string, pubDate time.Time, isVideo bool) (int64, error) { +func (ar ArticleRepository) Create(ctx context.Context, sourceId int64, tags, title, url, thumbnailUrl, description, authorName, authorImageUrl string, pubDate time.Time, isVideo bool) (int64, error) { dt := time.Now() queryBuilder := sqlbuilder.NewInsertBuilder() queryBuilder.InsertInto("articles") @@ -170,7 +184,7 @@ func (ar ArticleRepository) Create(sourceId int64, tags, title, url, thumbnailUr 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...) + _, err := ar.conn.ExecContext(ctx, query, args...) if err != nil { return 0, err } diff --git a/internal/repository/article_test.go b/internal/repository/article_test.go index ba81544..d907c60 100644 --- a/internal/repository/article_test.go +++ b/internal/repository/article_test.go @@ -1,6 +1,7 @@ package repository_test import ( + "context" "testing" "time" @@ -21,7 +22,7 @@ func TestCreateArticle(t *testing.T) { defer db.Close() r := repository.NewArticleRepository(db) - created, err := r.Create(1, "", "unit test", articleFakeDotCom, "", "testing", "", "", time.Now(), false) + created, err := r.Create(context.Background(), 1, "", "unit test", articleFakeDotCom, "", "testing", "", "", time.Now(), false) if err != nil { t.Log(err) t.FailNow() @@ -48,7 +49,7 @@ func TestArticleByUrl(t *testing.T) { t.FailNow() } - article, err := r.GetByUrl(articleFakeDotCom) + article, err := r.GetByUrl(context.Background(), articleFakeDotCom) if err != nil { t.Log(err) t.FailNow() @@ -73,7 +74,7 @@ func TestPullingMultipleArticlesWithLimit(t *testing.T) { insertFakeArticles(r, "u3", 0) insertFakeArticles(r, "u4", 0) - items, err := r.ListTop(3) + items, err := r.ListTop(context.Background(), 3) if err != nil { t.Log(err) t.FailNow() @@ -98,7 +99,7 @@ func TestPullingMultipleArticlesWithPaging(t *testing.T) { insertFakeArticles(r, "u3", 0) insertFakeArticles(r, "u4", 0) - items, err := r.ListByPage(2, 1) + items, err := r.ListByPage(context.Background(), 2, 1) if err != nil { t.Log(err) t.FailNow() @@ -125,7 +126,7 @@ func TestPullingByPublishDate(t *testing.T) { insertFakeArticles(r, "u1", -1) insertFakeArticles(r, "u1", -2) - items, err := r.ListByPublishDate(0, 2, repository.ArticleOrderByPublishDateDesc) + items, err := r.ListByPublishDate(context.Background(), 0, 2, repository.ArticleOrderByPublishDateDesc) if err != nil { t.Log(err) t.FailNow() @@ -147,7 +148,7 @@ func TestPullingByPublishDate(t *testing.T) { 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) + _, err := r.Create(context.Background(), 1, "", title, articleFakeDotCom, "", "testing", "", "", pubDate, false) if err != nil { return err } diff --git a/internal/repository/discordWebHooks.go b/internal/repository/discordWebHooks.go index dead8c4..e549897 100644 --- a/internal/repository/discordWebHooks.go +++ b/internal/repository/discordWebHooks.go @@ -9,6 +9,19 @@ import ( "github.com/huandu/go-sqlbuilder" ) +type DiscordWebHookRepo interface{ + Create(ctx context.Context, url, server, channel string, enabled bool) (int64, error) + Enable(ctx context.Context, id int64) (int64, error) + Disable(ctx context.Context, id int64) (int64, error) + SoftDelete(ctx context.Context, id int64) (int64, error) + Restore(ctx context.Context, id int64) (int64, error) + Delete(ctx context.Context, id int64) (int64, error) + GetById(ctx context.Context, id int64) (domain.DiscordWebHookEntity, error) + GetByUrl(ctx context.Context, url string) (domain.DiscordWebHookEntity, error) + ListByServerName(ctx context.Context, name string) ([]domain.DiscordWebHookEntity, error) + ListByServerAndChannel(ctx context.Context, server, channel string) ([]domain.DiscordWebHookEntity, error) +} + type discordWebHookRepository struct { conn *sql.DB } diff --git a/internal/repository/refreshTokens.go b/internal/repository/refreshTokens.go index 3a46a8c..fc6ce83 100644 --- a/internal/repository/refreshTokens.go +++ b/internal/repository/refreshTokens.go @@ -14,7 +14,7 @@ const ( refreshTokenTableName = "RefreshTokens" ) -type RefreshTokenTable interface { +type RefreshToken interface { Create(username string, token string) (int64, error) GetByUsername(name string) (domain.RefreshTokenEntity, error) DeleteById(id int64) (int64, error) diff --git a/internal/repository/source.go b/internal/repository/source.go index 2073768..e17bd61 100644 --- a/internal/repository/source.go +++ b/internal/repository/source.go @@ -9,6 +9,20 @@ import ( "github.com/huandu/go-sqlbuilder" ) +type Sources interface { + Create(ctx context.Context, source, displayName, url, tags string, enabled bool) (int64, error) + GetById(ctx context.Context, id int64) (domain.SourceEntity, error) + GetByDisplayName(ctx context.Context, displayName string) (domain.SourceEntity, error) + GetBySource(ctx context.Context, source string) (domain.SourceEntity, error) + List(ctx context.Context, page, limit int) ([]domain.SourceEntity, error) + ListBySource(ctx context.Context, page, limit int, source string) ([]domain.SourceEntity, error) + Enable(ctx context.Context, id int64) (int64, error) + Disable(ctx context.Context, id int64) (int64, error) + SoftDelete(ctx context.Context, id int64) (int64, error) + Restore(ctx context.Context, id int64) (int64, error) + Delete(ctx context.Context, id int64) (int64, error) +} + type sourceRepository struct { conn *sql.DB } diff --git a/internal/repository/users.go b/internal/repository/users.go index 416a093..209c6f5 100644 --- a/internal/repository/users.go +++ b/internal/repository/users.go @@ -17,7 +17,7 @@ const ( ErrUserNotFound string = "requested user was not found" ) -type IUserTable interface { +type Users interface { GetByName(name string) (domain.UserEntity, error) Create(name, password, scope string) (int64, error) Update(id int, entity domain.UserEntity) error @@ -27,17 +27,17 @@ type IUserTable interface { } // Creates a new instance of UserRepository with the bound sql -func NewUserRepository(conn *sql.DB) UserRepository { - return UserRepository{ +func NewUserRepository(conn *sql.DB) userRepository { + return userRepository{ connection: conn, } } -type UserRepository struct { +type userRepository struct { connection *sql.DB } -func (ur UserRepository) GetByName(name string) (domain.UserEntity, error) { +func (ur userRepository) GetByName(name string) (domain.UserEntity, error) { builder := sqlbuilder.NewSelectBuilder() builder.Select("*").From("users").Where( builder.E("Name", name), @@ -57,7 +57,7 @@ func (ur UserRepository) GetByName(name string) (domain.UserEntity, error) { return data[0], nil } -func (ur UserRepository) Create(name, password, scope string) (int64, error) { +func (ur userRepository) Create(name, password, scope string) (int64, error) { passwordBytes := []byte(password) hash, err := bcrypt.GenerateFromPassword(passwordBytes, bcrypt.DefaultCost) if err != nil { @@ -79,11 +79,11 @@ func (ur UserRepository) Create(name, password, scope string) (int64, error) { return 1, nil } -func (ur UserRepository) Update(id int, entity domain.UserEntity) error { +func (ur userRepository) Update(id int, entity domain.UserEntity) error { return errors.New("not implemented") } -func (ur UserRepository) UpdatePassword(name, password string) error { +func (ur userRepository) UpdatePassword(name, password string) error { _, err := ur.GetByName(name) if err != nil { return nil @@ -97,7 +97,7 @@ func (ur UserRepository) UpdatePassword(name, password string) error { // If the hash matches what we have in the database, an error will not be returned. // If the user does not exist or the hash does not match, an error will be returned -func (ur UserRepository) CheckUserHash(name, password string) error { +func (ur userRepository) CheckUserHash(name, password string) error { record, err := ur.GetByName(name) if err != nil { return err @@ -111,7 +111,7 @@ func (ur UserRepository) CheckUserHash(name, password string) error { return nil } -func (ur UserRepository) UpdateScopes(name, scope string) error { +func (ur userRepository) UpdateScopes(name, scope string) error { builder := sqlbuilder.NewUpdateBuilder() builder.Update("users") builder.Set( @@ -129,7 +129,7 @@ func (ur UserRepository) UpdateScopes(name, scope string) error { return nil } -func (ur UserRepository) processRows(rows *sql.Rows) []domain.UserEntity { +func (ur userRepository) processRows(rows *sql.Rows) []domain.UserEntity { items := []domain.UserEntity{} for rows.Next() { -- 2.45.2 From ef15af6cbd55b125ee62d5d670ee4ad28fad9db4 Mon Sep 17 00:00:00 2001 From: James Tombleson Date: Sun, 28 Apr 2024 11:40:19 -0700 Subject: [PATCH 07/13] cleaned up the article handler with new reponse models and moving to the repo structs --- internal/handler/v1/articles.go | 178 +++++++++++++------------------- 1 file changed, 70 insertions(+), 108 deletions(-) diff --git a/internal/handler/v1/articles.go b/internal/handler/v1/articles.go index 5703404..8bae404 100644 --- a/internal/handler/v1/articles.go +++ b/internal/handler/v1/articles.go @@ -4,186 +4,148 @@ import ( "net/http" "strconv" - "git.jamestombleson.com/jtom38/newsbot-api/internal/domain/models" - "github.com/google/uuid" + "git.jamestombleson.com/jtom38/newsbot-api/internal/domain" + "git.jamestombleson.com/jtom38/newsbot-api/internal/services" "github.com/labstack/echo/v4" ) -//func (s *Handler) GetArticleRouter() http.Handler { -// r := chi.NewRouter() -// -// r.Get("/", s.listArticles) -// r.Route("/{ID}", func(r chi.Router) { -// r.Get("/", s.getArticle) -// r.Get("/details", s.getArticleDetails) -// }) -// r.Get("/by/sourceid", s.ListArticlesBySourceId) -// -// return r -//} - -type ArticlesListResults struct { - ApiStatusModel - Payload []models.ArticleDto `json:"payload"` -} - -type ArticleGetResults struct { - ApiStatusModel - Payload models.ArticleDto `json:"payload"` -} - -type ArticleDetailsResult struct { - ApiStatusModel - Payload models.ArticleDetailsDto `json:"payload"` -} - // ListArticles // @Summary Lists the top 25 records ordering from newest to oldest. // @Produce application/json // @Param page query string false "page number" // @Tags Articles // @Router /articles [get] -// @Success 200 {object} ArticlesListResults "OK" +// @Success 200 {object} domain.ArticleResponse +// @Failure 400 {object} domain.BaseResponse +// @Failure 500 {object} domain.BaseResponse func (s *Handler) listArticles(c echo.Context) error { - p := ArticlesListResults{ - ApiStatusModel: ApiStatusModel{ - Message: "OK", - StatusCode: http.StatusOK, + resp := domain.ArticleResponse{ + BaseResponse: domain.BaseResponse{ + Message: ResponseMessageSuccess, }, } - queryPage := c.QueryParam("page") - - // if a page number was sent, process it - if len(queryPage) >= 1 { - page, err := strconv.Atoi(queryPage) - if err != nil { - return c.JSON(http.StatusBadRequest, err) - } - - res, err := s.dto.ListArticles(c.Request().Context(), 25, page) - if err != nil { - return c.JSON(http.StatusInternalServerError, err) - } - p.Payload = res - return c.JSON(http.StatusOK, p) - } else { - res, err := s.dto.ListArticles(c.Request().Context(), 25, 0) - if err != nil { - return c.JSON(http.StatusInternalServerError, err) - } - p.Payload = res - return c.JSON(http.StatusOK, p) + page, err := strconv.Atoi(c.QueryParam("page")) + if err != nil { + page = 0 } + + res, err := s.repo.Articles.ListByPage(c.Request().Context(), page, 25) + if err != nil { + s.WriteError(c, err, http.StatusInternalServerError) + } + + resp.Payload = services.ArticlesToDto(res) + return c.JSON(http.StatusOK, resp) } // GetArticle // @Summary Returns an article based on defined ID. -// @Param ID path string true "uuid" +// @Param ID path string true "int" // @Produce application/json // @Tags Articles // @Router /articles/{ID} [get] // @Success 200 {object} ArticleGetResults "OK" +// @Failure 400 {object} domain.BaseResponse +// @Failure 500 {object} domain.BaseResponse func (s *Handler) getArticle(c echo.Context) error { - p := ArticleGetResults{ - ApiStatusModel: ApiStatusModel{ - Message: "OK", - StatusCode: http.StatusOK, + p := domain.ArticleResponse{ + BaseResponse: domain.BaseResponse{ + Message: ResponseMessageSuccess, }, } id := c.Param("ID") - uuid, err := uuid.Parse(id) + idNumber, err := strconv.Atoi(id) if err != nil { - return c.JSON(http.StatusBadRequest, err) + s.WriteError(c, err, http.StatusBadRequest) } - res, err := s.dto.GetArticle(c.Request().Context(), uuid) + item, err := s.repo.Articles.GetById(c.Request().Context(), int64(idNumber)) if err != nil { return c.JSON(http.StatusInternalServerError, err) } - p.Payload = res + var dtos []domain.ArticleDto + dtos = append(dtos, services.ArticleToDto(item)) + p.Payload = dtos return c.JSON(http.StatusOK, p) } // GetArticleDetails // @Summary Returns an article and source based on defined ID. -// @Param ID path string true "uuid" +// @Param ID path string true "int" // @Produce application/json // @Tags Articles // @Router /articles/{ID}/details [get] // @Success 200 {object} ArticleDetailsResult "OK" +// @Failure 400 {object} domain.BaseResponse +// @Failure 500 {object} domain.BaseResponse func (s *Handler) getArticleDetails(c echo.Context) error { - p := ArticleDetailsResult{ - ApiStatusModel: ApiStatusModel{ - Message: "OK", - StatusCode: http.StatusOK, + p := domain.ArticleDetailResponse{ + BaseResponse: domain.BaseResponse{ + Message: ResponseMessageSuccess, + }, + Payload: domain.ArticleAndSourceModel{ + }, } - id := c.Param("ID") - uuid, err := uuid.Parse(id) + id, err := strconv.Atoi(c.Param("ID")) if err != nil { - return c.JSON(http.StatusBadRequest, err) + s.WriteError(c, err, http.StatusBadRequest) } - res, err := s.dto.GetArticleDetails(c.Request().Context(), uuid) + article, err := s.repo.Articles.GetById(c.Request().Context(), int64(id)) if err != nil { - return c.JSON(http.StatusInternalServerError, err) + s.WriteError(c, err, http.StatusInternalServerError) } - p.Payload = res + source, err := s.repo.Sources.GetById(c.Request().Context(), article.SourceID) + if err != nil { + s.WriteError(c, err, http.StatusInternalServerError) + } + + p.Payload.Article = services.ArticleToDto(article) + p.Payload.Source = services.SourceToDto(source) return c.JSON(http.StatusOK, p) } -// TODO add page support // ListArticlesBySourceID // @Summary Finds the articles based on the SourceID provided. Returns the top 25. -// @Param id query string true "Source ID UUID" +// @Param id query string true // @Param page query int false "Page to query" // @Produce application/json // @Tags Articles // @Router /articles/by/sourceid [get] // @Success 200 {object} ArticlesListResults "OK" +// @Failure 400 {object} domain.BaseResponse +// @Failure 500 {object} domain.BaseResponse func (s *Handler) ListArticlesBySourceId(c echo.Context) error { - p := ArticlesListResults{ - ApiStatusModel: ApiStatusModel{ - Message: "OK", - StatusCode: http.StatusOK, + p := domain.ArticleResponse{ + BaseResponse: domain.BaseResponse{ + Message: ResponseMessageSuccess, }, } - id := c.QueryParam("id") - - uuid, err := uuid.Parse(id) + id, err := strconv.Atoi(c.QueryParam("id")) if err != nil { - return c.JSON(http.StatusBadRequest, err) + s.WriteError(c, err, http.StatusBadRequest) } - // if a page number was sent, process it - if len(c.QueryParam("page")) >= 1 { - _page, err := strconv.Atoi(c.QueryParam("page")) - if err != nil { - return c.JSON(http.StatusBadRequest, err) - } - - res, err := s.dto.ListNewArticlesBySourceId(c.Request().Context(), uuid, 25, _page) - if err != nil { - return c.JSON(http.StatusInternalServerError, err) - } - - p.Payload = res - } else { - res, err := s.dto.ListNewArticlesBySourceId(c.Request().Context(), uuid, 25, 0) - if err != nil { - return c.JSON(http.StatusInternalServerError, err) - } - - p.Payload = res + // if the page number is missing, default to 0 + _page, err := strconv.Atoi(c.QueryParam("page")) + if err != nil { + _page = 0 } + + items, err := s.repo.Articles.ListBySource(c.Request().Context(), _page, 25, id, "") + if err != nil { + return c.JSON(http.StatusInternalServerError, err) + } + + p.Payload = services.ArticlesToDto(items) return c.JSON(http.StatusOK, p) - } -- 2.45.2 From bcbdfcbc5b6054aaf485075d1cafe2110a1b4006 Mon Sep 17 00:00:00 2001 From: James Tombleson Date: Sun, 28 Apr 2024 11:40:51 -0700 Subject: [PATCH 08/13] Created a new services.RepositoryService to roll up all the db calls --- internal/handler/v1/handler.go | 70 ++++++++++++---------------------- internal/services/database.go | 21 +++++++++- 2 files changed, 45 insertions(+), 46 deletions(-) diff --git a/internal/handler/v1/handler.go b/internal/handler/v1/handler.go index 26947c2..655f72a 100644 --- a/internal/handler/v1/handler.go +++ b/internal/handler/v1/handler.go @@ -15,22 +15,24 @@ import ( ) type Handler struct { - Router *echo.Echo - Db *database.Queries - dto *dto.DtoClient - config services.Configs - sqlConnection *sql.DB + Router *echo.Echo + Db *database.Queries + dto *dto.DtoClient + config services.Configs + repo services.RepositoryService } const ( HeaderContentType = "Content-Type" - ApplicationJson = "application/json" + //ApplicationJson = "application/json" ErrParameterIdMissing = "The requested parameter ID was not found." ErrParameterMissing = "The requested parameter was found found:" ErrUnableToParseId = "Unable to parse the requested ID." ErrRecordMissing = "The requested record was not found" + + ResponseMessageSuccess = "Success" ) var ( @@ -42,18 +44,12 @@ var ( func NewServer(ctx context.Context, db *database.Queries, configs services.Configs, conn *sql.DB) *Handler { s := &Handler{ - Db: db, - dto: dto.NewDtoClient(db), - config: configs, - sqlConnection: conn, + Db: db, + dto: dto.NewDtoClient(db), + config: configs, + repo: services.NewRepositoryService(conn), } - db, err := openDatabase(ctx) - if err != nil { - panic(err) - } - s.Db = db - router := echo.New() router.GET("/swagger/*", swagger.WrapHandler) @@ -73,11 +69,11 @@ func NewServer(ctx context.Context, db *database.Queries, configs services.Confi dwh.POST("/:ID/disable", s.disableDiscordWebHook) dwh.POST("/:ID/enable", s.enableDiscordWebHook) - queue := v1.Group("/queue") - queue.GET("/discord/webhooks", s.ListDiscordWebhookQueue) // TODO this needs to be reworked + //queue := v1.Group("/queue") + //queue.GET("/discord/webhooks", s.ListDiscordWebhookQueue) // TODO this needs to be reworked - settings := v1.Group("/settings") - settings.GET("/", s.getSettings) + //settings := v1.Group("/settings") + //settings.GET("/", s.getSettings) sources := v1.Group("/sources") sources.GET("/", s.listSources) @@ -103,28 +99,6 @@ func NewServer(ctx context.Context, db *database.Queries, configs services.Confi return s } -func openDatabase(ctx context.Context) (*database.Queries, error) { - _env := services.NewConfig() - connString := _env.GetConfig(services.Sql_Connection_String) - db, err := sql.Open("postgres", connString) - if err != nil { - panic(err) - } - - queries := database.New(db) - return queries, err -} - -func (s *Handler) MountRoutes() { - //s.Router.Get("/swagger/*", httpSwagger.Handler( - // httpSwagger.URL("doc.json"), //The url pointing to API definition - //)) - //s.Router.Get("/api/settings", s.getSettings) - - //s.Router.Mount("/api/sources", s.GetSourcesRouter()) - //s.Router.Mount("/api/subscriptions", s.GetSubscriptionsRouter()) -} - type ApiStatusModel struct { StatusCode int `json:"status"` Message string `json:"message"` @@ -134,8 +108,14 @@ type ApiError struct { *ApiStatusModel } -func (s *Handler) WriteError(c echo.Context, errMessage string, HttpStatusCode int) error { - return c.JSON(HttpStatusCode, domain.ErrorResponse{ - Message: errMessage, +func (s *Handler) WriteError(c echo.Context, errMessage error, HttpStatusCode int) error { + return c.JSON(HttpStatusCode, domain.BaseResponse{ + Message: errMessage.Error(), + }) +} + +func (s *Handler) WriteMessage(c echo.Context, msg string, HttpStatusCode int) error { + return c.JSON(HttpStatusCode, domain.BaseResponse{ + Message: msg, }) } diff --git a/internal/services/database.go b/internal/services/database.go index 80eb9ab..9d11878 100644 --- a/internal/services/database.go +++ b/internal/services/database.go @@ -1,6 +1,25 @@ package services -type RepositoryService struct { +import ( + "database/sql" + "git.jamestombleson.com/jtom38/newsbot-api/internal/repository" +) + +type RepositoryService struct { + Articles repository.ArticlesRepo + DiscordWebHooks repository.DiscordWebHookRepo + Sources repository.Sources + Users repository.Users + RefreshTokens repository.RefreshToken } +func NewRepositoryService(conn *sql.DB) RepositoryService { + return RepositoryService{ + Articles: repository.NewArticleRepository(conn), + DiscordWebHooks: repository.NewDiscordWebHookRepository(conn), + Sources: repository.NewSourceRepository(conn), + Users: repository.NewUserRepository(conn), + RefreshTokens: repository.NewRefreshTokenRepository(conn), + } +} -- 2.45.2 From 0073bb6775069485dc0d91bd466cc1abc2718fe9 Mon Sep 17 00:00:00 2001 From: James Tombleson Date: Sun, 28 Apr 2024 11:41:11 -0700 Subject: [PATCH 09/13] new dtoconv file to convert entities to dto for handler --- internal/services/dtoconv.go | 46 ++++++++++++++++++++++++++++++++++++ 1 file changed, 46 insertions(+) create mode 100644 internal/services/dtoconv.go diff --git a/internal/services/dtoconv.go b/internal/services/dtoconv.go new file mode 100644 index 0000000..1cddec3 --- /dev/null +++ b/internal/services/dtoconv.go @@ -0,0 +1,46 @@ +package services + +import "git.jamestombleson.com/jtom38/newsbot-api/internal/domain" + +func ArticlesToDto(items []domain.ArticleEntity) []domain.ArticleDto { + var dtos []domain.ArticleDto + for _, item := range items { + dtos = append(dtos, ArticleToDto(item)) + } + return dtos +} + +func ArticleToDto(item domain.ArticleEntity) domain.ArticleDto { + return domain.ArticleDto{ + ID: item.ID, + SourceID: item.SourceID, + Tags: item.Tags, + Title: item.Title, + Url: item.Url, + PubDate: item.PubDate, + IsVideo: item.IsVideo, + Thumbnail: item.Thumbnail, + Description: item.Description, + AuthorName: item.AuthorName, + AuthorImageUrl: item.AuthorImageUrl, + } +} + +func SourcesToDto(items []domain.SourceEntity) []domain.SourceDto { + var dtos []domain.SourceDto + for _, item := range items { + dtos = append(dtos, SourceToDto(item)) + } + return dtos +} + +func SourceToDto(item domain.SourceEntity) domain.SourceDto { + return domain.SourceDto{ + ID: item.ID, + Source: item.Source, + DisplayName: item.DisplayName, + Url: item.Url, + Tags: item.Tags, + Enabled: item.Enabled, + } +} -- 2.45.2 From dfd44714c06d49edfb751720031e18b7907ed71d Mon Sep 17 00:00:00 2001 From: James Tombleson Date: Sun, 28 Apr 2024 11:41:34 -0700 Subject: [PATCH 10/13] minor error updates and will soon be pulled apart --- internal/handler/v1/discordwebhooks.go | 26 +++++----- internal/handler/v1/queue.go | 2 +- internal/handler/v1/settings.go | 2 +- internal/handler/v1/sources.go | 69 +++++++++++--------------- internal/handler/v1/subscriptions.go | 37 +++++++------- 5 files changed, 63 insertions(+), 73 deletions(-) diff --git a/internal/handler/v1/discordwebhooks.go b/internal/handler/v1/discordwebhooks.go index 38cc14d..93ed20a 100644 --- a/internal/handler/v1/discordwebhooks.go +++ b/internal/handler/v1/discordwebhooks.go @@ -60,21 +60,21 @@ func (s *Handler) GetDiscordWebHooksById(c echo.Context) error { _id := c.Param("ID") if _id == "" { - return c.JSON(http.StatusBadRequest, domain.ErrorResponse{ + return c.JSON(http.StatusBadRequest, domain.BaseResponse{ Message: ErrIdValueMissing, }) } uuid, err := uuid.Parse(_id) if err != nil { - return c.JSON(http.StatusBadRequest, domain.ErrorResponse{ + return c.JSON(http.StatusBadRequest, domain.BaseResponse{ Message: ErrUnableToParseId, }) } res, err := s.dto.GetDiscordWebhook(c.Request().Context(), uuid) if err != nil { - return c.JSON(http.StatusBadRequest, domain.ErrorResponse{ + return c.JSON(http.StatusBadRequest, domain.BaseResponse{ Message: ErrNoRecordFound, }) } @@ -100,21 +100,21 @@ func (s *Handler) GetDiscordWebHooksByServerAndChannel(c echo.Context) error { _server := c.QueryParam("server") if _server == "" { - return c.JSON(http.StatusBadRequest, domain.ErrorResponse{ + return c.JSON(http.StatusBadRequest, domain.BaseResponse{ Message: ErrIdValueMissing, }) } _channel := c.QueryParam("channel") if _channel == "" { - return c.JSON(http.StatusBadRequest, domain.ErrorResponse{ + return c.JSON(http.StatusBadRequest, domain.BaseResponse{ Message: fmt.Sprintf("%s channel", ErrParameterMissing), }) } res, err := s.dto.GetDiscordWebHookByServerAndChannel(c.Request().Context(), _server, _channel) if err != nil { - return c.JSON(http.StatusInternalServerError, domain.ErrorResponse{ + return c.JSON(http.StatusInternalServerError, domain.BaseResponse{ Message: err.Error(), }) } @@ -136,22 +136,22 @@ func (s *Handler) NewDiscordWebHook(c echo.Context) error { _channel := c.QueryParam("channel") if _url == "" { - return c.JSON(http.StatusBadRequest, domain.ErrorResponse{ + return c.JSON(http.StatusBadRequest, domain.BaseResponse{ Message: "url is missing a value", }) } if !strings.Contains(_url, "discord.com/api/webhooks") { - return c.JSON(http.StatusBadRequest, domain.ErrorResponse{ + return c.JSON(http.StatusBadRequest, domain.BaseResponse{ Message: "invalid url", }) } if _server == "" { - return c.JSON(http.StatusBadRequest, domain.ErrorResponse{ + return c.JSON(http.StatusBadRequest, domain.BaseResponse{ Message: "server is missing", }) } if _channel == "" { - return c.JSON(http.StatusBadRequest, domain.ErrorResponse{ + return c.JSON(http.StatusBadRequest, domain.BaseResponse{ Message: "channel is missing", }) } @@ -176,7 +176,7 @@ func (s *Handler) disableDiscordWebHook(c echo.Context) error { id := c.Param("ID") uuid, err := uuid.Parse(id) if err != nil { - return c.JSON(http.StatusBadRequest, domain.ErrorResponse{ + return c.JSON(http.StatusBadRequest, domain.BaseResponse{ Message: err.Error(), }) } @@ -189,7 +189,7 @@ func (s *Handler) disableDiscordWebHook(c echo.Context) error { err = s.Db.DisableDiscordWebHook(c.Request().Context(), uuid) if err != nil { - return c.JSON(http.StatusInternalServerError, domain.ErrorResponse{ + return c.JSON(http.StatusInternalServerError, domain.BaseResponse{ Message: err.Error(), }) } @@ -205,7 +205,7 @@ func (s *Handler) enableDiscordWebHook(c echo.Context) error { id := c.Param("ID") uuid, err := uuid.Parse(id) if err != nil { - return c.JSON(http.StatusBadRequest, domain.ErrorResponse{ + return c.JSON(http.StatusBadRequest, domain.BaseResponse{ Message: err.Error(), }) } diff --git a/internal/handler/v1/queue.go b/internal/handler/v1/queue.go index 1f8ad59..deadb89 100644 --- a/internal/handler/v1/queue.go +++ b/internal/handler/v1/queue.go @@ -30,7 +30,7 @@ func (s *Handler) ListDiscordWebhookQueue(c echo.Context) error { // Get the raw resp from sql res, err := s.dto.ListDiscordWebhookQueueDetails(c.Request().Context(), 50) if err != nil { - return c.JSON(http.StatusInternalServerError, domain.ErrorResponse{ + return c.JSON(http.StatusInternalServerError, domain.BaseResponse{ Message: err.Error(), }) } diff --git a/internal/handler/v1/settings.go b/internal/handler/v1/settings.go index 96b472d..114500d 100644 --- a/internal/handler/v1/settings.go +++ b/internal/handler/v1/settings.go @@ -20,7 +20,7 @@ func (s *Handler) getSettings(c echo.Context) error { uuid, err := uuid.Parse(id) if err != nil { - return c.JSON(http.StatusBadRequest, domain.ErrorResponse{ + return c.JSON(http.StatusBadRequest, domain.BaseResponse{ Message: err.Error(), }) } diff --git a/internal/handler/v1/sources.go b/internal/handler/v1/sources.go index cadd521..c725ddf 100644 --- a/internal/handler/v1/sources.go +++ b/internal/handler/v1/sources.go @@ -10,19 +10,10 @@ import ( "git.jamestombleson.com/jtom38/newsbot-api/internal/database" "git.jamestombleson.com/jtom38/newsbot-api/internal/domain" "git.jamestombleson.com/jtom38/newsbot-api/internal/domain/models" - "github.com/go-chi/chi/v5" "github.com/google/uuid" "github.com/labstack/echo/v4" ) -func (s *Handler) GetSourcesRouter() http.Handler { - r := chi.NewRouter() - - - - return r -} - type ListSources struct { ApiStatusModel Payload []models.SourceDto `json:"payload"` @@ -38,8 +29,8 @@ type GetSource struct { // @Produce application/json // @Tags Source // @Router /sources [get] -// @Success 200 {object} ListSources "ok" -// @Failure 400 {object} ApiError "Unable to reach SQL or Data problems" +// @Success 200 {object} ListSources "ok" +// @Failure 400 {object} domain.BaseResponse "Unable to reach SQL or Data problems" func (s *Handler) listSources(c echo.Context) error { //TODO Add top? /* @@ -61,9 +52,7 @@ func (s *Handler) listSources(c echo.Context) error { // Default way of showing all sources items, err := s.dto.ListSources(c.Request().Context(), 50) if err != nil { - return c.JSON(http.StatusInternalServerError, domain.ErrorResponse{ - Message: err.Error(), - }) + s.WriteError(c, err, http.StatusInternalServerError) } p.Payload = items @@ -102,7 +91,7 @@ func (s *Handler) listSourcesBySource(c echo.Context) error { // Shows the list by Sources.source res, err := s.dto.ListSourcesBySource(c.Request().Context(), source) if err != nil { - return c.JSON(http.StatusInternalServerError, domain.ErrorResponse{ + return c.JSON(http.StatusInternalServerError, domain.BaseResponse{ Message: err.Error(), }) } @@ -132,14 +121,14 @@ func (s *Handler) getSources(c echo.Context) error { id := c.Param("ID") uuid, err := uuid.Parse(id) if err != nil { - return c.JSON(http.StatusBadRequest, domain.ErrorResponse{ + return c.JSON(http.StatusBadRequest, domain.BaseResponse{ Message: ErrUnableToParseId, }) } res, err := s.dto.GetSourceById(c.Request().Context(), uuid) if err != nil { - return c.JSON(http.StatusInternalServerError, domain.ErrorResponse{ + return c.JSON(http.StatusInternalServerError, domain.BaseResponse{ Message: ErrNoRecordFound, }) } @@ -170,7 +159,7 @@ func (s *Handler) GetSourceBySourceAndName(c echo.Context) error { var param domain.GetSourceBySourceAndNameParamRequest err := c.Bind(¶m) if err != nil { - return c.JSON(http.StatusBadRequest, domain.ErrorResponse{ + return c.JSON(http.StatusBadRequest, domain.BaseResponse{ Message: err.Error(), }) } @@ -206,7 +195,7 @@ func (s *Handler) newRedditSource(c echo.Context) error { var param domain.NewSourceParamRequest err := c.Bind(¶m) if err != nil { - return c.JSON(http.StatusBadRequest, domain.ErrorResponse{ + return c.JSON(http.StatusBadRequest, domain.BaseResponse{ Message: err.Error(), }) } @@ -216,12 +205,12 @@ func (s *Handler) newRedditSource(c echo.Context) error { //_tags := query["tags"][0] if param.Url == "" { - return c.JSON(http.StatusBadRequest, domain.ErrorResponse{ + return c.JSON(http.StatusBadRequest, domain.BaseResponse{ Message: "Url is missing a value", }) } if !strings.Contains(param.Url, "reddit.com") { - return c.JSON(http.StatusBadRequest, domain.ErrorResponse{ + return c.JSON(http.StatusBadRequest, domain.BaseResponse{ Message: "Invalid URL given", }) } @@ -248,7 +237,7 @@ func (s *Handler) newRedditSource(c echo.Context) error { } err = s.Db.CreateSource(c.Request().Context(), params) if err != nil { - return c.JSON(http.StatusInternalServerError, domain.ErrorResponse{ + return c.JSON(http.StatusInternalServerError, domain.BaseResponse{ Message: err.Error(), }) } @@ -272,7 +261,7 @@ func (s *Handler) newYoutubeSource(c echo.Context) error { var param domain.NewSourceParamRequest err := c.Bind(¶m) if err != nil { - return c.JSON(http.StatusBadRequest, domain.ErrorResponse{ + return c.JSON(http.StatusBadRequest, domain.BaseResponse{ Message: err.Error(), }) } @@ -283,12 +272,12 @@ func (s *Handler) newYoutubeSource(c echo.Context) error { ////_tags := query["tags"][0] if param.Url == "" { - return c.JSON(http.StatusBadRequest, domain.ErrorResponse{ + return c.JSON(http.StatusBadRequest, domain.BaseResponse{ Message: "url is missing a value", }) } if !strings.Contains(param.Url, "youtube.com") { - return c.JSON(http.StatusBadRequest, domain.ErrorResponse{ + return c.JSON(http.StatusBadRequest, domain.BaseResponse{ Message: "Invalid URL", }) } @@ -318,7 +307,7 @@ func (s *Handler) newYoutubeSource(c echo.Context) error { bJson, err := json.Marshal(¶ms) if err != nil { - return c.JSON(http.StatusInternalServerError, domain.ErrorResponse{ + return c.JSON(http.StatusInternalServerError, domain.BaseResponse{ Message: err.Error(), }) } @@ -335,7 +324,7 @@ func (s *Handler) newTwitchSource(c echo.Context) error { var param domain.NewSourceParamRequest err := c.Bind(¶m) if err != nil { - return c.JSON(http.StatusBadRequest, domain.ErrorResponse{ + return c.JSON(http.StatusBadRequest, domain.BaseResponse{ Message: err.Error(), }) } @@ -358,14 +347,14 @@ func (s *Handler) newTwitchSource(c echo.Context) error { } err = s.Db.CreateSource(c.Request().Context(), params) if err != nil { - return c.JSON(http.StatusInternalServerError, domain.ErrorResponse{ + return c.JSON(http.StatusInternalServerError, domain.BaseResponse{ Message: err.Error(), }) } bJson, err := json.Marshal(¶ms) if err != nil { - return c.JSON(http.StatusInternalServerError, domain.ErrorResponse{ + return c.JSON(http.StatusInternalServerError, domain.BaseResponse{ Message: err.Error(), }) } @@ -382,7 +371,7 @@ func (s *Handler) deleteSources(c echo.Context) error { id := c.Param("ID") uuid, err := uuid.Parse(id) if err != nil { - return c.JSON(http.StatusBadRequest, domain.ErrorResponse{ + return c.JSON(http.StatusBadRequest, domain.BaseResponse{ Message: err.Error(), }) } @@ -390,7 +379,7 @@ func (s *Handler) deleteSources(c echo.Context) error { // Check to make sure we can find the record _, err = s.Db.GetSourceByID(c.Request().Context(), uuid) if err != nil { - return c.JSON(http.StatusInternalServerError, domain.ErrorResponse{ + return c.JSON(http.StatusInternalServerError, domain.BaseResponse{ Message: err.Error(), }) } @@ -398,7 +387,7 @@ func (s *Handler) deleteSources(c echo.Context) error { // Delete the record err = s.Db.DeleteSource(c.Request().Context(), uuid) if err != nil { - return c.JSON(http.StatusInternalServerError, domain.ErrorResponse{ + return c.JSON(http.StatusInternalServerError, domain.BaseResponse{ Message: err.Error(), }) } @@ -410,7 +399,7 @@ func (s *Handler) deleteSources(c echo.Context) error { b, err := json.Marshal(p) if err != nil { - return c.JSON(http.StatusInternalServerError, domain.ErrorResponse{ + return c.JSON(http.StatusInternalServerError, domain.BaseResponse{ Message: err.Error(), }) } @@ -427,7 +416,7 @@ func (s *Handler) disableSource(c echo.Context) error { id := c.Param("ID") uuid, err := uuid.Parse(id) if err != nil { - return c.JSON(http.StatusBadRequest, domain.ErrorResponse{ + return c.JSON(http.StatusBadRequest, domain.BaseResponse{ Message: err.Error(), }) } @@ -435,14 +424,14 @@ func (s *Handler) disableSource(c echo.Context) error { // Check to make sure we can find the record _, err = s.Db.GetSourceByID(c.Request().Context(), uuid) if err != nil { - return c.JSON(http.StatusInternalServerError, domain.ErrorResponse{ + return c.JSON(http.StatusInternalServerError, domain.BaseResponse{ Message: err.Error(), }) } err = s.Db.DisableSource(context.Background(), uuid) if err != nil { - return c.JSON(http.StatusInternalServerError, domain.ErrorResponse{ + return c.JSON(http.StatusInternalServerError, domain.BaseResponse{ Message: err.Error(), }) } @@ -454,7 +443,7 @@ func (s *Handler) disableSource(c echo.Context) error { b, err := json.Marshal(p) if err != nil { - return c.JSON(http.StatusInternalServerError, domain.ErrorResponse{ + return c.JSON(http.StatusInternalServerError, domain.BaseResponse{ Message: err.Error(), }) } @@ -477,14 +466,14 @@ func (s *Handler) enableSource(c echo.Context) error { // Check to make sure we can find the record _, err = s.Db.GetSourceByID(c.Request().Context(), uuid) if err != nil { - return c.JSON(http.StatusInternalServerError, domain.ErrorResponse{ + return c.JSON(http.StatusInternalServerError, domain.BaseResponse{ Message: err.Error(), }) } err = s.Db.EnableSource(c.Request().Context(), uuid) if err != nil { - return c.JSON(http.StatusInternalServerError, domain.ErrorResponse{ + return c.JSON(http.StatusInternalServerError, domain.BaseResponse{ Message: err.Error(), }) } @@ -496,7 +485,7 @@ func (s *Handler) enableSource(c echo.Context) error { b, err := json.Marshal(p) if err != nil { - return c.JSON(http.StatusInternalServerError, domain.ErrorResponse{ + return c.JSON(http.StatusInternalServerError, domain.BaseResponse{ Message: err.Error(), }) } diff --git a/internal/handler/v1/subscriptions.go b/internal/handler/v1/subscriptions.go index 7af0ccc..eff2512 100644 --- a/internal/handler/v1/subscriptions.go +++ b/internal/handler/v1/subscriptions.go @@ -3,6 +3,7 @@ package v1 import ( "context" "encoding/json" + "errors" "net/http" "git.jamestombleson.com/jtom38/newsbot-api/internal/database" @@ -44,7 +45,7 @@ func (s *Handler) ListSubscriptions(c echo.Context) error { res, err := s.dto.ListSubscriptions(c.Request().Context(), 50) if err != nil { - return s.WriteError(c, err.Error(), http.StatusBadRequest) + return s.WriteError(c, err, http.StatusBadRequest) } payload.Payload = res @@ -67,7 +68,7 @@ func (s *Handler) ListSubscriptionDetails(c echo.Context) error { res, err := s.dto.ListSubscriptionDetails(c.Request().Context(), 50) if err != nil { - return s.WriteError(c, err.Error(), http.StatusInternalServerError) + return s.WriteError(c, err, http.StatusInternalServerError) } payload.Payload = res @@ -93,18 +94,18 @@ func (s *Handler) GetSubscriptionsByDiscordId(c echo.Context) error { id := c.QueryParam("id") if id == "" { - return s.WriteError(c, ErrIdValueMissing, http.StatusBadRequest) + return s.WriteError(c, errors.New(ErrIdValueMissing), http.StatusBadRequest) } uuid, err := uuid.Parse(id) if err != nil { - return s.WriteError(c, ErrValueNotUuid, http.StatusBadRequest) + return s.WriteError(c, errors.New(ErrValueNotUuid), http.StatusBadRequest) } res, err := s.dto.ListSubscriptionsByDiscordWebhookId(context.Background(), uuid) if err != nil { - return s.WriteError(c, err.Error(), http.StatusNoContent) + return s.WriteError(c, err, http.StatusNoContent) } p.Payload = res @@ -128,17 +129,17 @@ func (s *Handler) GetSubscriptionsBySourceId(c echo.Context) error { _id := c.QueryParam("id") if _id == "" { - return s.WriteError(c, ErrIdValueMissing, http.StatusBadRequest) + return s.WriteError(c, errors.New(ErrIdValueMissing), http.StatusBadRequest) } uuid, err := uuid.Parse(_id) if err != nil { - return s.WriteError(c, err.Error(), http.StatusBadRequest) + return s.WriteError(c, err, http.StatusBadRequest) } res, err := s.dto.ListSubscriptionsBySourceId(context.Background(), uuid) if err != nil { - return s.WriteError(c, err.Error(), http.StatusNoContent) + return s.WriteError(c, err, http.StatusNoContent) } p.Payload = res @@ -158,20 +159,20 @@ func (s *Handler) newDiscordWebHookSubscription(c echo.Context) error { // Check to make we didn't get a null if discordWebHookId == "" { - return s.WriteError(c, "invalid discordWebHooksId given", http.StatusBadRequest) + return s.WriteError(c, errors.New("invalid discordWebHooksId given"), http.StatusBadRequest) } if sourceId == "" { - return s.WriteError(c, "invalid sourceID given", http.StatusBadRequest) + return s.WriteError(c, errors.New("invalid sourceID given"), http.StatusBadRequest) } // Validate they are UUID values uHook, err := uuid.Parse(discordWebHookId) if err != nil { - return s.WriteError(c, err.Error(), http.StatusBadRequest) + return s.WriteError(c, err, http.StatusBadRequest) } uSource, err := uuid.Parse(sourceId) if err != nil { - return s.WriteError(c, err.Error(), http.StatusBadRequest) + return s.WriteError(c, err, http.StatusBadRequest) } // Check if the sub already exists @@ -180,7 +181,7 @@ func (s *Handler) newDiscordWebHookSubscription(c echo.Context) error { Sourceid: uSource, }) if err == nil { - return s.WriteError(c, "a subscription already exists between these two entities", http.StatusBadRequest) + return s.WriteError(c, errors.New("a subscription already exists between these two entities"), http.StatusBadRequest) } // Does not exist, so make it. @@ -191,12 +192,12 @@ func (s *Handler) newDiscordWebHookSubscription(c echo.Context) error { } err = s.Db.CreateSubscription(context.Background(), params) if err != nil { - return s.WriteError(c, err.Error(), http.StatusInternalServerError) + return s.WriteError(c, err, http.StatusInternalServerError) } bJson, err := json.Marshal(¶ms) if err != nil { - return s.WriteError(c, err.Error(), http.StatusInternalServerError) + return s.WriteError(c, err, http.StatusInternalServerError) } return c.JSON(http.StatusOK, bJson) @@ -212,17 +213,17 @@ func (s *Handler) DeleteDiscordWebHookSubscription(c echo.Context) error { id := c.QueryParam("id") if id == "" { - return s.WriteError(c, ErrMissingSubscriptionID, http.StatusBadRequest) + return s.WriteError(c, errors.New(ErrMissingSubscriptionID), http.StatusBadRequest) } uid, err := uuid.Parse(id) if err != nil { - return s.WriteError(c, err.Error(), http.StatusBadRequest) + return s.WriteError(c, err, http.StatusBadRequest) } err = s.Db.DeleteSubscription(context.Background(), uid) if err != nil { - return s.WriteError(c, err.Error(), http.StatusInternalServerError) + return s.WriteError(c, err, http.StatusInternalServerError) } return c.JSON(http.StatusOK, nil) -- 2.45.2 From 3d2420343c54135f502a947d23e1ee028bd7dc41 Mon Sep 17 00:00:00 2001 From: James Tombleson Date: Sun, 28 Apr 2024 11:41:55 -0700 Subject: [PATCH 11/13] cleaning up the dto's and making new response types --- internal/domain/dto.go | 56 +++++++++++------------------------- internal/domain/responses.go | 23 ++++++++++++++- 2 files changed, 39 insertions(+), 40 deletions(-) diff --git a/internal/domain/dto.go b/internal/domain/dto.go index 4109cab..0a59cd6 100644 --- a/internal/domain/dto.go +++ b/internal/domain/dto.go @@ -3,33 +3,26 @@ package domain import "time" type ArticleDto struct { - ID int64 `json:"id"` - SourceID int64 `json:"sourceId"` - Tags string `json:"tags"` - Title string `json:"title"` - Url string `json:"url"` - PubDate time.Time `json:"pubDate"` - Video string `json:"video"` - VideoHeight uint16 `json:"videoHeight"` - VideoWidth uint16 `json:"videoWidth"` - Thumbnail string `json:"thumbnail"` - Description string `json:"description"` - AuthorName string `json:"authorName"` - AuthorImage string `json:"authorImage"` + ID int64 `json:"id"` + SourceID int64 `json:"sourceId"` + Tags string `json:"tags"` + Title string `json:"title"` + Url string `json:"url"` + PubDate time.Time `json:"pubDate"` + IsVideo bool `json:"isVideo"` + Thumbnail string `json:"thumbnail"` + Description string `json:"description"` + AuthorName string `json:"authorName"` + AuthorImageUrl string `json:"authorImage"` } type DiscordQueueDto struct { - //CreatedAt time.Time `json:"createdAt"` - //UpdatedAt time.Time `json:"updatedAt"` ID int64 `json:"id"` ArticleId int64 `json:"articleId"` SourceId int64 `json:"sourceId"` } type DiscordWebHookDto struct { - //CreatedAt time.Time `json:"CreatedAt"` - //UpdatedAt time.Time `json:"UpdatedAt"` - //DeletedAt time.Time `json:"DeletedAt"` ID uint `json:"id"` Name string `json:"name"` Key string `json:"key"` @@ -40,18 +33,12 @@ type DiscordWebHookDto struct { } type IconDto struct { - //CreatedAt time.Time `json:"CreatedAt"` - //UpdatedAt time.Time `json:"UpdatedAt"` - //DeletedAt time.Time `json:"DeletedAt"` ID int64 `json:"id"` FileName string `json:"fileName"` Site string `json:"site"` } type SettingDto struct { - //CreatedAt time.Time `json:"CreatedAt"` - //UpdatedAt time.Time `json:"UpdatedAt"` - //DeletedAt time.Time `json:"DeletedAt"` ID int64 `json:"id"` Key string `json:"key"` Value string `json:"value"` @@ -59,9 +46,6 @@ type SettingDto struct { } type SubscriptionDto struct { - //CreatedAt time.Time `json:"CreatedAt"` - //UpdatedAt time.Time `json:"UpdatedAt"` - //DeletedAt time.Time `json:"DeletedAt"` ID int64 `json:"id"` SourceID int64 `json:"sourceId"` SourceType string `json:"sourceType"` @@ -71,16 +55,10 @@ type SubscriptionDto struct { } type SourceDto struct { - //CreatedAt time.Time `json:"CreatedAt"` - //UpdatedAt time.Time `json:"UpdatedAt"` - //DeletedAt time.Time `json:"DeletedAt"` - ID int64 `json:"id"` - Site string `json:"site"` - Name string `json:"name"` - Source string `json:"source"` - Type string `json:"type"` - Value string `json:"value"` - Enabled bool `json:"enabled"` - Url string `json:"url"` - Tags string `json:"tags"` + ID int64 `json:"id"` + Source string `json:"source"` + DisplayName string `json:"name"` + Url string `json:"url"` + Tags string `json:"tags"` + Enabled bool `json:"enabled"` } diff --git a/internal/domain/responses.go b/internal/domain/responses.go index b4805a6..5175940 100644 --- a/internal/domain/responses.go +++ b/internal/domain/responses.go @@ -1,5 +1,26 @@ package domain -type ErrorResponse struct { + +type BaseResponse struct { Message string `json:"message"` +} + +type ArticleResponse struct { + BaseResponse + Payload []ArticleDto `json:"payload"` +} + +type ArticleAndSourceModel struct { + Article ArticleDto `json:"article"` + Source SourceDto `json:"source"` +} + +type ArticleDetailResponse struct { + BaseResponse + Payload ArticleAndSourceModel `json:"payload"` +} + +type DiscordWebhookResponse struct { + BaseResponse + Payload []DiscordWebHookDto `json:"payload"` } \ No newline at end of file -- 2.45.2 From 228e08fef3c045981b06e9587b57f9207e6b0c89 Mon Sep 17 00:00:00 2001 From: James Tombleson Date: Sun, 28 Apr 2024 11:42:10 -0700 Subject: [PATCH 12/13] docker will now use go 1.22 --- Dockerfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Dockerfile b/Dockerfile index 0f015e6..1d7645e 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,4 +1,4 @@ -FROM golang:1.18.4 as build +FROM golang:1.22 as build COPY . /app WORKDIR /app -- 2.45.2 From 2b6ab134d9583d76a72e066d74f9f85661aee3d2 Mon Sep 17 00:00:00 2001 From: James Tombleson Date: Sun, 28 Apr 2024 11:42:30 -0700 Subject: [PATCH 13/13] make swag now works with the new cmd pathing --- makefile | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/makefile b/makefile index baec9f5..c0ccd02 100644 --- a/makefile +++ b/makefile @@ -18,8 +18,8 @@ migrate-dev-down: ## revert sql migrations to dev db goose -dir "./internal/database/migrations" postgres "user=postgres password=postgres dbname=postgres sslmode=disable" down swag: ## Generates the swagger documentation with the swag tool - ~/go/bin/swag f - ~/go/bin/swag i + ~/go/bin/swag f + ~/go/bin/swag init -g cmd/server.go gensql: ## Generates SQL code with sqlc sqlc generate \ No newline at end of file -- 2.45.2