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"
/>
+