Merge pull request 'features/repo-updates' (#4) from features/repo-updates into main

Reviewed-on: #4
This commit is contained in:
jtom38 2024-04-28 11:42:57 -07:00
commit 8fc2e56ad5
30 changed files with 1849 additions and 347 deletions

4
.gitignore vendored
View File

@ -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

View File

@ -1,4 +1,4 @@
FROM golang:1.18.4 as build
FROM golang:1.22 as build
COPY . /app
WORKDIR /app

View File

@ -4,20 +4,20 @@ SELECT 'up SQL query';
CREATE TABLE Articles (
ID INTEGER PRIMARY KEY AUTOINCREMENT,
CreatedAt DATETIME NOT NULL,
LastUpdated DATETIME NOT NULL,
DeletedAt DATETIME,
UpdatedAt DATETIME NOT NULL,
DeletedAt DATETIME NOT NULL,
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 (
@ -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

View File

@ -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

9
internal/domain/const.go Normal file
View File

@ -0,0 +1,9 @@
package domain
const (
SourceCollectorRss = "rss"
SourceCollectorFfxiv = "ffxiv"
SourceCollectorTwitch = "twitch"
SourceCollectorYoutube = "youtube"
SourceCollectorReddit = "reddit"
)

View File

@ -9,27 +9,20 @@ type ArticleDto struct {
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"`
IsVideo bool `json:"isVideo"`
Thumbnail string `json:"thumbnail"`
Description string `json:"description"`
AuthorName string `json:"authorName"`
AuthorImage string `json:"authorImage"`
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"`
DisplayName string `json:"name"`
Url string `json:"url"`
Tags string `json:"tags"`
Enabled bool `json:"enabled"`
}

View File

@ -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
@ -23,7 +24,7 @@ type ArticleEntity struct {
type DiscordQueueEntity struct {
ID int64
CreatedAt time.Time
LastUpdated time.Time
UpdatedAt time.Time
DeletedAt time.Time
ArticleId int64
SourceId int64
@ -34,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
@ -66,14 +67,22 @@ type SourceEntity struct {
CreatedAt time.Time
UpdatedAt time.Time
DeletedAt time.Time
Site string
Name string
// Who will collect from it. Used
// domain.SourceCollector...
Source string
Type string
Value string
Enabled bool
// 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 {

View File

@ -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"`
}

View File

@ -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)
page, err := strconv.Atoi(c.QueryParam("page"))
if err != nil {
return c.JSON(http.StatusBadRequest, err)
page = 0
}
res, err := s.dto.ListArticles(c.Request().Context(), 25, page)
res, err := s.repo.Articles.ListByPage(c.Request().Context(), page, 25)
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)
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 {
// if the page number is missing, default to 0
_page, err := strconv.Atoi(c.QueryParam("page"))
if err != nil {
return c.JSON(http.StatusBadRequest, err)
_page = 0
}
res, err := s.dto.ListNewArticlesBySourceId(c.Request().Context(), uuid, 25, _page)
items, err := s.repo.Articles.ListBySource(c.Request().Context(), _page, 25, id, "")
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
}
p.Payload = services.ArticlesToDto(items)
return c.JSON(http.StatusOK, p)
}

View File

@ -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(),
})
}

View File

@ -19,18 +19,20 @@ type Handler struct {
Db *database.Queries
dto *dto.DtoClient
config services.Configs
sqlConnection *sql.DB
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 (
@ -45,15 +47,9 @@ func NewServer(ctx context.Context, db *database.Queries, configs services.Confi
Db: db,
dto: dto.NewDtoClient(db),
config: configs,
sqlConnection: conn,
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,
})
}

View File

@ -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(),
})
}

View File

@ -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(),
})
}

View File

@ -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"`
@ -39,7 +30,7 @@ type GetSource struct {
// @Tags Source
// @Router /sources [get]
// @Success 200 {object} ListSources "ok"
// @Failure 400 {object} ApiError "Unable to reach SQL or Data problems"
// @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(&param)
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(&param)
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(&param)
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(&params)
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(&param)
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(&params)
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(),
})
}

View File

@ -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(&params)
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)

View File

