From bf969c5abd4a8660430bab15a41a3ac2f6d04a5a Mon Sep 17 00:00:00 2001 From: Satont Date: Fri, 22 Dec 2023 19:24:51 +0300 Subject: [PATCH] kruto --- cmd/main.go | 21 +- go.mod | 19 +- go.sum | 52 +++-- internal/domain/enums.go | 1 + internal/i18n/store/i18n_store.go | 2 +- internal/i18n/store/impl.go | 99 +++++++++ internal/pgx/pgx.go | 42 ++++ internal/repository/channel/channel.go | 2 +- internal/repository/channel/fx.go | 12 ++ internal/repository/channel/pgx.go | 87 ++++---- internal/repository/chat/chat.go | 15 +- internal/repository/chat/fx.go | 12 ++ internal/repository/chat/pgx.go | 141 +++++++++++++ .../repository/chat_settings/chat_settings.go | 3 +- internal/repository/chat_settings/fx.go | 12 ++ internal/repository/chat_settings/pgx.go | 175 ++++++++++++++++ internal/repository/follow/follow.go | 5 - internal/repository/follow/fx.go | 12 ++ internal/repository/follow/pgx.go | 169 +++++++++++++++ internal/repository/fx/fx.go | 18 ++ internal/repository/stream/fx.go | 12 ++ internal/repository/stream/pgx.go | 196 ++++++++++++++++++ internal/repository/stream/stream.go | 13 +- locales/{uk.json => ua.json} | 0 pkg/i18n/helpers.go | 15 -- pkg/i18n/helpers_test.go | 26 --- pkg/i18n/i18n.go | 81 -------- pkg/i18n/i18n_test.go | 124 ----------- pkg/i18n/mocks/i18_mock.go | 21 -- pkg/i18n/test_locales/en.json | 7 - pkg/logger/fx.go | 21 ++ pkg/logger/impl.go | 105 ++++++++++ pkg/logger/logger.go | 14 ++ 33 files changed, 1183 insertions(+), 351 deletions(-) create mode 100644 internal/i18n/store/impl.go create mode 100644 internal/pgx/pgx.go create mode 100644 internal/repository/channel/fx.go create mode 100644 internal/repository/chat/fx.go create mode 100644 internal/repository/chat/pgx.go create mode 100644 internal/repository/chat_settings/fx.go create mode 100644 internal/repository/chat_settings/pgx.go create mode 100644 internal/repository/follow/fx.go create mode 100644 internal/repository/follow/pgx.go create mode 100644 internal/repository/fx/fx.go create mode 100644 internal/repository/stream/fx.go create mode 100644 internal/repository/stream/pgx.go rename locales/{uk.json => ua.json} (100%) delete mode 100644 pkg/i18n/helpers.go delete mode 100644 pkg/i18n/helpers_test.go delete mode 100644 pkg/i18n/i18n.go delete mode 100644 pkg/i18n/i18n_test.go delete mode 100644 pkg/i18n/mocks/i18_mock.go delete mode 100644 pkg/i18n/test_locales/en.json create mode 100644 pkg/logger/fx.go create mode 100644 pkg/logger/impl.go create mode 100644 pkg/logger/logger.go diff --git a/cmd/main.go b/cmd/main.go index faed34d4..86d84524 100644 --- a/cmd/main.go +++ b/cmd/main.go @@ -1,9 +1,28 @@ package main import ( + i18nstore "github.com/satont/twitch-notifier/internal/i18n/store" + "github.com/satont/twitch-notifier/internal/pgx" + repositories "github.com/satont/twitch-notifier/internal/repository/fx" + "github.com/satont/twitch-notifier/pkg/logger" "go.uber.org/fx" ) func main() { - fx.New().Run() + fx.New( + // fx.NopLogger, + fx.Provide( + fx.Annotate( + logger.NewFx(), + fx.As(new(logger.Logger)), + ), + fx.Annotate( + i18nstore.New, + fx.As(new(i18nstore.I18nStore)), + ), + pgx.New, + ), + repositories.Module, + fx.Invoke(pgx.New), + ).Run() } diff --git a/go.mod b/go.mod index 51bb1d85..ead5e63d 100644 --- a/go.mod +++ b/go.mod @@ -4,28 +4,35 @@ go 1.21 require ( github.com/Masterminds/squirrel v1.5.4 + github.com/getsentry/sentry-go v0.22.0 github.com/google/uuid v1.4.0 github.com/jackc/pgx/v5 v5.5.1 - github.com/stretchr/testify v1.8.4 + github.com/rs/zerolog v1.31.0 + github.com/samber/slog-multi v1.0.2 + github.com/samber/slog-sentry/v2 v2.2.1 + github.com/samber/slog-zerolog/v2 v2.1.0 go.uber.org/fx v1.20.1 + gopkg.in/guregu/null.v4 v4.0.0 ) require ( - github.com/davecgh/go-spew v1.1.1 // indirect github.com/jackc/pgpassfile v1.0.0 // indirect github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a // indirect github.com/jackc/puddle/v2 v2.2.1 // indirect github.com/lann/builder v0.0.0-20180802200727-47ae307949d0 // indirect github.com/lann/ps v0.0.0-20150810152359-62de8c46ede0 // indirect - github.com/pmezard/go-difflib v1.0.0 // indirect - github.com/rogpeppe/go-internal v1.9.0 // indirect - github.com/stretchr/objx v0.5.1 // indirect + github.com/mattn/go-colorable v0.1.13 // indirect + github.com/mattn/go-isatty v0.0.19 // indirect + github.com/pkg/errors v0.9.1 // indirect + github.com/samber/lo v1.38.1 // indirect + github.com/samber/slog-common v0.11.0 // indirect + github.com/stretchr/testify v1.8.4 // indirect go.uber.org/dig v1.17.0 // indirect go.uber.org/multierr v1.11.0 // indirect go.uber.org/zap v1.26.0 // indirect golang.org/x/crypto v0.9.0 // indirect + golang.org/x/exp v0.0.0-20230522175609-2e198f4a06a1 // indirect golang.org/x/sync v0.1.0 // indirect golang.org/x/sys v0.14.0 // indirect golang.org/x/text v0.14.0 // indirect - gopkg.in/yaml.v3 v3.0.1 // indirect ) diff --git a/go.sum b/go.sum index c0d88ee7..75a1c248 100644 --- a/go.sum +++ b/go.sum @@ -2,9 +2,17 @@ github.com/Masterminds/squirrel v1.5.4 h1:uUcX/aBc8O7Fg9kaISIUsHXdKuqehiXAMQTYX8 github.com/Masterminds/squirrel v1.5.4/go.mod h1:NNaOrjSoIDfDA40n7sr2tPNZRfjzjA400rg+riTZj10= github.com/benbjohnson/clock v1.3.0 h1:ip6w0uFQkncKQ979AypyG0ER7mqUSBdKLOgAle/AT8A= github.com/benbjohnson/clock v1.3.0/go.mod h1:J11/hYXuz8f4ySSvYwY0FKfm+ezbsZBKZxNJlLklBHA= +github.com/coreos/go-systemd/v22 v22.5.0/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/getsentry/sentry-go v0.22.0 h1:XNX9zKbv7baSEI65l+H1GEJgSeIC1c7EN5kluWaP6dM= +github.com/getsentry/sentry-go v0.22.0/go.mod h1:lc76E2QywIyW8WuBnwl8Lc4bkmQH4+w1gwTf25trprY= +github.com/go-errors/errors v1.4.2 h1:J6MZopCL4uSllY1OfXM374weqZFFItUbrImctkmUxIA= +github.com/go-errors/errors v1.4.2/go.mod h1:sIVyrIiJhuEF+Pj9Ebtd6P/rEYROXFi3BopGUQ5a5Og= +github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA= +github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38= +github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= github.com/google/uuid v1.4.0 h1:MtMxsa51/r9yyhkyLsVeVt0B+BGQZzpQiTQ4eHZ8bc4= github.com/google/uuid v1.4.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsIM= @@ -15,29 +23,38 @@ github.com/jackc/pgx/v5 v5.5.1 h1:5I9etrGkLrN+2XPCsi6XLlV5DITbSL/xBZdmAxFcXPI= github.com/jackc/pgx/v5 v5.5.1/go.mod h1:Ig06C2Vu0t5qXC60W8sqIthScaEnFvojjj9dSljmHRA= github.com/jackc/puddle/v2 v2.2.1 h1:RhxXJtFG022u4ibrCSMSiu5aOq1i77R3OHKNJj77OAk= github.com/jackc/puddle/v2 v2.2.1/go.mod h1:vriiEXHvEE654aYKXXjOvZM39qJ0q+azkZFrfEOc3H4= -github.com/kr/pretty v0.3.0 h1:WgNl7dwNpEZ6jJ9k1snq4pZsg7DOEN8hP9Xw0Tsjwk0= -github.com/kr/pretty v0.3.0/go.mod h1:640gp4NfQd8pI5XOwp5fnNeVWj67G7CFk/SaSQn7NBk= -github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= -github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= github.com/lann/builder v0.0.0-20180802200727-47ae307949d0 h1:SOEGU9fKiNWd/HOJuq6+3iTQz8KNCLtVX6idSoTLdUw= github.com/lann/builder v0.0.0-20180802200727-47ae307949d0/go.mod h1:dXGbAdH5GtBTC4WfIxhKZfyBF/HBFgRZSWwZ9g/He9o= github.com/lann/ps v0.0.0-20150810152359-62de8c46ede0 h1:P6pPBnrTSX3DEVR4fDembhRWSsG5rVo6hYhAB/ADZrk= github.com/lann/ps v0.0.0-20150810152359-62de8c46ede0/go.mod h1:vmVJ0l/dxyfGW6FmdpVm2joNMFikkuWg0EoCKLGUMNw= +github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA= +github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg= +github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= +github.com/mattn/go-isatty v0.0.19 h1:JITubQf0MOLdlGRuRq+jtsDlekdYPia9ZFsB8h/APPA= +github.com/mattn/go-isatty v0.0.19/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= +github.com/pingcap/errors v0.11.4 h1:lFuQV/oaUMGcD2tqt+01ROSmJs75VG1ToEOkZIZ4nE4= +github.com/pingcap/errors v0.11.4/go.mod h1:Oi8TUi2kEtXXLMJk9l1cGmz20kV3TaQ0usTwv5KuLY8= +github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= +github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= -github.com/rogpeppe/go-internal v1.9.0 h1:73kH8U+JUqXU8lRuOHeVHaa/SZPifC7BkcraZVejAe8= -github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs= +github.com/rs/xid v1.5.0/go.mod h1:trrq9SKmegXys3aeAKXMUTdJsYXVwGY3RLcfgqegfbg= +github.com/rs/zerolog v1.31.0 h1:FcTR3NnLWW+NnTwwhFWiJSZr4ECLpqCm6QsEnyvbV4A= +github.com/rs/zerolog v1.31.0/go.mod h1:/7mN4D5sKwJLZQ2b/znpjC3/GQWY/xaDXUM0kKWRHss= +github.com/samber/lo v1.38.1 h1:j2XEAqXKb09Am4ebOg31SpvzUTTs6EN3VfgeLUhPdXM= +github.com/samber/lo v1.38.1/go.mod h1:+m/ZKRl6ClXCE2Lgf3MsQlWfh4bn1bz6CXEOxnEXnEA= +github.com/samber/slog-common v0.11.0 h1:JdESCaXcEwdtoTCYHKQFfHGbWN2vZJq0DDGEE/lwTUQ= +github.com/samber/slog-common v0.11.0/go.mod h1:Qjrfhwk79XiCIhBj8+jTq1Cr0u9rlWbjawh3dWXzaHk= +github.com/samber/slog-multi v1.0.2 h1:6BVH9uHGAsiGkbbtQgAOQJMpKgV8unMrHhhJaw+X1EQ= +github.com/samber/slog-multi v1.0.2/go.mod h1:uLAvHpGqbYgX4FSL0p1ZwoLuveIAJvBECtE07XmYvFo= +github.com/samber/slog-sentry/v2 v2.2.1 h1:W1ztzl5t1QYZkdQt9uZywgKpQFThyUoU+habjlTk9b0= +github.com/samber/slog-sentry/v2 v2.2.1/go.mod h1:Bo0hgH6/fqXoF2ZwtHLHFh4uyA/YbXykQR5aaI+IkYc= +github.com/samber/slog-zerolog/v2 v2.1.0 h1:QdS3X5gLqrP3TVS7fAu3gk1e92LsZAdk5AnRHFc8X0E= +github.com/samber/slog-zerolog/v2 v2.1.0/go.mod h1:9WQLMvggH2Tt0yt0D9hJ6rpnOfAKLpdvF8bqRqF4YY4= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= -github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= -github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= -github.com/stretchr/objx v0.5.1 h1:4VhoImhV/Bm0ToFkXFi8hXNXwpDRZ/ynw3amt82mzq0= -github.com/stretchr/objx v0.5.1/go.mod h1:/iHQpkQwBD6DLUmQ4pE+s1TXdob1mORJ4/UFdrifcy0= github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= -github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= -github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= -github.com/stretchr/testify v1.8.2/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk= github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= go.uber.org/atomic v1.7.0 h1:ADUqmZGgLDDfbSL9ZmPxKTybcoEYHgpYfELNoN+7hsw= @@ -54,15 +71,20 @@ go.uber.org/zap v1.26.0 h1:sI7k6L95XOKS281NhVKOFCUNIvv9e0w4BF8N3u+tCRo= go.uber.org/zap v1.26.0/go.mod h1:dtElttAiwGvoJ/vj4IwHBS/gXsEu/pZ50mUIRWuG0so= golang.org/x/crypto v0.9.0 h1:LF6fAI+IutBocDJ2OT0Q1g8plpYljMZ4+lty+dsqw3g= golang.org/x/crypto v0.9.0/go.mod h1:yrmDGqONDYtNj3tH8X9dzUun2m2lzPa9ngI6/RUPGR0= +golang.org/x/exp v0.0.0-20230522175609-2e198f4a06a1 h1:k/i9J1pBpvlfR+9QsetwPyERsqu1GIbi967PQMq3Ivc= +golang.org/x/exp v0.0.0-20230522175609-2e198f4a06a1/go.mod h1:V1LtkGg67GoY2N1AnLN78QLrzxkLyJw7RJb1gzOOz9w= golang.org/x/sync v0.1.0 h1:wsuoTGHzEhffawBOhz5CYhcrV4IdKZbEyZjBMuTp12o= golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.14.0 h1:Vz7Qs629MkJkGyHxUlRHizWJRG2j8fbQKjELVSNhy7Q= golang.org/x/sys v0.14.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ= golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= -gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= -gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= +gopkg.in/guregu/null.v4 v4.0.0 h1:1Wm3S1WEA2I26Kq+6vcW+w0gcDo44YKYD7YIEJNHDjg= +gopkg.in/guregu/null.v4 v4.0.0/go.mod h1:YoQhUrADuG3i9WqesrCmpNRwm1ypAgSHYqoOcTu/JrI= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/internal/domain/enums.go b/internal/domain/enums.go index 3520e98a..75f7f583 100644 --- a/internal/domain/enums.go +++ b/internal/domain/enums.go @@ -9,6 +9,7 @@ func (l Language) String() string { const ( LanguageEN Language = "en" LanguageRU Language = "ru" + LanguageUA Language = "ua" ) type ChatService string diff --git a/internal/i18n/store/i18n_store.go b/internal/i18n/store/i18n_store.go index d7f7d1be..70efb2cd 100644 --- a/internal/i18n/store/i18n_store.go +++ b/internal/i18n/store/i18n_store.go @@ -6,5 +6,5 @@ import ( type I18nStore interface { GetKey(language domain.Language, key string) (string, error) - GetSupportedLanguages() ([]domain.Language, error) + GetSupportedLanguages() []domain.Language } diff --git a/internal/i18n/store/impl.go b/internal/i18n/store/impl.go new file mode 100644 index 00000000..421afeb9 --- /dev/null +++ b/internal/i18n/store/impl.go @@ -0,0 +1,99 @@ +package store + +import ( + "encoding/json" + "errors" + "log/slog" + "os" + "path/filepath" + "strings" + + "github.com/satont/twitch-notifier/internal/domain" + "github.com/satont/twitch-notifier/pkg/logger" +) + +type translation map[string]any + +func New(l logger.Logger) (*Store, error) { + store := &Store{ + locales: make(map[domain.Language]translation), + } + + err := store.readLocales() + if err != nil { + return nil, err + } + + supported := store.GetSupportedLanguages() + l.Info("Locales loaded", slog.Any("locales", supported)) + + return store, nil +} + +type Store struct { + locales map[domain.Language]translation +} + +var _ I18nStore = (*Store)(nil) + +func (c *Store) readLocales() error { + pwd, err := os.Getwd() + if err != nil { + return err + } + + files, err := os.ReadDir(filepath.Join(pwd, "locales")) + if err != nil { + return err + } + + for _, file := range files { + if file.IsDir() { + panic("locales directory should not contain directories") + } + + data := make(map[string]any) + + content, err := os.ReadFile(filepath.Join(pwd, "locales", file.Name())) + if err != nil { + return err + } + + if err := json.Unmarshal(content, &data); err != nil { + return err + } + + name := strings.Replace(file.Name(), ".json", "", 1) + + c.locales[domain.Language(name)] = data + } + + return nil +} + +var ErrLocaleNotFound = errors.New("locale not found") +var ErrKeyNotFound = errors.New("key not found") + +func (c *Store) GetKey(language domain.Language, key string) (string, error) { + lang := c.locales[language] + if lang == nil { + return "", ErrLocaleNotFound + } + + value, ok := lang[key] + if !ok { + return "", ErrKeyNotFound + } + + return value.(string), nil +} + +func (c *Store) GetSupportedLanguages() []domain.Language { + languages := make([]domain.Language, 0, len(c.locales)) + + for language := range c.locales { + languages = append(languages, language) + } + + return languages +} diff --git a/internal/pgx/pgx.go b/internal/pgx/pgx.go new file mode 100644 index 00000000..6f594a1d --- /dev/null +++ b/internal/pgx/pgx.go @@ -0,0 +1,42 @@ +package pgx + +import ( + "context" + + "github.com/jackc/pgx/v5/pgxpool" + "github.com/satont/twitch-notifier/pkg/logger" + "go.uber.org/fx" +) + +type Opts struct { + fx.In + LC fx.Lifecycle + + Logger logger.Logger +} + +func New(opts Opts) (*pgxpool.Pool, error) { + pgx, err := pgxpool.New( + context.Background(), + "postgres://postgres:postgres@localhost:5432/twitch_notifier", + ) + if err != nil { + return nil, err + } + + opts.LC.Append( + fx.Hook{ + OnStop: func(ctx context.Context) error { + pgx.Close() + return nil + }, + OnStart: func(ctx context.Context) error { + // return pgx.Ping(ctx) + opts.Logger.Info("Connected to postgres") + return nil + }, + }, + ) + + return pgx, nil +} diff --git a/internal/repository/channel/channel.go b/internal/repository/channel/channel.go index 5dc3718c..cfa13784 100644 --- a/internal/repository/channel/channel.go +++ b/internal/repository/channel/channel.go @@ -21,6 +21,6 @@ type Repository interface { ) GetAll(ctx context.Context) ([]Channel, error) Create(ctx context.Context, channel Channel) error - Update(ctx context.Context, channel Channel) error + // Update(ctx context.Context, channel Channel) error Delete(ctx context.Context, id uuid.UUID) error } diff --git a/internal/repository/channel/fx.go b/internal/repository/channel/fx.go new file mode 100644 index 00000000..db87dea7 --- /dev/null +++ b/internal/repository/channel/fx.go @@ -0,0 +1,12 @@ +package channel + +import ( + "go.uber.org/fx" +) + +var Module = fx.Provide( + fx.Annotate( + NewPgx, + fx.As(new(Repository)), + ), +) diff --git a/internal/repository/channel/pgx.go b/internal/repository/channel/pgx.go index a331dd14..c8d3ced4 100644 --- a/internal/repository/channel/pgx.go +++ b/internal/repository/channel/pgx.go @@ -24,10 +24,13 @@ type Pgx struct { func (c *Pgx) GetById(ctx context.Context, id uuid.UUID) (Channel, error) { channel := Channel{} - query, args, err := repository.Sq.Select("id", "channel_id", "service").Where( - "id = ?", - id, - ).ToSql() + query, args, err := repository.Sq. + Select("id", "channel_id", "service"). + From("channels"). + Where( + "id = ?", + id, + ).ToSql() if err != nil { return channel, err } @@ -47,11 +50,14 @@ func (c *Pgx) GetByStreamServiceAndID( ) (Channel, error) { channel := Channel{} - query, args, err := repository.Sq.Select("id", "channel_id", "service").Where( - "channel_id = ? AND service = ?", - id, - service, - ).ToSql() + query, args, err := repository.Sq. + Select("id", "channel_id", "service"). + From("channels"). + Where( + "channel_id = ? AND service = ?", + id, + service, + ).ToSql() if err != nil { return channel, err } @@ -67,7 +73,10 @@ func (c *Pgx) GetByStreamServiceAndID( func (c *Pgx) GetAll(ctx context.Context) ([]Channel, error) { var channels []Channel - query, args, err := repository.Sq.Select("id", "channel_id", "service").ToSql() + query, args, err := repository.Sq. + Select("id", "channel_id", "service"). + From("channels"). + ToSql() if err != nil { return channels, err } @@ -91,7 +100,7 @@ func (c *Pgx) GetAll(ctx context.Context) ([]Channel, error) { } func (c *Pgx) Create(ctx context.Context, channel Channel) error { - query, args, err := repository.Sq.Insert("channel").Columns( + query, args, err := repository.Sq.Insert("channels").Columns( "id", "channel_id", "service", @@ -112,35 +121,37 @@ func (c *Pgx) Create(ctx context.Context, channel Channel) error { return nil } -func (c *Pgx) Update(ctx context.Context, channel Channel) error { - query, args, err := repository.Sq.Update("channel").Set( - "channel_id", - channel.ChannelID, - ).Set( - "service", - channel.Service, - ).Where( - "id = ?", - channel.ID, - ).ToSql() - - if err != nil { - return err - } - - _, err = c.pg.Exec(ctx, query, args...) - if err != nil { - return err - } - - return nil -} +// func (c *Pgx) Update(ctx context.Context, channel Channel) error { +// query, args, err := repository.Sq.Update("channel").Set( +// "channel_id", +// channel.ChannelID, +// ).Set( +// "service", +// channel.Service, +// ).Where( +// "id = ?", +// channel.ID, +// ).ToSql() +// +// if err != nil { +// return err +// } +// +// _, err = c.pg.Exec(ctx, query, args...) +// if err != nil { +// return err +// } +// +// return nil +// } func (c *Pgx) Delete(ctx context.Context, id uuid.UUID) error { - query, args, err := repository.Sq.Delete("channel").Where( - "id = ?", - id, - ).ToSql() + query, args, err := repository.Sq. + Delete("channel"). + Where( + "id = ?", + id, + ).ToSql() if err != nil { return err diff --git a/internal/repository/chat/chat.go b/internal/repository/chat/chat.go index 83e95213..96d6b74c 100644 --- a/internal/repository/chat/chat.go +++ b/internal/repository/chat/chat.go @@ -7,17 +7,20 @@ import ( "github.com/satont/twitch-notifier/internal/domain" ) -type User struct { +type Chat struct { ID uuid.UUID Service domain.ChatService ChatID string } type Repository interface { - GetByID(ctx context.Context, id uuid.UUID) (User, error) - GetByChatServiceAndChatID(ctx context.Context, service domain.ChatService, chatID string) (User, error) - GetAll(ctx context.Context) ([]User, error) - Create(ctx context.Context, user User) error - Update(ctx context.Context, user User) error + GetByID(ctx context.Context, id uuid.UUID) (Chat, error) + GetByChatServiceAndChatID(ctx context.Context, service domain.ChatService, chatID string) ( + Chat, + error, + ) + GetAll(ctx context.Context) ([]Chat, error) + Create(ctx context.Context, user Chat) error + // Update(ctx context.Context, user Chat) error Delete(ctx context.Context, id uuid.UUID) error } diff --git a/internal/repository/chat/fx.go b/internal/repository/chat/fx.go new file mode 100644 index 00000000..dc35c6cb --- /dev/null +++ b/internal/repository/chat/fx.go @@ -0,0 +1,12 @@ +package chat + +import ( + "go.uber.org/fx" +) + +var Module = fx.Provide( + fx.Annotate( + NewPgx, + fx.As(new(Repository)), + ), +) diff --git a/internal/repository/chat/pgx.go b/internal/repository/chat/pgx.go new file mode 100644 index 00000000..8c4f29d6 --- /dev/null +++ b/internal/repository/chat/pgx.go @@ -0,0 +1,141 @@ +package chat + +import ( + "context" + + "github.com/google/uuid" + "github.com/jackc/pgx/v5/pgxpool" + "github.com/satont/twitch-notifier/internal/domain" + "github.com/satont/twitch-notifier/internal/repository" +) + +func NewPgx(pg *pgxpool.Pool) *Pgx { + return &Pgx{ + pg: pg, + } +} + +var _ Repository = (*Pgx)(nil) + +type Pgx struct { + pg *pgxpool.Pool +} + +func (c *Pgx) GetByID(ctx context.Context, id uuid.UUID) (Chat, error) { + chat := Chat{} + + query, args, err := repository.Sq. + Select("id", "chat_id", "service"). + From("chats"). + Where( + "id = ?", + id, + ).ToSql() + + if err != nil { + return chat, err + } + + err = c.pg.QueryRow(ctx, query, args...).Scan(&chat.ID, &chat.ChatID, &chat.Service) + if err != nil { + return chat, err + } + + return chat, nil +} + +func (c *Pgx) GetByChatServiceAndChatID( + ctx context.Context, + service domain.ChatService, + chatID string, +) (Chat, error) { + chat := Chat{} + + query, args, err := repository.Sq. + Select("id", "chat_id", "service"). + From("chats"). + Where( + "chat_id = ? AND service = ?", + chatID, + service, + ).ToSql() + if err != nil { + return chat, err + } + + err = c.pg.QueryRow(ctx, query, args...).Scan(&chat.ID, &chat.ChatID, &chat.Service) + if err != nil { + return chat, err + } + + return chat, nil +} + +func (c *Pgx) GetAll(ctx context.Context) ([]Chat, error) { + var chats []Chat + + query, args, err := repository.Sq. + Select("id", "chat_id", "service"). + From("chats"). + ToSql() + if err != nil { + return chats, err + } + + rows, err := c.pg.Query(ctx, query, args...) + if err != nil { + return chats, err + } + + for rows.Next() { + var chat Chat + err = rows.Scan(&chat.ID, &chat.ChatID, &chat.Service) + if err != nil { + return chats, err + } + chats = append(chats, chat) + } + + return chats, nil +} + +func (c *Pgx) Create(ctx context.Context, user Chat) error { + query, args, err := repository.Sq. + Insert("chats"). + Columns( + "id", + "chat_id", + "service", + ).Values( + user.ID, + user.ChatID, + user.Service, + ).ToSql() + if err != nil { + return err + } + + _, err = c.pg.Exec(ctx, query, args...) + if err != nil { + return err + } + + return nil +} + +func (c *Pgx) Delete(ctx context.Context, id uuid.UUID) error { + query, args, err := repository.Sq.Delete("chats").Where( + "id = ?", + id, + ).ToSql() + if err != nil { + return err + } + + _, err = c.pg.Exec(ctx, query, args...) + if err != nil { + return err + } + + return nil +} diff --git a/internal/repository/chat_settings/chat_settings.go b/internal/repository/chat_settings/chat_settings.go index 32d0580a..03fd8006 100644 --- a/internal/repository/chat_settings/chat_settings.go +++ b/internal/repository/chat_settings/chat_settings.go @@ -15,13 +15,12 @@ type ChatSettings struct { TitleChangeNotifications bool OfflineNotifications bool GameAndTitleNotifications bool - ShotThumbnail bool + ShowThumbnail bool } type Repository interface { GetByID(ctx context.Context, id uuid.UUID) (ChatSettings, error) GetByChatID(ctx context.Context, chatID uuid.UUID) (ChatSettings, error) - GetAll(ctx context.Context) ([]ChatSettings, error) Create(ctx context.Context, chatSettings ChatSettings) error Update(ctx context.Context, chatSettings ChatSettings) error Delete(ctx context.Context, id uuid.UUID) error diff --git a/internal/repository/chat_settings/fx.go b/internal/repository/chat_settings/fx.go new file mode 100644 index 00000000..e1a24db3 --- /dev/null +++ b/internal/repository/chat_settings/fx.go @@ -0,0 +1,12 @@ +package chat_settings + +import ( + "go.uber.org/fx" +) + +var Module = fx.Provide( + fx.Annotate( + NewPgx, + fx.As(new(Repository)), + ), +) diff --git a/internal/repository/chat_settings/pgx.go b/internal/repository/chat_settings/pgx.go new file mode 100644 index 00000000..45191181 --- /dev/null +++ b/internal/repository/chat_settings/pgx.go @@ -0,0 +1,175 @@ +package chat_settings + +import ( + "context" + + "github.com/google/uuid" + "github.com/jackc/pgx/v5/pgxpool" + "github.com/satont/twitch-notifier/internal/repository" +) + +func NewPgx(pg *pgxpool.Pool) *Pgx { + return &Pgx{ + pg: pg, + } +} + +var _ Repository = (*Pgx)(nil) + +type Pgx struct { + pg *pgxpool.Pool +} + +func (c *Pgx) GetByID(ctx context.Context, id uuid.UUID) (ChatSettings, error) { + settings := ChatSettings{} + + query, args, err := repository.Sq. + Select( + "id", + "chat_id", + "language", + "game_change_notifications", + "title_change_notifications", + "offline_notifications", + "game_and_title_notifications", + "show_thumbnail", + ). + From("chat_settings"). + Where( + "id = ?", + id, + ).ToSql() + if err != nil { + return settings, err + } + + err = c.pg.QueryRow(ctx, query, args...).Scan( + &settings.ID, + &settings.ChatID, + &settings.Language, + &settings.GameChangeNotifications, + &settings.TitleChangeNotifications, + &settings.OfflineNotifications, + &settings.GameAndTitleNotifications, + &settings.ShowThumbnail, + ) + + return settings, err +} + +func (c *Pgx) GetByChatID(ctx context.Context, chatID uuid.UUID) (ChatSettings, error) { + settings := ChatSettings{} + + query, args, err := repository.Sq. + Select( + "id", + "chat_id", + "language", + "game_change_notifications", + "title_change_notifications", + "offline_notifications", + "game_and_title_notifications", + "show_thumbnail", + ). + From("chat_settings"). + Where( + "chat_id = ?", + chatID, + ).ToSql() + if err != nil { + return settings, err + } + + err = c.pg.QueryRow(ctx, query, args...).Scan( + &settings.ID, + &settings.ChatID, + &settings.Language, + &settings.GameChangeNotifications, + &settings.TitleChangeNotifications, + &settings.OfflineNotifications, + &settings.GameAndTitleNotifications, + &settings.ShowThumbnail, + ) + + return settings, err +} + +func (c *Pgx) Create(ctx context.Context, chatSettings ChatSettings) error { + query, args, err := repository.Sq.Insert("chat_settings").Columns( + "id", + "chat_id", + "language", + "game_change_notifications", + "title_change_notifications", + "offline_notifications", + "game_and_title_notifications", + "show_thumbnail", + ).Values( + chatSettings.ID, + chatSettings.ChatID, + chatSettings.Language, + chatSettings.GameChangeNotifications, + chatSettings.TitleChangeNotifications, + chatSettings.OfflineNotifications, + chatSettings.GameAndTitleNotifications, + chatSettings.ShowThumbnail, + ).ToSql() + if err != nil { + return err + } + + _, err = c.pg.Exec(ctx, query, args...) + return err +} + +func (c *Pgx) Update(ctx context.Context, chatSettings ChatSettings) error { + query, args, err := repository.Sq. + Update("chat_settings"). + Set( + "language", + chatSettings.Language, + ). + Set( + "game_change_notifications", + chatSettings.GameChangeNotifications, + ). + Set( + "title_change_notifications", + chatSettings.TitleChangeNotifications, + ). + Set( + "offline_notifications", + chatSettings.OfflineNotifications, + ). + Set( + "game_and_title_notifications", + chatSettings.GameAndTitleNotifications, + ). + Set( + "show_thumbnail", + chatSettings.ShowThumbnail, + ). + Where( + "id = ?", + chatSettings.ID, + ).ToSql() + if err != nil { + return err + } + + _, err = c.pg.Exec(ctx, query, args...) + return err +} + +func (c *Pgx) Delete(ctx context.Context, id uuid.UUID) error { + query, args, err := repository.Sq.Delete("chat_settings").Where( + "id = ?", + id, + ).ToSql() + if err != nil { + return err + } + + _, err = c.pg.Exec(ctx, query, args...) + return err +} diff --git a/internal/repository/follow/follow.go b/internal/repository/follow/follow.go index 15666dc6..ac6d168b 100644 --- a/internal/repository/follow/follow.go +++ b/internal/repository/follow/follow.go @@ -12,17 +12,12 @@ type Follow struct { ChatID uuid.UUID ChannelID uuid.UUID CreatedAt time.Time - UpdatedAt time.Time } type Repository interface { GetByID(ctx context.Context, id uuid.UUID) (Follow, error) GetByChatID(ctx context.Context, chatID uuid.UUID) ([]Follow, error) GetByChannelID(ctx context.Context, channelID uuid.UUID) ([]Follow, error) - GetAll(ctx context.Context) ([]Follow, error) Create(ctx context.Context, follow Follow) error - Update(ctx context.Context, follow Follow) error Delete(ctx context.Context, id uuid.UUID) error - GetAllByChannelID(ctx context.Context, channelID uuid.UUID) ([]Follow, error) - GetAllByChatID(ctx context.Context, chatID uuid.UUID) ([]Follow, error) } diff --git a/internal/repository/follow/fx.go b/internal/repository/follow/fx.go new file mode 100644 index 00000000..e5369cc3 --- /dev/null +++ b/internal/repository/follow/fx.go @@ -0,0 +1,12 @@ +package follow + +import ( + "go.uber.org/fx" +) + +var Module = fx.Provide( + fx.Annotate( + NewPgx, + fx.As(new(Repository)), + ), +) diff --git a/internal/repository/follow/pgx.go b/internal/repository/follow/pgx.go new file mode 100644 index 00000000..4f442f2b --- /dev/null +++ b/internal/repository/follow/pgx.go @@ -0,0 +1,169 @@ +package follow + +import ( + "context" + + "github.com/google/uuid" + "github.com/jackc/pgx/v5/pgxpool" + "github.com/satont/twitch-notifier/internal/repository" +) + +func NewPgx(pg *pgxpool.Pool) *Pgx { + return &Pgx{ + pg: pg, + } +} + +var _ Repository = (*Pgx)(nil) + +type Pgx struct { + pg *pgxpool.Pool +} + +func (c *Pgx) GetByID(ctx context.Context, id uuid.UUID) (Follow, error) { + follow := Follow{} + + query, args, err := repository.Sq. + Select( + "id", + "chat_id", + "channel_id", + "created_at", + ). + From("follows"). + Where( + "id = ?", + id, + ).ToSql() + if err != nil { + return follow, err + } + + err = c.pg.QueryRow(ctx, query, args...).Scan( + &follow.ID, + &follow.ChatID, + &follow.ChannelID, + &follow.CreatedAt, + ) + + return follow, err +} + +func (c *Pgx) GetByChatID(ctx context.Context, chatID uuid.UUID) ([]Follow, error) { + query, args, err := repository.Sq. + Select( + "id", + "chat_id", + "channel_id", + "created_at", + ). + From("follows"). + Where( + "chat_id = ?", + chatID, + ).ToSql() + if err != nil { + return nil, err + } + + rows, err := c.pg.Query(ctx, query, args...) + if err != nil { + return nil, err + } + + defer rows.Close() + + var follows []Follow + for rows.Next() { + follow := Follow{} + err = rows.Scan( + &follow.ID, + &follow.ChatID, + &follow.ChannelID, + &follow.CreatedAt, + ) + if err != nil { + return nil, err + } + follows = append(follows, follow) + } + + return follows, err +} + +func (c *Pgx) GetByChannelID(ctx context.Context, channelID uuid.UUID) ([]Follow, error) { + query, args, err := repository.Sq. + Select( + "id", + "chat_id", + "channel_id", + "created_at", + ). + From("follows"). + Where( + "channel_id = ?", + channelID, + ).ToSql() + if err != nil { + return nil, err + } + + rows, err := c.pg.Query(ctx, query, args...) + if err != nil { + return nil, err + } + + defer rows.Close() + var follows []Follow + for rows.Next() { + follow := Follow{} + err = rows.Scan( + &follow.ID, + &follow.ChatID, + &follow.ChannelID, + &follow.CreatedAt, + ) + if err != nil { + return nil, err + } + follows = append(follows, follow) + } + + return follows, err +} + +func (c *Pgx) Create(ctx context.Context, follow Follow) error { + query, args, err := repository.Sq. + Insert("follows"). + Columns( + "id", + "chat_id", + "channel_id", + ). + Values( + follow.ID, + follow.ChatID, + follow.ChannelID, + ).ToSql() + if err != nil { + return err + } + + _, err = c.pg.Exec(ctx, query, args...) + return err +} + +func (c *Pgx) Delete(ctx context.Context, id uuid.UUID) error { + query, args, err := repository.Sq. + Delete("follows"). + Where( + "id = ?", + id, + ).ToSql() + if err != nil { + return err + } + + _, err = c.pg.Exec(ctx, query, args...) + return err +} diff --git a/internal/repository/fx/fx.go b/internal/repository/fx/fx.go new file mode 100644 index 00000000..75f6dc44 --- /dev/null +++ b/internal/repository/fx/fx.go @@ -0,0 +1,18 @@ +package fx + +import ( + "github.com/satont/twitch-notifier/internal/repository/channel" + "github.com/satont/twitch-notifier/internal/repository/chat" + "github.com/satont/twitch-notifier/internal/repository/chat_settings" + "github.com/satont/twitch-notifier/internal/repository/follow" + "github.com/satont/twitch-notifier/internal/repository/stream" + "go.uber.org/fx" +) + +var Module = fx.Options( + channel.Module, + chat.Module, + chat_settings.Module, + follow.Module, + stream.Module, +) diff --git a/internal/repository/stream/fx.go b/internal/repository/stream/fx.go new file mode 100644 index 00000000..194bc324 --- /dev/null +++ b/internal/repository/stream/fx.go @@ -0,0 +1,12 @@ +package stream + +import ( + "go.uber.org/fx" +) + +var Module = fx.Provide( + fx.Annotate( + NewPgx, + fx.As(new(Repository)), + ), +) diff --git a/internal/repository/stream/pgx.go b/internal/repository/stream/pgx.go new file mode 100644 index 00000000..d61d3bac --- /dev/null +++ b/internal/repository/stream/pgx.go @@ -0,0 +1,196 @@ +package stream + +import ( + "context" + + "github.com/google/uuid" + "github.com/jackc/pgx/v5/pgxpool" + "github.com/satont/twitch-notifier/internal/repository" +) + +func NewPgx(pg *pgxpool.Pool) *Pgx { + return &Pgx{ + pg: pg, + } +} + +var _ Repository = (*Pgx)(nil) + +type Pgx struct { + pg *pgxpool.Pool +} + +func (c *Pgx) GetById(ctx context.Context, id uuid.UUID) (Stream, error) { + stream := Stream{} + query, args, err := repository.Sq. + Select( + "id", + "channel_id", + "titles", + "categories", + "started_at", + "updated_at", + "ended_at", + ). + From("streams"). + Where("id = ?", id). + ToSql() + if err != nil { + return stream, err + } + + err = c.pg.QueryRow(ctx, query, args...).Scan( + &stream.ID, + &stream.ChannelID, + &stream.Titles, + &stream.Categories, + &stream.StartedAt, + &stream.UpdatedAt, + &stream.EndedAt, + ) + return stream, err +} + +func (c *Pgx) GetLatestByChannelId(ctx context.Context, channelId uuid.UUID) (Stream, error) { + stream := Stream{} + + query, args, err := repository.Sq. + Select( + "id", + "channel_id", + "titles", + "categories", + "started_at", + "updated_at", + "ended_at", + ). + From("streams"). + Where("channel_id = ?", channelId). + OrderBy("started_at DESC"). + Limit(1). + ToSql() + if err != nil { + return stream, err + } + + err = c.pg.QueryRow(ctx, query, args...).Scan( + &stream.ID, + &stream.ChannelID, + &stream.Titles, + &stream.Categories, + &stream.StartedAt, + &stream.UpdatedAt, + &stream.EndedAt, + ) + return stream, err +} + +func (c *Pgx) GetByChannelId(ctx context.Context, channelId uuid.UUID) ([]Stream, error) { + query, args, err := repository.Sq. + Select( + "id", + "channel_id", + "titles", + "categories", + "started_at", + "updated_at", + "ended_at", + ). + From("streams"). + Where("channel_id = ?", channelId). + OrderBy("started_at DESC"). + ToSql() + if err != nil { + return nil, err + } + + rows, err := c.pg.Query(ctx, query, args...) + if err != nil { + return nil, err + } + + defer rows.Close() + var streams []Stream + + rows.RawValues() + for rows.Next() { + stream := Stream{} + err = rows.Scan( + &stream.ID, + &stream.ChannelID, + &stream.Titles, + &stream.Categories, + &stream.StartedAt, + &stream.UpdatedAt, + &stream.EndedAt, + ) + if err != nil { + return nil, err + } + streams = append(streams, stream) + } + + return streams, nil +} + +func (c *Pgx) Create(ctx context.Context, stream Stream) error { + query, args, err := repository.Sq. + Insert("streams"). + Columns( + "id", + "channel_id", + "titles", + "categories", + "started_at", + "updated_at", + "ended_at", + ). + Values( + stream.ID, + stream.ChannelID, + stream.Titles, + stream.Categories, + stream.StartedAt, + stream.UpdatedAt, + stream.EndedAt, + ).ToSql() + if err != nil { + return err + } + + _, err = c.pg.Exec(ctx, query, args...) + return err +} + +func (c *Pgx) Update(ctx context.Context, stream Stream) error { + query, args, err := repository.Sq. + Update("streams"). + Set("channel_id", stream.ChannelID). + Set("titles", stream.Titles). + Set("categories", stream.Categories). + Set("started_at", stream.StartedAt). + Set("updated_at", stream.UpdatedAt). + Set("ended_at", stream.EndedAt). + Where("id = ?", stream.ID). + ToSql() + + if err != nil { + return err + } + + _, err = c.pg.Exec(ctx, query, args...) + return err +} + +func (c *Pgx) Delete(ctx context.Context, id uuid.UUID) error { + query, args, err := repository.Sq. + Delete("streams"). + Where("id = ?", id). + ToSql() + if err != nil { + return err + } + + _, err = c.pg.Exec(ctx, query, args...) + return err +} diff --git a/internal/repository/stream/stream.go b/internal/repository/stream/stream.go index aa68e98d..0416d16a 100644 --- a/internal/repository/stream/stream.go +++ b/internal/repository/stream/stream.go @@ -2,17 +2,26 @@ package stream import ( "context" + "time" "github.com/google/uuid" + "gopkg.in/guregu/null.v4" ) type Stream struct { - ID uuid.UUID + ID uuid.UUID + ChannelID uuid.UUID + Titles []string + Categories []string + StartedAt time.Time + UpdatedAt time.Time + EndedAt null.Time } type Repository interface { GetById(ctx context.Context, id uuid.UUID) (Stream, error) - GetAll(ctx context.Context) ([]Stream, error) + GetLatestByChannelId(ctx context.Context, channelId uuid.UUID) (Stream, error) + GetByChannelId(ctx context.Context, channelId uuid.UUID) ([]Stream, error) Create(ctx context.Context, stream Stream) error Update(ctx context.Context, stream Stream) error Delete(ctx context.Context, id uuid.UUID) error diff --git a/locales/uk.json b/locales/ua.json similarity index 100% rename from locales/uk.json rename to locales/ua.json diff --git a/pkg/i18n/helpers.go b/pkg/i18n/helpers.go deleted file mode 100644 index c53fdfef..00000000 --- a/pkg/i18n/helpers.go +++ /dev/null @@ -1,15 +0,0 @@ -package i18n - -func GetNested[T any](v any, keys ...string) (T, bool) { - res := v - for _, key := range keys { - mp, ok := res.(map[string]any) - if !ok { - var e T - return e, false - } - res = mp[key] - } - a, ok := res.(T) - return a, ok -} diff --git a/pkg/i18n/helpers_test.go b/pkg/i18n/helpers_test.go deleted file mode 100644 index f4d0e4e1..00000000 --- a/pkg/i18n/helpers_test.go +++ /dev/null @@ -1,26 +0,0 @@ -package i18n - -import ( - "github.com/stretchr/testify/assert" - "testing" -) - -func TestGetNested(t *testing.T) { - t.Parallel() - - data := map[string]any{ - "foo": "bar", - } - - res, ok := GetNested[string](data, "foo") - assert.True(t, ok, "expected to get a value") - assert.Equal(t, "bar", res, "expected to get a value") - - res, ok = GetNested[string](data, "bar") - assert.False(t, ok, "expected to be false") - assert.Equal(t, "", res, "expected to not get a value") - - res, ok = GetNested[string](nil, "foo") - assert.False(t, ok, "expected to be false") - assert.Equal(t, "", res, "expected to not get a value") -} diff --git a/pkg/i18n/i18n.go b/pkg/i18n/i18n.go deleted file mode 100644 index be5ec86f..00000000 --- a/pkg/i18n/i18n.go +++ /dev/null @@ -1,81 +0,0 @@ -package i18n - -import ( - "encoding/json" - "os" - "path/filepath" - "strings" - "text/template" -) - -type Interface interface { - Translate(key, language string, data map[string]string) string - GetLanguagesCodes() []string -} - -type I18n struct { - translations map[string]map[string]any -} - -var readFile = os.ReadFile -var readDir = os.ReadDir - -func NewI18n(localesPath string) (Interface, error) { - entries, err := os.ReadDir(localesPath) - if err != nil { - return nil, err - } - - translations := make(map[string]map[string]any) - - for _, entry := range entries { - if entry.IsDir() { - continue - } - - name := strings.Replace(entry.Name(), ".json", "", 1) - - fileContent := make(map[string]any) - f, err := readFile(filepath.Join(localesPath, entry.Name())) - if err != nil { - return nil, err - } - err = json.Unmarshal(f, &fileContent) - translations[name] = fileContent - } - - return &I18n{ - translations: translations, - }, nil -} - -func (i *I18n) Translate(key, language string, data map[string]string) string { - if data == nil { - data = make(map[string]string) - } - - str, _ := GetNested[string](i.translations[language], strings.Split(key, ".")...) - if str == "" { - str, _ = GetNested[string](i.translations["en"], strings.Split(key, ".")...) - } - - str = strings.ReplaceAll(str, "{{ ", "{{.") - - tmpl, err := template.New("t").Parse(str) - if err != nil { - return str - } - - res := &strings.Builder{} - _ = tmpl.Execute(res, data) - - return res.String() -} - -func (i *I18n) GetLanguagesCodes() []string { - var codes []string - for code, _ := range i.translations { - codes = append(codes, code) - } - return codes -} diff --git a/pkg/i18n/i18n_test.go b/pkg/i18n/i18n_test.go deleted file mode 100644 index 243429f0..00000000 --- a/pkg/i18n/i18n_test.go +++ /dev/null @@ -1,124 +0,0 @@ -package i18n - -import ( - "os" - "path/filepath" - "testing" - - "github.com/stretchr/testify/assert" -) - -func TestNewI18n(t *testing.T) { - t.Parallel() - - wd, err := os.Getwd() - if err != nil { - t.Fatal(err) - } - - localesPath := filepath.Join(wd, "test_locales") - - table := []struct { - translation string - lang string - data map[string]string - expected string - expectErr bool - localesPath string - patchReadFile bool - patchReadDir bool - }{ - { - translation: "hello", - lang: "en", - data: nil, - expected: "world", - localesPath: localesPath, - }, - { - translation: "nested.templated", - lang: "en", - data: map[string]string{ - "who": "world", - }, - expected: "hello world", - localesPath: localesPath, - }, - { - translation: "templated", - lang: "en", - data: map[string]string{ - "hello": "templated", - }, - expected: "hello templated", - localesPath: localesPath, - }, - { - translation: "expectEmptyString", - lang: "en", - data: nil, - expected: "", - localesPath: localesPath, - }, - { - translation: "expect error", - expectErr: true, - localesPath: "/tmp/somefreakingstupidnotifierlocalespath", - }, - { - translation: "expect readFile error", - expectErr: true, - patchReadFile: true, - }, - { - translation: "expect readDir error", - expectErr: true, - patchReadDir: true, - }, - } - - for _, tt := range table { - t.Run( - tt.translation, func(t *testing.T) { - if tt.patchReadFile { - readFile = func(string) ([]byte, error) { - return nil, os.ErrNotExist - } - defer func() { readFile = os.ReadFile }() - } - - if tt.patchReadDir { - readDir = func(string) ([]os.DirEntry, error) { - return nil, os.ErrNotExist - } - defer func() { readDir = os.ReadDir }() - } - - i18, err := NewI18n(tt.localesPath) - if tt.expectErr { - assert.Error(t, err) - return - } - - assert.Equal( - t, - tt.expected, - i18.Translate(tt.translation, tt.lang, tt.data), - ) - }, - ) - } -} - -func TestGetLanguagesCodes(t *testing.T) { - t.Parallel() - - wd, err := os.Getwd() - if err != nil { - t.Fatal(err) - } - i18, err := NewI18n(filepath.Join(wd, "test_locales")) - - assert.NoError(t, err) - assert.Equal(t, []string{"en"}, i18.GetLanguagesCodes()) -} diff --git a/pkg/i18n/mocks/i18_mock.go b/pkg/i18n/mocks/i18_mock.go deleted file mode 100644 index 2d90f8cd..00000000 --- a/pkg/i18n/mocks/i18_mock.go +++ /dev/null @@ -1,21 +0,0 @@ -package i18nmocks - -import "github.com/stretchr/testify/mock" - -type I18nMock struct { - mock.Mock -} - -func (m *I18nMock) Translate(key, language string, data map[string]string) string { - args := m.Called(key, language, data) - return args.String(0) -} - -func (m *I18nMock) GetLanguagesCodes() []string { - args := m.Called() - return args.Get(0).([]string) -} - -func NewI18nMock() *I18nMock { - return &I18nMock{} -} diff --git a/pkg/i18n/test_locales/en.json b/pkg/i18n/test_locales/en.json deleted file mode 100644 index 14a35c1d..00000000 --- a/pkg/i18n/test_locales/en.json +++ /dev/null @@ -1,7 +0,0 @@ -{ - "hello": "world", - "templated": "hello {{ hello }}", - "nested": { - "templated": "hello {{ who }}" - } -} diff --git a/pkg/logger/fx.go b/pkg/logger/fx.go new file mode 100644 index 00000000..bb1616cb --- /dev/null +++ b/pkg/logger/fx.go @@ -0,0 +1,21 @@ +package logger + +import ( + "go.uber.org/fx" +) + +// TODO: read from config +func NewFx() func() *Impl { + return func() *Impl { + return New( + Opts{ + Env: "development", + }, + ) + } +} + +var Module = fx.Annotate( + NewFx, + fx.As(new(Logger)), +) diff --git a/pkg/logger/impl.go b/pkg/logger/impl.go new file mode 100644 index 00000000..92dd99f0 --- /dev/null +++ b/pkg/logger/impl.go @@ -0,0 +1,105 @@ +package logger + +import ( + "context" + "io" + "log/slog" + "os" + "runtime" + "time" + + "github.com/getsentry/sentry-go" + "github.com/rs/zerolog" + "github.com/rs/zerolog/pkgerrors" + slogmulti "github.com/samber/slog-multi" + slogsentry "github.com/samber/slog-sentry/v2" + slogzerolog "github.com/samber/slog-zerolog/v2" +) + +type Impl struct { + log *slog.Logger + + service string + sentry *sentry.Client +} + +type Opts struct { + Env string + + Sentry *sentry.Client + Level slog.Level +} + +var _ Logger = (*Impl)(nil) + +func New(opts Opts) *Impl { + level := opts.Level + + var zeroLogWriter io.Writer + if opts.Env == "production" { + zeroLogWriter = os.Stderr + } else { + zeroLogWriter = zerolog.ConsoleWriter{Out: os.Stderr} + } + + zerolog.ErrorStackMarshaler = pkgerrors.MarshalStack + slogzerolog.SourceKey = "source" + slogzerolog.ErrorKeys = []string{"error", "err"} + zerolog.ErrorStackFieldName = "stack" + + zeroLogLogger := zerolog.New(zeroLogWriter) + + log := slog.New( + slogmulti.Fanout( + slogzerolog.Option{ + Level: level, + Logger: &zeroLogLogger, + AddSource: false, + }.NewZerologHandler(), + slogsentry.Option{Level: slog.LevelError, AddSource: true}.NewSentryHandler(), + ), + ) + + return &Impl{ + log: log, + sentry: opts.Sentry, + } +} + +func (c *Impl) handle(level slog.Level, input string, fields ...any) { + var pcs [1]uintptr + runtime.Callers(3, pcs[:]) + r := slog.NewRecord(time.Now(), level, input, pcs[0]) + for _, f := range fields { + r.Add(f) + } + _ = c.log.Handler().Handle(context.Background(), r) +} + +func (c *Impl) Info(input string, fields ...any) { + c.log.Info(input, fields...) +} + +func (c *Impl) Warn(input string, fields ...any) { + c.log.Warn(input, fields...) +} + +func (c *Impl) Error(input string, fields ...any) { + c.log.Error(input, fields...) +} + +func (c *Impl) Debug(input string, fields ...any) { + c.log.Debug(input, fields...) +} + +func (c *Impl) WithComponent(name string) Logger { + return &Impl{ + log: c.log.With(slog.String("component", name)), + sentry: c.sentry, + service: c.service, + } +} + +func (c *Impl) GetSlog() *slog.Logger { + return c.log +} diff --git a/pkg/logger/logger.go b/pkg/logger/logger.go new file mode 100644 index 00000000..8207d2e6 --- /dev/null +++ b/pkg/logger/logger.go @@ -0,0 +1,14 @@ +package logger + +import ( + "log/slog" +) + +type Logger interface { + Info(input string, fields ...any) + Error(input string, fields ...any) + Debug(input string, fields ...any) + Warn(input string, fields ...any) + WithComponent(name string) Logger + GetSlog() *slog.Logger +}