diff --git a/internal/database/migrations/20240425083756_init.sql b/internal/database/migrations/20240425083756_init.sql index 96e82ae..8ab36dd 100644 --- a/internal/database/migrations/20240425083756_init.sql +++ b/internal/database/migrations/20240425083756_init.sql @@ -72,15 +72,36 @@ CREATE Table Sources ( Tags TEXT NOT NULL ); +/* CREATE TABLE Subscriptions ( ID INTEGER PRIMARY KEY AUTOINCREMENT, CreatedAt DATETIME NOT NULL, UpdatedAt DATETIME NOT NULL, - DeletedAt DATETIME, + DeletedAt DATETIME NOT NULL, DiscordWebHookID NUMBER NOT NULL, SourceID NUMBER NOT NULL, UserID NUMBER NOT NULL ); +*/ + +CREATE TABLE UserSourceSubscriptions ( + ID INTEGER PRIMARY KEY AUTOINCREMENT, + CreatedAt DATETIME NOT NULL, + UpdatedAt DATETIME NOT NULL, + DeletedAt DATETIME NOT NULL, + UserID NUMBER NOT NULL, + SourceID NUMBER NOT NULL +); + +CREATE TABLE AlertDiscord ( + ID INTEGER PRIMARY KEY AUTOINCREMENT, + CreatedAt DATETIME NOT NULL, + UpdatedAt DATETIME NOT NULL, + DeletedAt DATETIME NOT NULL, + UserID NUMBER NOT NULL, + SourceID NUMBER NOT NULL, + DiscordWebHookID NUMBER NOT NULL +); CREATE TABLE Users ( ID INTEGER PRIMARY KEY AUTOINCREMENT, diff --git a/internal/domain/entity.go b/internal/domain/entity.go index bc56a9b..6983fb5 100644 --- a/internal/domain/entity.go +++ b/internal/domain/entity.go @@ -4,6 +4,18 @@ import ( "time" ) +// This links a source to a discord webhook. +// It is owned by a user so they can remove the link +type AlertDiscordEntity struct { + ID int64 + CreatedAt time.Time + UpdatedAt time.Time + DeletedAt time.Time + UserID int64 + SourceID int64 + DiscordWebHookId int64 +} + type ArticleEntity struct { ID int64 CreatedAt time.Time @@ -35,10 +47,11 @@ type DiscordWebHookEntity struct { CreatedAt time.Time UpdatedAt time.Time DeletedAt time.Time - Url string - Server string - Channel string - Enabled bool + UserID int64 + Url string + Server string + Channel string + Enabled bool } type IconEntity struct { @@ -61,38 +74,50 @@ type SettingEntity struct { } type SourceEntity struct { - ID int64 - CreatedAt time.Time - UpdatedAt time.Time - DeletedAt time.Time + ID int64 + CreatedAt time.Time + UpdatedAt time.Time + DeletedAt time.Time - // Who will collect from it. Used + // Who will collect from it. Used // domain.SourceCollector... - Source string + Source string // Human Readable value to state what is getting collected DisplayName string // Tells the parser where to look for data - Url string - + Url string + // Static tags for this defined record - Tags string + Tags string // If the record is disabled, then it will be skipped on processing - Enabled bool + Enabled bool } -type SubscriptionEntity struct { - ID int64 - CreatedAt time.Time - UpdatedAt time.Time - DeletedAt time.Time - SourceID int64 - SourceType string - SourceName string - DiscordID int64 - DiscordName string +//type SubscriptionEntity struct { +// ID int64 +// CreatedAt time.Time +// UpdatedAt time.Time +// DeletedAt time.Time +// UserID int64 +// SourceID int64 +// //SourceType string +// //SourceName string +// DiscordID int64 +// //DiscordName string +//} + +// This defines what sources a user wants to follow. +// These will show up for the user as a front page +type UserSourceSubscriptionEntity struct { + ID int64 + CreatedAt time.Time + UpdatedAt time.Time + DeletedAt time.Time + UserID int64 + SourceID int64 } type UserEntity struct { diff --git a/internal/domain/scopes.go b/internal/domain/scopes.go new file mode 100644 index 0000000..c3ef7df --- /dev/null +++ b/internal/domain/scopes.go @@ -0,0 +1,6 @@ +package domain + +const ( + ScopeAll = "newsbot:all" + ScopeRead = "newsbot:read" +) diff --git a/internal/repository/alertDiscord.go b/internal/repository/alertDiscord.go new file mode 100644 index 0000000..e6ef6de --- /dev/null +++ b/internal/repository/alertDiscord.go @@ -0,0 +1,122 @@ +package repository + +import ( + "context" + "database/sql" + "errors" + "fmt" + "time" + + "git.jamestombleson.com/jtom38/newsbot-api/internal/domain" + "github.com/huandu/go-sqlbuilder" +) + +type AlertDiscordRepo interface { + Create(ctx context.Context, userId, sourceId, webhookId 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) + ListByUser(ctx context.Context, page, limit int, userId int64) ([]domain.AlertDiscordEntity, error) +} + +type alertDiscordRepository struct { + conn *sql.DB + defaultLimit int + defaultOffset int +} + +func NewAlertDiscordRepository(conn *sql.DB) alertDiscordRepository { + return alertDiscordRepository{ + conn: conn, + defaultLimit: 50, + defaultOffset: 50, + } +} + +func (r alertDiscordRepository) Create(ctx context.Context, userId, sourceId, webhookId int64) (int64, error) { + dt := time.Now() + queryBuilder := sqlbuilder.NewInsertBuilder() + queryBuilder.InsertInto("AlertDiscord") + queryBuilder.Cols("UpdatedAt", "CreatedAt", "DeletedAt", "UserID", "SourceID", "DiscordWebHookID") + queryBuilder.Values(dt, dt, timeZero, userId, sourceId, webhookId) + query, args := queryBuilder.Build() + + _, err := r.conn.ExecContext(ctx, query, args...) + if err != nil { + return 0, err + } + + return 1, nil +} + +func (r alertDiscordRepository) SoftDelete(ctx context.Context, id int64) (int64, error) { + return softDeleteRow(ctx, r.conn, "AlertDiscord", id) +} + +func (r alertDiscordRepository) Restore(ctx context.Context, id int64) (int64, error) { + return restoreRow(ctx, r.conn, "AlertDiscord", id) +} + +func (r alertDiscordRepository) Delete(ctx context.Context, id int64) (int64, error) { + return deleteFromTable(ctx, r.conn, "AlertDiscord", id) +} + +func (r alertDiscordRepository) ListByUser(ctx context.Context, page, limit int, userId int64) ([]domain.AlertDiscordEntity, error) { + builder := sqlbuilder.NewSelectBuilder() + builder.Select("*") + builder.From("AlertDiscord") + builder.Where( + builder.Equal("UserID", userId), + ) + builder.Offset(page * limit) + builder.Limit(limit) + + query, args := builder.Build() + rows, err := r.conn.QueryContext(ctx, query, args...) + if err != nil { + return []domain.AlertDiscordEntity{}, err + } + + data := r.processRows(rows) + if len(data) == 0 { + return []domain.AlertDiscordEntity{}, errors.New(ErrUserNotFound) + } + + return data, nil +} + +func (ur alertDiscordRepository) processRows(rows *sql.Rows) []domain.AlertDiscordEntity { + items := []domain.AlertDiscordEntity{} + + for rows.Next() { + var id int64 + var createdAt time.Time + var updatedAt time.Time + var deletedAt time.Time + var userId int64 + var sourceId int64 + var webhookId int64 + + err := rows.Scan( + &id, &createdAt, &updatedAt, &deletedAt, + &userId, &sourceId, &webhookId, + ) + if err != nil { + fmt.Println(err) + } + + item := domain.AlertDiscordEntity{ + ID: id, + CreatedAt: createdAt, + UpdatedAt: updatedAt, + DeletedAt: deletedAt, + UserID: userId, + SourceID: sourceId, + DiscordWebHookId: webhookId, + } + + items = append(items, item) + } + + return items +} diff --git a/internal/repository/alertDiscord_test.go b/internal/repository/alertDiscord_test.go new file mode 100644 index 0000000..e9cadcb --- /dev/null +++ b/internal/repository/alertDiscord_test.go @@ -0,0 +1,63 @@ +package repository_test + +import ( + "context" + "testing" + "time" + + "git.jamestombleson.com/jtom38/newsbot-api/internal/domain" + "git.jamestombleson.com/jtom38/newsbot-api/internal/repository" +) + +func TestAlertDiscordCreate(t *testing.T) { + t.Log(time.Time{}) + db, err := setupInMemoryDb() + if err != nil { + t.Log(err) + t.FailNow() + } + defer db.Close() + + r := repository.NewAlertDiscordRepository(db) + created, err := r.Create(context.Background(), 1, 1, 1) + if err != nil { + t.Log(err) + t.FailNow() + } + + if created != 1 { + t.Log("failed to create the record") + t.FailNow() + } +} + +func TestAlertDiscordCreateAndValidate(t *testing.T) { + t.Log(time.Time{}) + db, err := setupInMemoryDb() + if err != nil { + t.Log(err) + t.FailNow() + } + defer db.Close() + + source := repository.NewSourceRepository(db) + source.Create(context.Background(), domain.SourceCollectorRss, "Unit Testing", "www.fake.com", "testing,units", true) + sourceRecord, _ := source.GetBySourceAndName(context.Background(), domain.SourceCollectorRss, "Unit Testing") + + webhookRepo := repository.NewDiscordWebHookRepository(db) + webhookRepo.Create(context.Background(), 999, "discord.com", "Unit Testing", "memes", true) + webhook, _ := webhookRepo.GetByUrl(context.Background(), "discord.com") + + r := repository.NewAlertDiscordRepository(db) + r.Create(context.Background(), 999, sourceRecord.ID, webhook.ID) + alert, err := r.ListByUser(context.Background(), 0, 10, 999) + if err != nil { + t.Error(err) + t.FailNow() + } + + if len(alert) != 1 { + t.Error("got the incorrect number of rows back") + t.FailNow() + } +} diff --git a/internal/repository/userSourceSubscription.go b/internal/repository/userSourceSubscription.go new file mode 100644 index 0000000..7a9a5c2 --- /dev/null +++ b/internal/repository/userSourceSubscription.go @@ -0,0 +1,120 @@ +package repository + +import ( + "context" + "database/sql" + "errors" + "fmt" + "time" + + "git.jamestombleson.com/jtom38/newsbot-api/internal/domain" + "github.com/huandu/go-sqlbuilder" +) + +type UserSourceRepo interface { + Create(ctx context.Context, userId, sourceId 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) + ListByUser(ctx context.Context, page, limit int, userId int64) ([]domain.UserSourceSubscriptionEntity, error) +} + +type userSourceRepository struct { + conn *sql.DB + defaultLimit int + defaultOffset int +} + +func NewUserSourceRepository(conn *sql.DB) userSourceRepository { + return userSourceRepository{ + conn: conn, + defaultLimit: 50, + defaultOffset: 50, + } +} + +func (r userSourceRepository) Create(ctx context.Context, userId, sourceId int64) (int64, error) { + dt := time.Now() + queryBuilder := sqlbuilder.NewInsertBuilder() + queryBuilder.InsertInto("UserSourceSubscriptions") + queryBuilder.Cols("UpdatedAt", "CreatedAt", "DeletedAt", "UserID", "SourceID") + queryBuilder.Values(dt, dt, timeZero, userId, sourceId) + query, args := queryBuilder.Build() + + _, err := r.conn.ExecContext(ctx, query, args...) + if err != nil { + return 0, err + } + + return 1, nil +} + +func (r userSourceRepository) SoftDelete(ctx context.Context, id int64) (int64, error) { + return softDeleteRow(ctx, r.conn, "UserSourceSubscriptions", id) +} + +func (r userSourceRepository) Restore(ctx context.Context, id int64) (int64, error) { + return restoreRow(ctx, r.conn, "UserSourceSubscriptions", id) +} + +func (r userSourceRepository) Delete(ctx context.Context, id int64) (int64, error) { + return deleteFromTable(ctx, r.conn, "UserSourceSubscriptions", id) +} + +func (r userSourceRepository) ListByUser(ctx context.Context, page, limit int, userId int64) ([]domain.UserSourceSubscriptionEntity, error) { + builder := sqlbuilder.NewSelectBuilder() + builder.Select("*") + builder.From("UserSourceSubscriptions") + builder.Where( + builder.Equal("UserID", userId), + ) + builder.Offset(page * limit) + builder.Limit(limit) + + query, args := builder.Build() + rows, err := r.conn.QueryContext(ctx, query, args...) + if err != nil { + return []domain.UserSourceSubscriptionEntity{}, err + } + + data := r.processRows(rows) + if len(data) == 0 { + return []domain.UserSourceSubscriptionEntity{}, errors.New(ErrUserNotFound) + } + + return data, nil +} + +func (ur userSourceRepository) processRows(rows *sql.Rows) []domain.UserSourceSubscriptionEntity { + items := []domain.UserSourceSubscriptionEntity{} + + for rows.Next() { + var id int64 + var createdAt time.Time + var updatedAt time.Time + var deletedAt time.Time + var userId int64 + var sourceId int64 + + err := rows.Scan( + &id, &createdAt, &updatedAt, &deletedAt, + &userId, &sourceId, + ) + if err != nil { + fmt.Println(err) + } + + item := domain.UserSourceSubscriptionEntity{ + ID: id, + CreatedAt: createdAt, + UpdatedAt: updatedAt, + DeletedAt: deletedAt, + UserID: userId, + SourceID: sourceId, + } + + items = append(items, item) + } + + return items +} diff --git a/internal/services/database.go b/internal/services/database.go index 9d11878..d5db9cd 100644 --- a/internal/services/database.go +++ b/internal/services/database.go @@ -7,19 +7,23 @@ import ( ) type RepositoryService struct { - Articles repository.ArticlesRepo - DiscordWebHooks repository.DiscordWebHookRepo - Sources repository.Sources - Users repository.Users - RefreshTokens repository.RefreshToken + AlertDiscord repository.AlertDiscordRepo + Articles repository.ArticlesRepo + DiscordWebHooks repository.DiscordWebHookRepo + RefreshTokens repository.RefreshToken + Sources repository.Sources + Users repository.Users + UserSourceSubscriptions repository.UserSourceRepo } 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), + AlertDiscord: repository.NewAlertDiscordRepository(conn), + Articles: repository.NewArticleRepository(conn), + DiscordWebHooks: repository.NewDiscordWebHookRepository(conn), + RefreshTokens: repository.NewRefreshTokenRepository(conn), + Sources: repository.NewSourceRepository(conn), + Users: repository.NewUserRepository(conn), + UserSourceSubscriptions: repository.NewUserSourceRepository(conn), } }