@ -0,0 +1,244 @@
package repository
import (
"context"
"database/sql"
"errors"
"fmt"
"time"
"git.jamestombleson.com/jtom38/newsbot-api/internal/domain"
"github.com/huandu/go-sqlbuilder"
)
const (
ArticleOrderByPublishDateDesc = "pubDate desc"
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
defaultOffset int
}
func NewArticleRepository(conn *sql.DB) ArticleRepository {
return ArticleRepository{
conn: conn,
defaultLimit: 50,
defaultOffset: 50,
}
}
func (ar ArticleRepository) GetById(ctx context.Context, 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.QueryContext(ctx, 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(ctx context.Context, 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.QueryContext(ctx, 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) 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.QueryContext(ctx, 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(ctx context.Context, page, limit int) ([]domain.ArticleEntity, error) {
builder := sqlbuilder.NewSelectBuilder()
builder.Select("*")
builder.From("articles")
builder.OrderBy(ArticleOrderByPublishDateDesc)
builder.Offset(page * limit)
builder.Limit(limit)
query, args := builder.Build()
rows, err := ar.conn.QueryContext(ctx, 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(ctx context.Context, page, limit int, orderBy string) ([]domain.ArticleEntity, error) {
builder := sqlbuilder.NewSelectBuilder()
builder.Select("*")
builder.From("articles")
if orderBy != "" {
builder.OrderBy(orderBy)
}
builder.Offset(page * limit)
builder.Limit(limit)
query, args := builder.Build()
rows, err := ar.conn.QueryContext(ctx, 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(ctx context.Context, page, limit, sourceId int, orderBy string) ([]domain.ArticleEntity, error) {
builder := sqlbuilder.NewSelectBuilder()
builder.Select("*")
builder.From("articles")
builder.JoinWithOption("InnerJoin", "sources", "articles.sourceId=sources.Id")
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.QueryContext(ctx, 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(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")
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.ExecContext(ctx, 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 time.Time
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,
DeletedAt: deletedAt,
SourceID: sourceId,
Tags: tags,
Title: title,
Url: url,
PubDate: pubDate,
IsVideo: isVideo,
Thumbnail: thumbnail,
Description: description,
AuthorName: authorName,
AuthorImageUrl: authorImageUrl,
}
items = append(items, item)
}
return items
}

View File

@ -0,0 +1,156 @@
package repository_test
import (
"context"
"testing"
"time"
"git.jamestombleson.com/jtom38/newsbot-api/internal/repository"
)
const (
articleFakeDotCom = "www.fake.com"
)
func TestCreateArticle(t *testing.T) {
t.Log(time.Time{})
db, err := setupInMemoryDb()
if err != nil {
t.Log(err)
t.FailNow()
}
defer db.Close()
r := repository.NewArticleRepository(db)
created, err := r.Create(context.Background(), 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", 0)
if err != nil {
t.Log(err)
t.FailNow()
}
article, err := r.GetByUrl(context.Background(), 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", 0)
insertFakeArticles(r, "u2", 0)
insertFakeArticles(r, "u3", 0)
insertFakeArticles(r, "u4", 0)
items, err := r.ListTop(context.Background(), 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", 0)
insertFakeArticles(r, "u2", 0)
insertFakeArticles(r, "u3", 0)
insertFakeArticles(r, "u4", 0)
items, err := r.ListByPage(context.Background(), 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 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(context.Background(), 0, 2, repository.ArticleOrderByPublishDateDesc)
if err != nil {
t.Log(err)
t.FailNow()
}
if len(items) != 2 {
t.Log("expected two items back")
t.FailNow()
}
pubDate := items[1].PubDate.Day()
expectedDay := today.Day() - 1
if pubDate != expectedDay {
t.Log("expected a 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(context.Background(), 1, "", title, articleFakeDotCom, "", "testing", "", "", pubDate, false)
if err != nil {
return err
}
return nil
}

View File

@ -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
}

View File

@ -0,0 +1,226 @@
package repository
import (
"context"
"database/sql"
"time"
"git.jamestombleson.com/jtom38/newsbot-api/internal/domain"
"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
}
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", "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...)
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()),
)
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) 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()),
)
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) SoftDelete(ctx context.Context, id int64) (int64, error) {
return softDeleteRow(ctx, r.conn, "DiscordWebHooks", id)
}
func (r discordWebHookRepository) Restore(ctx context.Context, id int64) (int64, error) {
return restoreRow(ctx, r.conn, "DiscordWebHooks", id)
}
func (r discordWebHookRepository) Delete(ctx context.Context, id int64) (int64, error) {
return deleteFromTable(ctx, r.conn, "DiscordWebHooks", id)
}
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 time.Time
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,
DeletedAt: deletedAt,
Url: url,
Server: server,
Channel: channel,
Enabled: enabled,
}
items = append(items, item)
}
return items, nil
}

View File

@ -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)
}

View File

@ -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)

View File

@ -0,0 +1,253 @@
package repository
import (
"context"
"database/sql"
"time"
"git.jamestombleson.com/jtom38/newsbot-api/internal/domain"
"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
}
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
}

View File

@ -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()
}
}

View File

@ -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() {

View File

@ -12,7 +12,6 @@ import (
)
func TestCanCreateNewUser(t *testing.T) {
//t.Log(time.Now().String())
db, err := setupInMemoryDb()
if err != nil {
t.Log(err)

View File

@ -0,0 +1,25 @@
package services
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),
}
}

View File

@ -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,
}
}

View File

@ -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 {

View File

@ -9,7 +9,7 @@ import (
var rssRecord = domain.SourceEntity{
ID: 1,
Name: "ArsTechnica",
DisplayName: "ArsTechnica",
Url: "https://feeds.arstechnica.com/arstechnica/index",
}

View File

@ -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 .
@ -20,7 +19,7 @@ migrate-dev-down: ## revert sql migrations to dev db
swag: ## Generates the swagger documentation with the swag tool
~/go/bin/swag f
~/go/bin/swag i
~/go/bin/swag init -g cmd/server.go
gensql: ## Generates SQL code with sqlc
sqlc generate