diff --git a/.gitignore b/.gitignore index 39a88ef..3d13486 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,4 @@ *.json bin ./tags +*.sqlite diff --git a/.task/checksum/generate b/.task/checksum/generate index b4dae54..d47083b 100644 --- a/.task/checksum/generate +++ b/.task/checksum/generate @@ -1 +1 @@ -ba89728e33b4eb652254d64099b17c4 +3c1808b7a88ab24b1cacf9a132073105 diff --git a/cmd/background/config.go b/cmd/background/config.go index f18fa84..4dcc1a5 100644 --- a/cmd/background/config.go +++ b/cmd/background/config.go @@ -8,10 +8,20 @@ import ( "git.loyso.art/frx/kurious/internal/common/config" ) +type dbEngine string + +const ( + DBEngineUnknown dbEngine = "" + DBEngineYDB dbEngine = "ydb" + DBEngineSqlite dbEngine = "sqlite" +) + type Config struct { - Log config.Log `json:"log"` - YDB config.YDB `json:"ydb"` - SyncSravniCron string `json:"sync_sravni_cron"` + Log config.Log `json:"log"` + YDB config.YDB `json:"ydb"` + Sqlite config.Sqlite `json:"sqlite"` + DBEngine dbEngine `json:"db_engine"` + SyncSravniCron string `json:"sync_sravni_cron"` DebugHTTP bool `json:"debug_http"` } @@ -37,5 +47,7 @@ func defaultConfig() Config { Level: config.LogLevelInfo, Format: config.LogFormatText, }, + // TODO: change to sqlite once it proven to be working + DBEngine: DBEngineYDB, } } diff --git a/cmd/background/main.go b/cmd/background/main.go index e3ea585..f9cc98e 100644 --- a/cmd/background/main.go +++ b/cmd/background/main.go @@ -69,9 +69,19 @@ func app(ctx context.Context) error { mapper := adapters.NewMemoryMapper(courseThematcisMapped, learningTypeMapped) + var dbEngine service.RepositoryEngine + switch cfg.DBEngine { + case DBEngineSqlite: + dbEngine = service.RepositoryEngineSqlite + case DBEngineYDB: + dbEngine = service.RepositoryEngineYDB + } + app, err := service.NewApplication(ctx, service.ApplicationConfig{ LogConfig: cfg.Log, YDB: cfg.YDB, + Sqlite: cfg.Sqlite, + Engine: dbEngine, }, mapper) if err != nil { return fmt.Errorf("making new application: %w", err) diff --git a/cmd/dev/sravnicli/products.go b/cmd/dev/sravnicli/products.go index 8d2d4e0..ebd5db7 100644 --- a/cmd/dev/sravnicli/products.go +++ b/cmd/dev/sravnicli/products.go @@ -136,11 +136,14 @@ func (a *listProductsAction) parse(args []string, options map[string]string) err func (a *listProductsAction) handle() error { params := sravni.ListEducationProductsParams{ - LearningType: a.params.learningType, - CoursesThematics: []string{a.params.courseThematic}, - Limit: a.params.limit, - Offset: a.params.offset, + LearningType: a.params.learningType, + Limit: a.params.limit, + Offset: a.params.offset, } + if a.params.courseThematic != "" { + params.CoursesThematics = append(params.CoursesThematics, a.params.courseThematic) + } + result, err := a.client.ListEducationalProducts(a.ctx, params) if err != nil { return fmt.Errorf("listing education products: %w", err) diff --git a/go.mod b/go.mod index f324ca9..6693037 100644 --- a/go.mod +++ b/go.mod @@ -6,29 +6,45 @@ require ( github.com/a-h/templ v0.2.513 github.com/go-resty/resty/v2 v2.10.0 github.com/gorilla/mux v1.8.1 + github.com/jmoiron/sqlx v1.3.5 github.com/robfig/cron/v3 v3.0.0 + github.com/stretchr/testify v1.9.0 github.com/teris-io/cli v1.0.1 github.com/ydb-platform/ydb-go-sdk/v3 v3.54.2 github.com/ydb-platform/ydb-go-yc v0.12.1 golang.org/x/net v0.18.0 golang.org/x/sync v0.5.0 golang.org/x/time v0.5.0 + modernc.org/sqlite v1.29.3 ) require ( + github.com/davecgh/go-spew v1.1.1 // indirect + github.com/dustin/go-humanize v1.0.1 // indirect github.com/golang-jwt/jwt/v4 v4.5.0 // indirect github.com/golang/protobuf v1.5.3 // indirect github.com/google/uuid v1.4.0 // indirect + github.com/hashicorp/golang-lru/v2 v2.0.7 // indirect github.com/jonboulle/clockwork v0.4.0 // indirect - github.com/stretchr/testify v1.8.4 // indirect + github.com/mattn/go-isatty v0.0.20 // indirect + github.com/ncruces/go-strftime v0.1.9 // indirect + github.com/pmezard/go-difflib v1.0.0 // indirect + github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect github.com/yandex-cloud/go-genproto v0.0.0-20231120081503-a21e9fe75162 // indirect github.com/ydb-platform/ydb-go-genproto v0.0.0-20231012155159-f85a672542fd // indirect github.com/ydb-platform/ydb-go-yc-metadata v0.6.1 // indirect - golang.org/x/sys v0.14.0 // indirect + golang.org/x/sys v0.16.0 // indirect golang.org/x/text v0.14.0 // indirect google.golang.org/genproto v0.0.0-20231120223509-83a465c0220f // indirect google.golang.org/genproto/googleapis/api v0.0.0-20231120223509-83a465c0220f // indirect google.golang.org/genproto/googleapis/rpc v0.0.0-20231120223509-83a465c0220f // indirect google.golang.org/grpc v1.59.0 // indirect google.golang.org/protobuf v1.31.0 // indirect + gopkg.in/yaml.v3 v3.0.1 // indirect + modernc.org/gc/v3 v3.0.0-20240107210532-573471604cb6 // indirect + modernc.org/libc v1.41.0 // indirect + modernc.org/mathutil v1.6.0 // indirect + modernc.org/memory v1.7.2 // indirect + modernc.org/strutil v1.2.0 // indirect + modernc.org/token v1.1.0 // indirect ) diff --git a/go.sum b/go.sum index db61a11..dcd8cff 100644 --- a/go.sum +++ b/go.sum @@ -556,6 +556,8 @@ 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/docopt/docopt-go v0.0.0-20180111231733-ee0de3bc6815/go.mod h1:WwZ+bS3ebgob9U8Nd0kOddGdZWjyMGR8Wziv+TBNwSE= github.com/dustin/go-humanize v1.0.0/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk= +github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY= +github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto= github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98= @@ -588,6 +590,8 @@ github.com/go-pdf/fpdf v0.5.0/go.mod h1:HzcnA+A23uwogo0tp9yU+l3V+KXhiESpt1PMayhO github.com/go-pdf/fpdf v0.6.0/go.mod h1:HzcnA+A23uwogo0tp9yU+l3V+KXhiESpt1PMayhOh5M= github.com/go-resty/resty/v2 v2.10.0 h1:Qla4W/+TMmv0fOeeRqzEpXPLfTUnR5HZ1+lGs+CkiCo= github.com/go-resty/resty/v2 v2.10.0/go.mod h1:iiP/OpA0CkcL3IGt1O0+/SIItFUbkkyw5BGXiVdTu+A= +github.com/go-sql-driver/mysql v1.6.0 h1:BCTh4TKNUYmOmMUcQ3IipzF5prigylS7XXjEkfCHuOE= +github.com/go-sql-driver/mysql v1.6.0/go.mod h1:DCzpHaOWr8IXmIStZouvnhqoel9Qv2LBy8hT2VhHyBg= github.com/goccy/go-json v0.9.11/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I= github.com/golang-jwt/jwt/v4 v4.4.1/go.mod h1:m21LjoU+eqJr34lmDMbreY2eSTRJ1cv77w39/MY0Ch0= github.com/golang-jwt/jwt/v4 v4.4.3/go.mod h1:m21LjoU+eqJr34lmDMbreY2eSTRJ1cv77w39/MY0Ch0= @@ -671,6 +675,8 @@ github.com/google/pprof v0.0.0-20210226084205-cbba55b83ad5/go.mod h1:kpwsk12EmLe github.com/google/pprof v0.0.0-20210601050228-01bbb1931b22/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= github.com/google/pprof v0.0.0-20210609004039-a478d1d731e9/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= github.com/google/pprof v0.0.0-20210720184732-4bb14d4b1be1/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= +github.com/google/pprof v0.0.0-20221118152302-e6195bd50e26 h1:Xim43kblpZXfIBQsbuBVKCudVG457BR2GZFIz3uw3hQ= +github.com/google/pprof v0.0.0-20221118152302-e6195bd50e26/go.mod h1:dDKJzRmX4S37WGHujM7tX//fmj1uioxKzKxz3lo4HJo= github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI= github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= @@ -700,9 +706,13 @@ github.com/grpc-ecosystem/grpc-gateway/v2 v2.7.0/go.mod h1:hgWBS7lorOAVIJEQMi4Zs github.com/grpc-ecosystem/grpc-gateway/v2 v2.11.3/go.mod h1:o//XUCC/F+yRGJoPO/VU0GSB0f8Nhgmxx0VIRUvaC0w= github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= +github.com/hashicorp/golang-lru/v2 v2.0.7 h1:a+bsQ5rvGLjzHuww6tVxozPZFVghXaHOwFs4luLUK2k= +github.com/hashicorp/golang-lru/v2 v2.0.7/go.mod h1:QeFd9opnmA6QUJc5vARoKUSoFhyfM2/ZepoAG6RGpeM= github.com/iancoleman/strcase v0.2.0/go.mod h1:iwCmte+B7n89clKwxIoIXy/HfoL7AsD47ZCWhYzw7ho= github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= github.com/ianlancetaylor/demangle v0.0.0-20200824232613-28f6c0f3b639/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= +github.com/jmoiron/sqlx v1.3.5 h1:vFFPA71p1o5gAeqtEAwLU4dnX2napprKtHr7PYIcN3g= +github.com/jmoiron/sqlx v1.3.5/go.mod h1:nRVWtLre0KfCLJvgxzCsLVMogSvQ1zNJtpYr2Ccp0mQ= github.com/jonboulle/clockwork v0.3.0/go.mod h1:Pkfl5aHPm1nk2H9h0bjmnJD/BcgbGXUBGnn1kMkgxc8= github.com/jonboulle/clockwork v0.4.0 h1:p4Cf1aMWXnXAUh8lVfewRBx1zaTSYKrKMF2g3ST4RZ4= github.com/jonboulle/clockwork v0.4.0/go.mod h1:xgRqUGwRcjKCO1vbZUEtSLrqKoPSsUpK7fnezOII0kc= @@ -718,18 +728,29 @@ github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa02 github.com/kr/fs v0.1.0/go.mod h1:FFnZGqtBN9Gxj7eW1uZ42v5BccTP0vu6NEaFoC2HwRg= github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= +github.com/kr/pretty v0.3.0 h1:WgNl7dwNpEZ6jJ9k1snq4pZsg7DOEN8hP9Xw0Tsjwk0= github.com/kr/pretty v0.3.0/go.mod h1:640gp4NfQd8pI5XOwp5fnNeVWj67G7CFk/SaSQn7NBk= github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= +github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= +github.com/lib/pq v1.2.0 h1:LXpIM/LZ5xGFhOpXAQUIMM1HdyqzVYM13zNdjCEEcA0= +github.com/lib/pq v1.2.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo= github.com/lyft/protoc-gen-star v0.6.0/go.mod h1:TGAoBVkt8w7MPG72TrKIu85MIdXwDuzJYeZuUPFPNwA= github.com/lyft/protoc-gen-star v0.6.1/go.mod h1:TGAoBVkt8w7MPG72TrKIu85MIdXwDuzJYeZuUPFPNwA= github.com/lyft/protoc-gen-star/v2 v2.0.1/go.mod h1:RcCdONR2ScXaYnQC5tUzxzlpA3WVYF7/opLeUgcQs/o= github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU= github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= +github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= +github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= +github.com/mattn/go-sqlite3 v1.14.6/go.mod h1:NyWgC/yNuGj7Q9rpYnZvas74GogHl5/Z4A/KQRfk6bU= github.com/mattn/go-sqlite3 v1.14.14/go.mod h1:NyWgC/yNuGj7Q9rpYnZvas74GogHl5/Z4A/KQRfk6bU= +github.com/mattn/go-sqlite3 v1.14.22 h1:2gZY6PC6kBnID23Tichd1K+Z0oS6nE/XwU+Vz/5o4kU= +github.com/mattn/go-sqlite3 v1.14.22/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y= github.com/minio/asm2plan9s v0.0.0-20200509001527-cdd76441f9d8/go.mod h1:mC1jAcsrzbxHt8iiaC+zU4b1ylILSosueou12R++wfY= github.com/minio/c2goasm v0.0.0-20190812172519-36a3d3bbc4f3/go.mod h1:RagcQ7I8IeTMnF8JTXieKnO4Z6JCsikNEzj0DwauVzE= +github.com/ncruces/go-strftime v0.1.9 h1:bY0MQC28UADQmHmaF5dgpLmImcShSi2kHU9XLdhx/f4= +github.com/ncruces/go-strftime v0.1.9/go.mod h1:Fwc5htZGVVkseilnfgOVb9mKy6w1naJmn9CehxcKcls= github.com/phpdave11/gofpdf v1.4.2/go.mod h1:zpO6xFn9yxo3YLyMvW8HcKWVdbNqgIfOOp2dXMnm1mY= github.com/phpdave11/gofpdi v1.0.12/go.mod h1:vBmVV0Do6hSBHC8uKUQ71JGW+ZGQq74llk/7bXwjDoI= github.com/phpdave11/gofpdi v1.0.13/go.mod h1:vBmVV0Do6hSBHC8uKUQ71JGW+ZGQq74llk/7bXwjDoI= @@ -746,11 +767,14 @@ github.com/prometheus/client_model v0.2.0/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6T github.com/prometheus/client_model v0.3.0/go.mod h1:LDGWKZIo7rky3hgvBe+caln+Dr3dPggB5dvjtD7w9+w= github.com/rekby/fixenv v0.3.2/go.mod h1:/b5LRc06BYJtslRtHKxsPWFT/ySpHV+rWvzTg+XWk4c= github.com/remyoudompheng/bigfft v0.0.0-20200410134404-eec4a21b6bb0/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo= +github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec h1:W09IVJc94icq4NjY3clb7Lk8O1qJ8BdBEF8z0ibU0rE= +github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo= github.com/robfig/cron/v3 v3.0.0 h1:kQ6Cb7aHOHTSzNVNEhmp8EcWKLb4CbiMW9h9VyIhO4E= github.com/robfig/cron/v3 v3.0.0/go.mod h1:eQICP3HwyT7UooqI/z+Ov+PtYAWygg1TEWWzGIFLtro= github.com/rogpeppe/fastuuid v1.2.0/go.mod h1:jVj6XXZzXRy/MSR5jhDC/2q6DgLz+nrA6LYCDYWNEvQ= github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= github.com/rogpeppe/go-internal v1.6.1/go.mod h1:xXDCJY+GAPziupqXw64V24skbSoqbTEfhy4qGm1nDQc= +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/ruudk/golang-pdf417 v0.0.0-20181029194003-1af4ab5afa58/go.mod h1:6lfFZQK844Gfx8o5WFuvpxWRwnSoipWe/p622j1v06w= github.com/ruudk/golang-pdf417 v0.0.0-20201230142125-a7e3863a1245/go.mod h1:pQAZKsJ8yyVxGRWYNEm9oFB8ieLgKFnamEyDmSA0BRk= @@ -770,8 +794,8 @@ github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/ github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= github.com/stretchr/testify v1.8.3/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= -github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk= -github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= +github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= +github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= github.com/teris-io/cli v1.0.1 h1:J6jnVHC552uqx7zT+Ux0++tIvLmJQULqxVhCid2u/Gk= github.com/teris-io/cli v1.0.1/go.mod h1:V9nVD5aZ873RU/tQXLSXO8FieVPQhQvuNohsdsKXsGw= github.com/yandex-cloud/go-genproto v0.0.0-20211115083454-9ca41db5ed9e/go.mod h1:HEUYX/p8966tMUHHT+TsS0hF/Ca/NYwqprC5WXSDMfE= @@ -876,6 +900,8 @@ golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91 golang.org/x/mod v0.7.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= golang.org/x/mod v0.9.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= +golang.org/x/mod v0.14.0 h1:dGoOF9QVLYng8IHTm7BAyWqCqSheQ5pYWGhzW00YJr0= +golang.org/x/mod v0.14.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= @@ -1060,8 +1086,8 @@ golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.13.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/sys v0.16.0 h1:xWw16ngr6ZMtmxDyKyIgsE93KNKz5HKmMa3b8ALHidU= +golang.org/x/sys v0.16.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= golang.org/x/term v0.2.0/go.mod h1:TVmDHMZPmdnySmBfhjOoOdhjzdE1h4u1VwSiw2l1Nuc= @@ -1159,6 +1185,8 @@ golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc golang.org/x/tools v0.3.0/go.mod h1:/rWhSS2+zyEVwoJf8YAX6L2f0ntZ7Kn/mGgAWcipA5k= golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU= golang.org/x/tools v0.7.0/go.mod h1:4pg6aUX35JBAogB10C9AtvVL+qowtN4pT3CGSQex14s= +golang.org/x/tools v0.17.0 h1:FvmRgNOcs3kOa+T20R1uhfP9F6HgG2mfxDv1vrx1Htc= +golang.org/x/tools v0.17.0/go.mod h1:xsh6VxdV005rRVaS6SSAf9oiAqljS7UZUacMZ8Bnsps= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= @@ -1431,6 +1459,7 @@ google.golang.org/protobuf v1.31.0 h1:g0LDEJHgrBl9N9r17Ru3sqWhkIx2NB67okBHPwC7hs google.golang.org/protobuf v1.31.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/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/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI= gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= @@ -1459,6 +1488,10 @@ modernc.org/ccgo/v3 v3.16.6/go.mod h1:tGtX0gE9Jn7hdZFeU88slbTh1UtCYKusWOoCJuvkWs modernc.org/ccgo/v3 v3.16.8/go.mod h1:zNjwkizS+fIFDrDjIAgBSCLkWbJuHF+ar3QRn+Z9aws= modernc.org/ccgo/v3 v3.16.9/go.mod h1:zNMzC9A9xeNUepy6KuZBbugn3c0Mc9TeiJO4lgvkJDo= modernc.org/ccorpus v1.11.6/go.mod h1:2gEUTrWqdpH2pXsmTM1ZkjeSrUWDpjMu2T6m29L/ErQ= +modernc.org/fileutil v1.3.0 h1:gQ5SIzK3H9kdfai/5x41oQiKValumqNTDXMvKo62HvE= +modernc.org/fileutil v1.3.0/go.mod h1:XatxS8fZi3pS8/hKG2GH/ArUogfxjpEKs3Ku3aK4JyQ= +modernc.org/gc/v3 v3.0.0-20240107210532-573471604cb6 h1:5D53IMaUuA5InSeMu9eJtlQXS2NxAhyWQvkKEgXZhHI= +modernc.org/gc/v3 v3.0.0-20240107210532-573471604cb6/go.mod h1:Qz0X07sNOR1jWYCrJMEnbW/X55x206Q7Vt4mz6/wHp4= modernc.org/httpfs v1.0.6/go.mod h1:7dosgurJGp0sPaRanU53W4xZYKh14wfzX420oZADeHM= modernc.org/libc v0.0.0-20220428101251-2d5f3daf273b/go.mod h1:p7Mg4+koNjc8jkqwcoFBJx7tXkpj00G77X7A72jXPXA= modernc.org/libc v1.16.0/go.mod h1:N4LD6DBE9cf+Dzf9buBlzVJndKr/iJHG97vGLHYnb5A= @@ -1467,19 +1500,31 @@ modernc.org/libc v1.16.17/go.mod h1:hYIV5VZczAmGZAnG15Vdngn5HSF5cSkbvfz2B7GRuVU= modernc.org/libc v1.16.19/go.mod h1:p7Mg4+koNjc8jkqwcoFBJx7tXkpj00G77X7A72jXPXA= modernc.org/libc v1.17.0/go.mod h1:XsgLldpP4aWlPlsjqKRdHPqCxCjISdHfM/yeWC5GyW0= modernc.org/libc v1.17.1/go.mod h1:FZ23b+8LjxZs7XtFMbSzL/EhPxNbfZbErxEHc7cbD9s= +modernc.org/libc v1.41.0 h1:g9YAc6BkKlgORsUWj+JwqoB1wU3o4DE3bM3yvA3k+Gk= +modernc.org/libc v1.41.0/go.mod h1:w0eszPsiXoOnoMJgrXjglgLuDy/bt5RR4y3QzUUeodY= modernc.org/mathutil v1.2.2/go.mod h1:mZW8CKdRPY1v87qxC/wUdX5O1qDzXMP5TH3wjfpga6E= modernc.org/mathutil v1.4.1/go.mod h1:mZW8CKdRPY1v87qxC/wUdX5O1qDzXMP5TH3wjfpga6E= modernc.org/mathutil v1.5.0/go.mod h1:mZW8CKdRPY1v87qxC/wUdX5O1qDzXMP5TH3wjfpga6E= +modernc.org/mathutil v1.6.0 h1:fRe9+AmYlaej+64JsEEhoWuAYBkOtQiMEU7n/XgfYi4= +modernc.org/mathutil v1.6.0/go.mod h1:Ui5Q9q1TR2gFm0AQRqQUaBWFLAhQpCwNcuhBOSedWPo= modernc.org/memory v1.1.1/go.mod h1:/0wo5ibyrQiaoUoH7f9D8dnglAmILJ5/cxZlRECf+Nw= modernc.org/memory v1.2.0/go.mod h1:/0wo5ibyrQiaoUoH7f9D8dnglAmILJ5/cxZlRECf+Nw= modernc.org/memory v1.2.1/go.mod h1:PkUhL0Mugw21sHPeskwZW4D6VscE/GQJOnIpCnW6pSU= +modernc.org/memory v1.7.2 h1:Klh90S215mmH8c9gO98QxQFsY+W451E8AnzjoE2ee1E= +modernc.org/memory v1.7.2/go.mod h1:NO4NVCQy0N7ln+T9ngWqOQfi7ley4vpwvARR+Hjw95E= modernc.org/opt v0.1.1/go.mod h1:WdSiB5evDcignE70guQKxYUl14mgWtbClRi5wmkkTX0= modernc.org/opt v0.1.3/go.mod h1:WdSiB5evDcignE70guQKxYUl14mgWtbClRi5wmkkTX0= modernc.org/sqlite v1.18.1/go.mod h1:6ho+Gow7oX5V+OiOQ6Tr4xeqbx13UZ6t+Fw9IRUG4d4= +modernc.org/sqlite v1.29.3 h1:6L71d3zXVB8oubdVSuwiurNyYRetQ3It8l1FSwylwQ0= +modernc.org/sqlite v1.29.3/go.mod h1:MjUIBKZ+tU/lqjNLbVAAMjsQPdWdA/ciwdhsT9kBwk8= modernc.org/strutil v1.1.1/go.mod h1:DE+MQQ/hjKBZS2zNInV5hhcipt5rLPWkmpbGeW5mmdw= modernc.org/strutil v1.1.3/go.mod h1:MEHNA7PdEnEwLvspRMtWTNnp2nnyvMfkimT1NKNAGbw= +modernc.org/strutil v1.2.0 h1:agBi9dp1I+eOnxXeiZawM8F4LawKv4NzGWSaLfyeNZA= +modernc.org/strutil v1.2.0/go.mod h1:/mdcBmfOibveCTBxUl5B5l6W+TTH1FXPLHZE6bTosX0= modernc.org/tcl v1.13.1/go.mod h1:XOLfOwzhkljL4itZkK6T72ckMgvj0BDsnKNdZVUOecw= modernc.org/token v1.0.0/go.mod h1:UGzOrNV1mAFSEB63lOFHIpNRUVMvYTc6yu1SMY/XTDM= +modernc.org/token v1.1.0 h1:Xl7Ap9dKaEs5kLoOQeQmPWevfnk/DM5qcLcYlA8ys6Y= +modernc.org/token v1.1.0/go.mod h1:UGzOrNV1mAFSEB63lOFHIpNRUVMvYTc6yu1SMY/XTDM= modernc.org/z v1.5.1/go.mod h1:eWFB510QWW5Th9YGZT81s+LwvaAs3Q2yr4sP0rmLkv8= rsc.io/binaryregexp v0.2.0/go.mod h1:qTv7/COck+e2FymRvadv62gMdZztPaShugOCi3I+8D8= rsc.io/pdf v0.1.1/go.mod h1:n8OzWcQ6Sp37PL01nO98y4iUCRdTGarVfzxY20ICaU4= diff --git a/internal/common/config/sqlite.go b/internal/common/config/sqlite.go new file mode 100644 index 0000000..7c265e7 --- /dev/null +++ b/internal/common/config/sqlite.go @@ -0,0 +1,8 @@ +package config + +import "time" + +type Sqlite struct { + DSN string `json:"dsn"` + ShutdownTimeout time.Duration `json:"shutdown_timeout"` +} diff --git a/internal/kurious/adapters/memory_mapper.go b/internal/kurious/adapters/memory_mapper.go index 32dfb40..42e5936 100644 --- a/internal/kurious/adapters/memory_mapper.go +++ b/internal/kurious/adapters/memory_mapper.go @@ -1,21 +1,73 @@ package adapters +import ( + "context" + "fmt" + + "git.loyso.art/frx/kurious/internal/kurious/domain" +) + type inMemoryMapper struct { courseThematicsByID map[string]string learningTypeByID map[string]string + + courseThematicsCountByID map[string]int + learningTypeCountByID map[string]int + totalCount int } -func NewMemoryMapper(courseThematics, learningType map[string]string) inMemoryMapper { - return inMemoryMapper{ +func NewMemoryMapper(courseThematics, learningType map[string]string) *inMemoryMapper { + return &inMemoryMapper{ courseThematicsByID: courseThematics, learningTypeByID: learningType, } } -func (m inMemoryMapper) CourseThematicNameByID(id string) string { +func (m *inMemoryMapper) CollectCounts(ctx context.Context, cr domain.CourseRepository) error { + const batchSize = 1000 + + m.courseThematicsCountByID = map[string]int{} + m.learningTypeCountByID = map[string]int{} + + var nextPageToken string + for { + result, err := cr.List(ctx, domain.ListCoursesParams{ + LearningType: "", + CourseThematic: "", + NextPageToken: nextPageToken, + Limit: batchSize, + }) + if err != nil { + return fmt.Errorf("listing courses: %w", err) + } + m.totalCount += len(result.Courses) + for _, course := range result.Courses { + m.courseThematicsCountByID[course.ThematicID]++ + m.learningTypeCountByID[course.LearningTypeID]++ + } + if len(result.Courses) < batchSize { + break + } + nextPageToken = result.NextPageToken + } + + return nil +} + +func (m *inMemoryMapper) GetCounts(byCourseThematic, byLearningType string) int { + if byCourseThematic != "" { + return m.courseThematicsCountByID[byCourseThematic] + } else if byLearningType != "" { + return m.learningTypeCountByID[byLearningType] + } else { + return m.totalCount + } +} + +func (m *inMemoryMapper) CourseThematicNameByID(id string) string { return m.courseThematicsByID[id] } -func (m inMemoryMapper) LearningTypeNameByID(id string) string { +func (m *inMemoryMapper) LearningTypeNameByID(id string) string { return m.learningTypeByID[id] } diff --git a/internal/kurious/adapters/sqlite_course_repository.go b/internal/kurious/adapters/sqlite_course_repository.go new file mode 100644 index 0000000..e41de1b --- /dev/null +++ b/internal/kurious/adapters/sqlite_course_repository.go @@ -0,0 +1,377 @@ +package adapters + +import ( + "context" + "database/sql" + "errors" + "fmt" + "log/slog" + "strings" + "time" + + "git.loyso.art/frx/kurious/internal/common/config" + "git.loyso.art/frx/kurious/internal/common/nullable" + "git.loyso.art/frx/kurious/internal/kurious/domain" + "git.loyso.art/frx/kurious/migrations/sqlite" + "git.loyso.art/frx/kurious/pkg/xdefault" + + "github.com/jmoiron/sqlx" + _ "modernc.org/sqlite" +) + +type sqliteConnection struct { + db *sqlx.DB + shutdownTimeout time.Duration + log *slog.Logger +} + +func NewSqliteConnection(ctx context.Context, cfg config.Sqlite, log *slog.Logger) (*sqliteConnection, error) { + conn, err := sqlx.Open("sqlite", cfg.DSN) + if err != nil { + return nil, fmt.Errorf("openning db connection: %w", err) + } + + err = sqlite.RunMigrations(ctx, conn.DB, log) + if err != nil { + return nil, fmt.Errorf("running migrations: %w", err) + } + + return &sqliteConnection{ + db: conn, + log: log, + shutdownTimeout: xdefault.WithFallback(cfg.ShutdownTimeout, defaultShutdownTimeout), + }, nil +} + +func (c *sqliteConnection) Close() error { + _, cancel := context.WithTimeout(context.Background(), c.shutdownTimeout) + defer cancel() + + return c.db.Close() +} + +func (c *sqliteConnection) CourseRepository() *sqliteCourseRepository { + return &sqliteCourseRepository{ + db: c.db, + log: c.log.With(slog.String("repository", "course")), + } +} + +type sqliteCourseRepository struct { + db *sqlx.DB + log *slog.Logger +} + +func (r *sqliteCourseRepository) List( + ctx context.Context, + params domain.ListCoursesParams, +) (result domain.ListCoursesResult, err error) { + const queryTemplate = `SELECT %s from courses WHERE 1=1` + + query := fmt.Sprintf(queryTemplate, coursesFieldsStr) + args := make([]any, 0, 1) + if params.LearningType != "" { + args = append(args, params.LearningType) + query += " AND learning_type = ?" + } + if params.CourseThematic != "" { + args = append(args, params.CourseThematic) + query += " AND course_thematic = ?" + } + if params.OrganizationID != "" { + args = append(args, params.OrganizationID) + query += " AND organization_id = ?" + } + if params.NextPageToken != "" { + args = append(args, params.NextPageToken) + query += " AND id > ?" + } + + query += " ORDER BY id ASC" + + if params.Limit > 0 { + query += " LIMIT ?" + args = append(args, params.Limit) + } + + scanF := func(s rowsScanner) (err error) { + var cdb sqliteCourseDB + err = s.StructScan(&cdb) + if err != nil { + return err + } + + result.Courses = append(result.Courses, cdb.AsDomain()) + return nil + } + err = scanRows(ctx, r.db, scanF, query, args...) + if err != nil { + return result, err + } + + if params.Limit > 0 && len(result.Courses) == params.Limit { + lastIDx := len(result.Courses) - 1 + result.NextPageToken = result.Courses[lastIDx].ID + } + + return result, nil +} + +func (r *sqliteCourseRepository) ListLearningTypes( + ctx context.Context, +) (result domain.ListLearningTypeResult, err error) { + const query = "SELECT DISTINCT learning_type FROM courses" + + err = r.db.SelectContext(ctx, &result.LearningTypeIDs, query) + if err != nil { + return result, fmt.Errorf("executing query: %w", err) + } + + return result, nil +} + +func (r *sqliteCourseRepository) ListCourseThematics( + ctx context.Context, + params domain.ListCourseThematicsParams, +) (result domain.ListCourseThematicsResult, err error) { + const queryTemplate = "SELECT DISTINCT course_thematic FROM courses WHERE 1=1" + + query := queryTemplate + args := make([]any, 0, 1) + if params.LearningTypeID != "" { + args = append(args, params.LearningTypeID) + query += " AND learning_type = ?" + } + + err = r.db.SelectContext(ctx, &result.CourseThematicIDs, query, args...) + if err != nil { + return result, fmt.Errorf("executing query: %w", err) + } + + return result, nil +} + +func (r *sqliteCourseRepository) Get( + ctx context.Context, + id string, +) (course domain.Course, err error) { + const queryTemplate = `SELECT %s FROM courses WHERE id = ?` + + query := fmt.Sprintf(queryTemplate, coursesFieldsStr) + var courseDB sqliteCourseDB + err = r.db.GetContext(ctx, &courseDB, query, id) + if err != nil { + return course, fmt.Errorf("executing query: %w", err) + } + + return courseDB.AsDomain(), nil +} + +func (r *sqliteCourseRepository) GetByExternalID( + ctx context.Context, id string, +) (course domain.Course, err error) { + return course, errors.New("not implemented") +} + +func (r *sqliteCourseRepository) CreateBatch(ctx context.Context, params ...domain.CreateCourseParams) error { + tx, err := r.db.BeginTxx(ctx, &sql.TxOptions{Isolation: sql.LevelDefault}) + if err != nil { + return fmt.Errorf("beginning tx: %w", err) + } + defer func() { + var errTx error + if err != nil { + errTx = tx.Rollback() + } else { + errTx = tx.Commit() + } + + err = errors.Join(err, errTx) + }() + + const queryTempalate = `INSERT INTO courses` + + ` (%s) VALUES (%s)` + + placeholders := strings.TrimSuffix(strings.Repeat("?,", len(coursesFields)), ",") + query := fmt.Sprintf(queryTempalate, coursesFieldsStr, placeholders) + + stmt, err := tx.PrepareContext(ctx, query) + if err != nil { + return fmt.Errorf("preparing statement: %w", err) + } + + for _, param := range params { + _, err := stmt.ExecContext(ctx, createCourseParamsAsValues(param)...) + if err != nil { + return fmt.Errorf("executing statement query: %w", err) + } + } + + return nil +} + +func (r *sqliteCourseRepository) Create(ctx context.Context, params domain.CreateCourseParams) (domain.Course, error) { + err := r.CreateBatch(ctx, params) + return domain.Course{}, err +} + +func (r *sqliteCourseRepository) UpdateCourseDescription(ctx context.Context, id, description string) error { + return errors.New("unimplemented") +} + +func (r *sqliteCourseRepository) Delete(ctx context.Context, id string) error { + return errors.New("unimplemented") +} + +type rowsScanner interface { + sqlx.ColScanner + + StructScan(dest any) error +} + +func scanRows(ctx context.Context, db *sqlx.DB, f func(rowsScanner) error, query string, args ...any) error { + rows, err := db.QueryxContext(ctx, query, args...) + if err != nil { + return fmt.Errorf("querying rows: %w", err) + } + defer func() { + err = errors.Join(err, rows.Close()) + }() + + for rows.Next() { + err = f(rows) + if err != nil { + return fmt.Errorf("scanning row: %w", err) + } + } + if err = rows.Err(); err != nil { + return fmt.Errorf("checking rows for errors: %w", err) + } + + return nil +} + +func createCourseParamsAsValues(params domain.CreateCourseParams) []any { + now := time.Now() + + return []any{ + params.ID, + nullableValueAsString(params.ExternalID), + mapSourceTypeFromDomain(params.SourceType), + nullableValueAsString(params.SourceName), + params.CourseThematic, + params.LearningType, + params.OrganizationID, + params.OriginLink, + params.ImageLink, + params.Name, + params.Description, + params.FullPrice, + params.Discount, + params.Duration.Truncate(time.Second).Milliseconds() / 1000, + params.StartsAt, + now, + now, + sql.NullTime{}, + } +} + +type sqliteCourseDB struct { + ID string `db:"id"` + ExternalID sql.NullString `db:"external_id"` + SourceType string `db:"source_type"` + SourceName sql.NullString `db:"source_name"` + ThematicID string `db:"course_thematic"` + LearningTypeID string `db:"learning_type"` + OrganizationID string `db:"organization_id"` + OriginLink string `db:"origin_link"` + ImageLink string `db:"image_link"` + Name string `db:"name"` + Description string `db:"description"` + FullPrice float64 `db:"full_price"` + Discount float64 `db:"discount"` + Duration int64 `db:"duration"` + CreatedAt time.Time `db:"created_at"` + StartsAt sql.NullTime `db:"starts_at"` + UpdatedAt time.Time `db:"updated_at"` + DeletedAt sql.NullTime `db:"deleted_at"` +} + +func nullStringAsDomain(s sql.NullString) nullable.Value[string] { + if s.Valid { + return nullable.NewValue(s.String) + } + + return nullable.Value[string]{} +} + +func nullTimeAsDomain(s sql.NullTime) nullable.Value[time.Time] { + if s.Valid { + return nullable.NewValue(s.Time) + } + + return nullable.Value[time.Time]{} +} + +func nullableValueAsString(v nullable.Value[string]) sql.NullString { + return sql.NullString{ + Valid: v.Valid(), + String: v.Value(), + } +} + +func nullableValueAsTime(v nullable.Value[time.Time]) sql.NullTime { + return sql.NullTime{ + Valid: v.Valid(), + Time: v.Value(), + } +} + +func (c sqliteCourseDB) AsDomain() domain.Course { + return domain.Course{ + ID: c.ID, + OrganizationID: c.OrganizationID, + OriginLink: c.OriginLink, + ImageLink: c.ImageLink, + Name: c.Name, + Description: c.Description, + FullPrice: c.FullPrice, + Discount: c.Discount, + ThematicID: c.ThematicID, + LearningTypeID: c.LearningTypeID, + Duration: time.Second * time.Duration(c.Duration), + StartsAt: c.StartsAt.Time, + CreatedAt: c.CreatedAt, + UpdatedAt: c.UpdatedAt, + ExternalID: nullStringAsDomain(c.ExternalID), + SourceType: mapSourceTypeToDomain(c.SourceType), + SourceName: nullStringAsDomain(c.SourceName), + DeletedAt: nullTimeAsDomain(c.DeletedAt), + } +} + +func (c *sqliteCourseDB) FromDomain(d domain.Course) { + *c = sqliteCourseDB{ + ID: d.ID, + OrganizationID: d.OrganizationID, + OriginLink: d.OriginLink, + ImageLink: d.ImageLink, + Name: d.Name, + Description: d.Description, + FullPrice: d.FullPrice, + Discount: d.Discount, + ThematicID: d.ThematicID, + LearningTypeID: d.LearningTypeID, + SourceType: mapSourceTypeFromDomain(d.SourceType), + Duration: d.Duration.Truncate(time.Second).Milliseconds() / 1000, + CreatedAt: d.CreatedAt, + UpdatedAt: d.UpdatedAt, + ExternalID: nullableValueAsString(d.ExternalID), + SourceName: nullableValueAsString(d.SourceName), + DeletedAt: nullableValueAsTime(d.DeletedAt), + StartsAt: sql.NullTime{ + Time: d.StartsAt, + Valid: true, + }, + } +} diff --git a/internal/kurious/adapters/sqlite_course_repository_test.go b/internal/kurious/adapters/sqlite_course_repository_test.go new file mode 100644 index 0000000..8a0b84a --- /dev/null +++ b/internal/kurious/adapters/sqlite_course_repository_test.go @@ -0,0 +1,119 @@ +package adapters + +import ( + "context" + "log/slog" + "os" + "testing" + "time" + + "git.loyso.art/frx/kurious/internal/common/config" + "git.loyso.art/frx/kurious/internal/common/nullable" + "git.loyso.art/frx/kurious/internal/kurious/domain" + "git.loyso.art/frx/kurious/migrations/sqlite" + "github.com/stretchr/testify/suite" +) + +func TestSqliteCourseRepository(t *testing.T) { + suite.Run(t, new(sqliteCourseRepositorySuite)) +} + +type sqliteCourseRepositorySuite struct { + suite.Suite + + // TODO: make baseTestSuite that provides this kind of things + ctx context.Context + cancel context.CancelFunc + log *slog.Logger + + connection *sqliteConnection +} + +func (s *sqliteCourseRepositorySuite) SetupSuite() { + s.ctx, s.cancel = context.WithCancel(context.Background()) + s.log = slog.New(slog.NewTextHandler(os.Stdout, &slog.HandlerOptions{ + AddSource: false, + Level: slog.LevelDebug, + })) + + connection, err := NewSqliteConnection(s.ctx, config.Sqlite{ + DSN: ":memory:", + ShutdownTimeout: time.Second * 3, + }, s.log) + s.Require().NoError(err) + + s.connection = connection + db := s.connection.db + + err = sqlite.RunMigrations(s.ctx, db.DB, s.log.With(slog.String("component", "migrator"))) + s.Require().NoError(err) +} + +func (s *sqliteCourseRepositorySuite) TearDownSuite() { + s.cancel() + err := s.connection.Close() + s.Require().NoError(err) +} + +func (s *sqliteCourseRepositorySuite) TearDownTest() { + db := s.connection.db + _, err := db.ExecContext(s.ctx, "DELETE FROM courses") + s.Require().NoError(err, "cleaning up database") +} + +func (s *sqliteCourseRepositorySuite) TestCreateCourse() { + expcourse := domain.Course{ + ID: "test-id", + ExternalID: nullable.NewValue("ext-id"), + Name: "test-name", + SourceType: domain.SourceTypeParsed, + SourceName: nullable.NewValue("test-source"), + ThematicID: "test-thematic", + LearningTypeID: "test-learning", + OrganizationID: "test-org-id", + OriginLink: "test-link", + ImageLink: "test-image-link", + Description: "description", + FullPrice: 123, + Discount: 321, + Duration: time.Second * 360, + StartsAt: time.Date(2020, 10, 01, 11, 22, 33, 0, time.UTC), + Thematic: "", + LearningType: "", + CreatedAt: time.Time{}, + UpdatedAt: time.Time{}, + DeletedAt: nullable.Value[time.Time]{}, + } + + cr := s.connection.CourseRepository() + _, err := cr.Create(s.ctx, domain.CreateCourseParams{ + ID: "test-id", + ExternalID: nullable.NewValue("ext-id"), + Name: "test-name", + SourceType: domain.SourceTypeParsed, + SourceName: nullable.NewValue("test-source"), + CourseThematic: "test-thematic", + LearningType: "test-learning", + OrganizationID: "test-org-id", + OriginLink: "test-link", + ImageLink: "test-image-link", + Description: "description", + FullPrice: 123, + Discount: 321, + Duration: time.Second * 360, + StartsAt: time.Date(2020, 10, 01, 11, 22, 33, 0, time.UTC), + }) + s.Require().NoError(err) + + gotCourse, err := cr.Get(s.ctx, expcourse.ID) + s.Require().NoError(err) + + s.Require().NotEmpty(gotCourse.CreatedAt) + s.Require().NotEmpty(gotCourse.UpdatedAt) + s.Require().Empty(gotCourse.DeletedAt) + + expcourse.CreatedAt = gotCourse.CreatedAt + expcourse.UpdatedAt = gotCourse.UpdatedAt + + s.Require().Equal(expcourse, gotCourse) +} diff --git a/internal/kurious/app/query/listcourses.go b/internal/kurious/app/query/listcourses.go index 05303ae..3c8efe9 100644 --- a/internal/kurious/app/query/listcourses.go +++ b/internal/kurious/app/query/listcourses.go @@ -41,6 +41,7 @@ func NewListCourseHandler( } func (h listCourseHandler) Handle(ctx context.Context, query ListCourse) (out domain.ListCoursesResult, err error) { + out.AvailableCoursesOfSub = map[string]int{} out.NextPageToken = query.NextPageToken drainFull := query.Limit == 0 if !drainFull { @@ -76,5 +77,11 @@ func (h listCourseHandler) Handle(ctx context.Context, query ListCourse) (out do break } + for _, course := range out.Courses { + if _, ok := out.AvailableCoursesOfSub[course.ThematicID]; !ok { + out.AvailableCoursesOfSub[course.ThematicID] = h.mapper.GetCounts(course.ThematicID, course.LearningTypeID) + } + } + return out, nil } diff --git a/internal/kurious/domain/mapper.go b/internal/kurious/domain/mapper.go index b0e96d3..f637925 100644 --- a/internal/kurious/domain/mapper.go +++ b/internal/kurious/domain/mapper.go @@ -1,6 +1,11 @@ package domain +import "context" + type CourseMapper interface { CourseThematicNameByID(string) string LearningTypeNameByID(string) string + + CollectCounts(context.Context, CourseRepository) error + GetCounts(byCourseThematic, byLearningType string) int } diff --git a/internal/kurious/domain/repository.go b/internal/kurious/domain/repository.go index bdd72df..fceceb3 100644 --- a/internal/kurious/domain/repository.go +++ b/internal/kurious/domain/repository.go @@ -35,8 +35,9 @@ type CreateCourseParams struct { } type ListCoursesResult struct { - Courses []Course - NextPageToken string + Courses []Course + AvailableCoursesOfSub map[string]int + NextPageToken string } type ListLearningTypeResult struct { diff --git a/internal/kurious/ports/http/bootstrap/core.templ b/internal/kurious/ports/http/bootstrap/core.templ index 970418f..0ab4b4a 100644 --- a/internal/kurious/ports/http/bootstrap/core.templ +++ b/internal/kurious/ports/http/bootstrap/core.templ @@ -11,6 +11,10 @@ templ head(title string) { integrity="sha384-T3c6CoIi6uLrA9TneNEoa7RxnatzjcDSCmG1MXxSR1GAsXEV/Dwwykc2MPK8M2HN" crossorigin="anonymous" /> +