add sqlite support
This commit is contained in:
1
.gitignore
vendored
1
.gitignore
vendored
@ -1,3 +1,4 @@
|
|||||||
*.json
|
*.json
|
||||||
bin
|
bin
|
||||||
./tags
|
./tags
|
||||||
|
*.sqlite
|
||||||
|
|||||||
@ -1 +1 @@
|
|||||||
ba89728e33b4eb652254d64099b17c4
|
3c1808b7a88ab24b1cacf9a132073105
|
||||||
|
|||||||
@ -8,10 +8,20 @@ import (
|
|||||||
"git.loyso.art/frx/kurious/internal/common/config"
|
"git.loyso.art/frx/kurious/internal/common/config"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
type dbEngine string
|
||||||
|
|
||||||
|
const (
|
||||||
|
DBEngineUnknown dbEngine = ""
|
||||||
|
DBEngineYDB dbEngine = "ydb"
|
||||||
|
DBEngineSqlite dbEngine = "sqlite"
|
||||||
|
)
|
||||||
|
|
||||||
type Config struct {
|
type Config struct {
|
||||||
Log config.Log `json:"log"`
|
Log config.Log `json:"log"`
|
||||||
YDB config.YDB `json:"ydb"`
|
YDB config.YDB `json:"ydb"`
|
||||||
SyncSravniCron string `json:"sync_sravni_cron"`
|
Sqlite config.Sqlite `json:"sqlite"`
|
||||||
|
DBEngine dbEngine `json:"db_engine"`
|
||||||
|
SyncSravniCron string `json:"sync_sravni_cron"`
|
||||||
|
|
||||||
DebugHTTP bool `json:"debug_http"`
|
DebugHTTP bool `json:"debug_http"`
|
||||||
}
|
}
|
||||||
@ -37,5 +47,7 @@ func defaultConfig() Config {
|
|||||||
Level: config.LogLevelInfo,
|
Level: config.LogLevelInfo,
|
||||||
Format: config.LogFormatText,
|
Format: config.LogFormatText,
|
||||||
},
|
},
|
||||||
|
// TODO: change to sqlite once it proven to be working
|
||||||
|
DBEngine: DBEngineYDB,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -69,9 +69,19 @@ func app(ctx context.Context) error {
|
|||||||
|
|
||||||
mapper := adapters.NewMemoryMapper(courseThematcisMapped, learningTypeMapped)
|
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{
|
app, err := service.NewApplication(ctx, service.ApplicationConfig{
|
||||||
LogConfig: cfg.Log,
|
LogConfig: cfg.Log,
|
||||||
YDB: cfg.YDB,
|
YDB: cfg.YDB,
|
||||||
|
Sqlite: cfg.Sqlite,
|
||||||
|
Engine: dbEngine,
|
||||||
}, mapper)
|
}, mapper)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("making new application: %w", err)
|
return fmt.Errorf("making new application: %w", err)
|
||||||
|
|||||||
@ -136,11 +136,14 @@ func (a *listProductsAction) parse(args []string, options map[string]string) err
|
|||||||
|
|
||||||
func (a *listProductsAction) handle() error {
|
func (a *listProductsAction) handle() error {
|
||||||
params := sravni.ListEducationProductsParams{
|
params := sravni.ListEducationProductsParams{
|
||||||
LearningType: a.params.learningType,
|
LearningType: a.params.learningType,
|
||||||
CoursesThematics: []string{a.params.courseThematic},
|
Limit: a.params.limit,
|
||||||
Limit: a.params.limit,
|
Offset: a.params.offset,
|
||||||
Offset: a.params.offset,
|
|
||||||
}
|
}
|
||||||
|
if a.params.courseThematic != "" {
|
||||||
|
params.CoursesThematics = append(params.CoursesThematics, a.params.courseThematic)
|
||||||
|
}
|
||||||
|
|
||||||
result, err := a.client.ListEducationalProducts(a.ctx, params)
|
result, err := a.client.ListEducationalProducts(a.ctx, params)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("listing education products: %w", err)
|
return fmt.Errorf("listing education products: %w", err)
|
||||||
|
|||||||
20
go.mod
20
go.mod
@ -6,29 +6,45 @@ require (
|
|||||||
github.com/a-h/templ v0.2.513
|
github.com/a-h/templ v0.2.513
|
||||||
github.com/go-resty/resty/v2 v2.10.0
|
github.com/go-resty/resty/v2 v2.10.0
|
||||||
github.com/gorilla/mux v1.8.1
|
github.com/gorilla/mux v1.8.1
|
||||||
|
github.com/jmoiron/sqlx v1.3.5
|
||||||
github.com/robfig/cron/v3 v3.0.0
|
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/teris-io/cli v1.0.1
|
||||||
github.com/ydb-platform/ydb-go-sdk/v3 v3.54.2
|
github.com/ydb-platform/ydb-go-sdk/v3 v3.54.2
|
||||||
github.com/ydb-platform/ydb-go-yc v0.12.1
|
github.com/ydb-platform/ydb-go-yc v0.12.1
|
||||||
golang.org/x/net v0.18.0
|
golang.org/x/net v0.18.0
|
||||||
golang.org/x/sync v0.5.0
|
golang.org/x/sync v0.5.0
|
||||||
golang.org/x/time v0.5.0
|
golang.org/x/time v0.5.0
|
||||||
|
modernc.org/sqlite v1.29.3
|
||||||
)
|
)
|
||||||
|
|
||||||
require (
|
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-jwt/jwt/v4 v4.5.0 // indirect
|
||||||
github.com/golang/protobuf v1.5.3 // indirect
|
github.com/golang/protobuf v1.5.3 // indirect
|
||||||
github.com/google/uuid v1.4.0 // 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/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/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-genproto v0.0.0-20231012155159-f85a672542fd // indirect
|
||||||
github.com/ydb-platform/ydb-go-yc-metadata v0.6.1 // 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
|
golang.org/x/text v0.14.0 // indirect
|
||||||
google.golang.org/genproto v0.0.0-20231120223509-83a465c0220f // 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/api v0.0.0-20231120223509-83a465c0220f // indirect
|
||||||
google.golang.org/genproto/googleapis/rpc 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/grpc v1.59.0 // indirect
|
||||||
google.golang.org/protobuf v1.31.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
|
||||||
)
|
)
|
||||||
|
|||||||
53
go.sum
53
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/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/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.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.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.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
|
||||||
github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98=
|
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-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 h1:Qla4W/+TMmv0fOeeRqzEpXPLfTUnR5HZ1+lGs+CkiCo=
|
||||||
github.com/go-resty/resty/v2 v2.10.0/go.mod h1:iiP/OpA0CkcL3IGt1O0+/SIItFUbkkyw5BGXiVdTu+A=
|
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/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.1/go.mod h1:m21LjoU+eqJr34lmDMbreY2eSTRJ1cv77w39/MY0Ch0=
|
||||||
github.com/golang-jwt/jwt/v4 v4.4.3/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-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-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-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/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.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||||
github.com/google/uuid v1.3.0/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/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.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 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/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-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/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.3.0/go.mod h1:Pkfl5aHPm1nk2H9h0bjmnJD/BcgbGXUBGnn1kMkgxc8=
|
||||||
github.com/jonboulle/clockwork v0.4.0 h1:p4Cf1aMWXnXAUh8lVfewRBx1zaTSYKrKMF2g3ST4RZ4=
|
github.com/jonboulle/clockwork v0.4.0 h1:p4Cf1aMWXnXAUh8lVfewRBx1zaTSYKrKMF2g3ST4RZ4=
|
||||||
github.com/jonboulle/clockwork v0.4.0/go.mod h1:xgRqUGwRcjKCO1vbZUEtSLrqKoPSsUpK7fnezOII0kc=
|
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/fs v0.1.0/go.mod h1:FFnZGqtBN9Gxj7eW1uZ42v5BccTP0vu6NEaFoC2HwRg=
|
||||||
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
|
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.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/pretty v0.3.0/go.mod h1:640gp4NfQd8pI5XOwp5fnNeVWj67G7CFk/SaSQn7NBk=
|
||||||
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
|
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.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/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.0/go.mod h1:TGAoBVkt8w7MPG72TrKIu85MIdXwDuzJYeZuUPFPNwA=
|
||||||
github.com/lyft/protoc-gen-star v0.6.1/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/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.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.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.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/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/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/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.12/go.mod h1:vBmVV0Do6hSBHC8uKUQ71JGW+ZGQq74llk/7bXwjDoI=
|
||||||
github.com/phpdave11/gofpdi v1.0.13/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/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/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-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 h1:kQ6Cb7aHOHTSzNVNEhmp8EcWKLb4CbiMW9h9VyIhO4E=
|
||||||
github.com/robfig/cron/v3 v3.0.0/go.mod h1:eQICP3HwyT7UooqI/z+Ov+PtYAWygg1TEWWzGIFLtro=
|
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/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.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.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/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-20181029194003-1af4ab5afa58/go.mod h1:6lfFZQK844Gfx8o5WFuvpxWRwnSoipWe/p622j1v06w=
|
||||||
github.com/ruudk/golang-pdf417 v0.0.0-20201230142125-a7e3863a1245/go.mod h1:pQAZKsJ8yyVxGRWYNEm9oFB8ieLgKFnamEyDmSA0BRk=
|
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.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.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.3/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
|
||||||
github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk=
|
github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg=
|
||||||
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
|
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 h1:J6jnVHC552uqx7zT+Ux0++tIvLmJQULqxVhCid2u/Gk=
|
||||||
github.com/teris-io/cli v1.0.1/go.mod h1:V9nVD5aZ873RU/tQXLSXO8FieVPQhQvuNohsdsKXsGw=
|
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=
|
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.7.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
|
||||||
golang.org/x/mod v0.8.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.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-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-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||||
golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/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.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
golang.org/x/sys v0.8.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.13.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
golang.org/x/sys v0.14.0 h1:Vz7Qs629MkJkGyHxUlRHizWJRG2j8fbQKjELVSNhy7Q=
|
golang.org/x/sys v0.16.0 h1:xWw16ngr6ZMtmxDyKyIgsE93KNKz5HKmMa3b8ALHidU=
|
||||||
golang.org/x/sys v0.14.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
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-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.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
|
||||||
golang.org/x/term v0.2.0/go.mod h1:TVmDHMZPmdnySmBfhjOoOdhjzdE1h4u1VwSiw2l1Nuc=
|
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.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.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=
|
||||||
golang.org/x/tools v0.7.0/go.mod h1:4pg6aUX35JBAogB10C9AtvVL+qowtN4pT3CGSQex14s=
|
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-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-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||||
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/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=
|
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 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-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/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/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI=
|
||||||
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
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.8/go.mod h1:zNjwkizS+fIFDrDjIAgBSCLkWbJuHF+ar3QRn+Z9aws=
|
||||||
modernc.org/ccgo/v3 v3.16.9/go.mod h1:zNMzC9A9xeNUepy6KuZBbugn3c0Mc9TeiJO4lgvkJDo=
|
modernc.org/ccgo/v3 v3.16.9/go.mod h1:zNMzC9A9xeNUepy6KuZBbugn3c0Mc9TeiJO4lgvkJDo=
|
||||||
modernc.org/ccorpus v1.11.6/go.mod h1:2gEUTrWqdpH2pXsmTM1ZkjeSrUWDpjMu2T6m29L/ErQ=
|
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/httpfs v1.0.6/go.mod h1:7dosgurJGp0sPaRanU53W4xZYKh14wfzX420oZADeHM=
|
||||||
modernc.org/libc v0.0.0-20220428101251-2d5f3daf273b/go.mod h1:p7Mg4+koNjc8jkqwcoFBJx7tXkpj00G77X7A72jXPXA=
|
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=
|
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.16.19/go.mod h1:p7Mg4+koNjc8jkqwcoFBJx7tXkpj00G77X7A72jXPXA=
|
||||||
modernc.org/libc v1.17.0/go.mod h1:XsgLldpP4aWlPlsjqKRdHPqCxCjISdHfM/yeWC5GyW0=
|
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.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.2.2/go.mod h1:mZW8CKdRPY1v87qxC/wUdX5O1qDzXMP5TH3wjfpga6E=
|
||||||
modernc.org/mathutil v1.4.1/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.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.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.0/go.mod h1:/0wo5ibyrQiaoUoH7f9D8dnglAmILJ5/cxZlRECf+Nw=
|
||||||
modernc.org/memory v1.2.1/go.mod h1:PkUhL0Mugw21sHPeskwZW4D6VscE/GQJOnIpCnW6pSU=
|
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.1/go.mod h1:WdSiB5evDcignE70guQKxYUl14mgWtbClRi5wmkkTX0=
|
||||||
modernc.org/opt v0.1.3/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.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.1/go.mod h1:DE+MQQ/hjKBZS2zNInV5hhcipt5rLPWkmpbGeW5mmdw=
|
||||||
modernc.org/strutil v1.1.3/go.mod h1:MEHNA7PdEnEwLvspRMtWTNnp2nnyvMfkimT1NKNAGbw=
|
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/tcl v1.13.1/go.mod h1:XOLfOwzhkljL4itZkK6T72ckMgvj0BDsnKNdZVUOecw=
|
||||||
modernc.org/token v1.0.0/go.mod h1:UGzOrNV1mAFSEB63lOFHIpNRUVMvYTc6yu1SMY/XTDM=
|
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=
|
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/binaryregexp v0.2.0/go.mod h1:qTv7/COck+e2FymRvadv62gMdZztPaShugOCi3I+8D8=
|
||||||
rsc.io/pdf v0.1.1/go.mod h1:n8OzWcQ6Sp37PL01nO98y4iUCRdTGarVfzxY20ICaU4=
|
rsc.io/pdf v0.1.1/go.mod h1:n8OzWcQ6Sp37PL01nO98y4iUCRdTGarVfzxY20ICaU4=
|
||||||
|
|||||||
8
internal/common/config/sqlite.go
Normal file
8
internal/common/config/sqlite.go
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
package config
|
||||||
|
|
||||||
|
import "time"
|
||||||
|
|
||||||
|
type Sqlite struct {
|
||||||
|
DSN string `json:"dsn"`
|
||||||
|
ShutdownTimeout time.Duration `json:"shutdown_timeout"`
|
||||||
|
}
|
||||||
@ -1,21 +1,73 @@
|
|||||||
package adapters
|
package adapters
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
"git.loyso.art/frx/kurious/internal/kurious/domain"
|
||||||
|
)
|
||||||
|
|
||||||
type inMemoryMapper struct {
|
type inMemoryMapper struct {
|
||||||
courseThematicsByID map[string]string
|
courseThematicsByID map[string]string
|
||||||
learningTypeByID 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 {
|
func NewMemoryMapper(courseThematics, learningType map[string]string) *inMemoryMapper {
|
||||||
return inMemoryMapper{
|
return &inMemoryMapper{
|
||||||
courseThematicsByID: courseThematics,
|
courseThematicsByID: courseThematics,
|
||||||
learningTypeByID: learningType,
|
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]
|
return m.courseThematicsByID[id]
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m inMemoryMapper) LearningTypeNameByID(id string) string {
|
func (m *inMemoryMapper) LearningTypeNameByID(id string) string {
|
||||||
return m.learningTypeByID[id]
|
return m.learningTypeByID[id]
|
||||||
}
|
}
|
||||||
|
|||||||
377
internal/kurious/adapters/sqlite_course_repository.go
Normal file
377
internal/kurious/adapters/sqlite_course_repository.go
Normal file
@ -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,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
119
internal/kurious/adapters/sqlite_course_repository_test.go
Normal file
119
internal/kurious/adapters/sqlite_course_repository_test.go
Normal file
@ -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)
|
||||||
|
}
|
||||||
@ -41,6 +41,7 @@ func NewListCourseHandler(
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (h listCourseHandler) Handle(ctx context.Context, query ListCourse) (out domain.ListCoursesResult, err error) {
|
func (h listCourseHandler) Handle(ctx context.Context, query ListCourse) (out domain.ListCoursesResult, err error) {
|
||||||
|
out.AvailableCoursesOfSub = map[string]int{}
|
||||||
out.NextPageToken = query.NextPageToken
|
out.NextPageToken = query.NextPageToken
|
||||||
drainFull := query.Limit == 0
|
drainFull := query.Limit == 0
|
||||||
if !drainFull {
|
if !drainFull {
|
||||||
@ -76,5 +77,11 @@ func (h listCourseHandler) Handle(ctx context.Context, query ListCourse) (out do
|
|||||||
break
|
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
|
return out, nil
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,6 +1,11 @@
|
|||||||
package domain
|
package domain
|
||||||
|
|
||||||
|
import "context"
|
||||||
|
|
||||||
type CourseMapper interface {
|
type CourseMapper interface {
|
||||||
CourseThematicNameByID(string) string
|
CourseThematicNameByID(string) string
|
||||||
LearningTypeNameByID(string) string
|
LearningTypeNameByID(string) string
|
||||||
|
|
||||||
|
CollectCounts(context.Context, CourseRepository) error
|
||||||
|
GetCounts(byCourseThematic, byLearningType string) int
|
||||||
}
|
}
|
||||||
|
|||||||
@ -35,8 +35,9 @@ type CreateCourseParams struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type ListCoursesResult struct {
|
type ListCoursesResult struct {
|
||||||
Courses []Course
|
Courses []Course
|
||||||
NextPageToken string
|
AvailableCoursesOfSub map[string]int
|
||||||
|
NextPageToken string
|
||||||
}
|
}
|
||||||
|
|
||||||
type ListLearningTypeResult struct {
|
type ListLearningTypeResult struct {
|
||||||
|
|||||||
@ -11,6 +11,10 @@ templ head(title string) {
|
|||||||
integrity="sha384-T3c6CoIi6uLrA9TneNEoa7RxnatzjcDSCmG1MXxSR1GAsXEV/Dwwykc2MPK8M2HN"
|
integrity="sha384-T3c6CoIi6uLrA9TneNEoa7RxnatzjcDSCmG1MXxSR1GAsXEV/Dwwykc2MPK8M2HN"
|
||||||
crossorigin="anonymous"
|
crossorigin="anonymous"
|
||||||
/>
|
/>
|
||||||
|
<link
|
||||||
|
rel="stylesheet"
|
||||||
|
href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.11.3/font/bootstrap-icons.min.css"
|
||||||
|
/>
|
||||||
<script
|
<script
|
||||||
src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/js/bootstrap.bundle.min.js"
|
src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/js/bootstrap.bundle.min.js"
|
||||||
integrity="sha384-C6RzsynM9kWDrMNeT87bh95OGNyZPhcTNXj1NW7RuBCsyN/o0jlpcV8Qyq46cDfL"
|
integrity="sha384-C6RzsynM9kWDrMNeT87bh95OGNyZPhcTNXj1NW7RuBCsyN/o0jlpcV8Qyq46cDfL"
|
||||||
|
|||||||
@ -36,7 +36,7 @@ func head(title string) templ.Component {
|
|||||||
if templ_7745c5c3_Err != nil {
|
if templ_7745c5c3_Err != nil {
|
||||||
return templ_7745c5c3_Err
|
return templ_7745c5c3_Err
|
||||||
}
|
}
|
||||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("</title><meta charset=\"UTF-8\"><meta name=\"viewport\" content=\"width=device-width, initial-scale=1\"><link href=\"https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/css/bootstrap.min.css\" rel=\"stylesheet\" integrity=\"sha384-T3c6CoIi6uLrA9TneNEoa7RxnatzjcDSCmG1MXxSR1GAsXEV/Dwwykc2MPK8M2HN\" crossorigin=\"anonymous\"><script src=\"https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/js/bootstrap.bundle.min.js\" integrity=\"sha384-C6RzsynM9kWDrMNeT87bh95OGNyZPhcTNXj1NW7RuBCsyN/o0jlpcV8Qyq46cDfL\" crossorigin=\"anonymous\">")
|
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("</title><meta charset=\"UTF-8\"><meta name=\"viewport\" content=\"width=device-width, initial-scale=1\"><link href=\"https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/css/bootstrap.min.css\" rel=\"stylesheet\" integrity=\"sha384-T3c6CoIi6uLrA9TneNEoa7RxnatzjcDSCmG1MXxSR1GAsXEV/Dwwykc2MPK8M2HN\" crossorigin=\"anonymous\"><link rel=\"stylesheet\" href=\"https://cdn.jsdelivr.net/npm/bootstrap-icons@1.11.3/font/bootstrap-icons.min.css\"><script src=\"https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/js/bootstrap.bundle.min.js\" integrity=\"sha384-C6RzsynM9kWDrMNeT87bh95OGNyZPhcTNXj1NW7RuBCsyN/o0jlpcV8Qyq46cDfL\" crossorigin=\"anonymous\">")
|
||||||
if templ_7745c5c3_Err != nil {
|
if templ_7745c5c3_Err != nil {
|
||||||
return templ_7745c5c3_Err
|
return templ_7745c5c3_Err
|
||||||
}
|
}
|
||||||
|
|||||||
@ -89,7 +89,7 @@ templ listCoursesSectionFilters(params FilterFormParams) {
|
|||||||
id="learning-type-filter"
|
id="learning-type-filter"
|
||||||
class={"form-select"}
|
class={"form-select"}
|
||||||
>
|
>
|
||||||
<option selected?={params.ActiveLearningType.ID==""}>All</option>
|
<option value="" selected?={params.ActiveLearningType.ID==""}>All</option>
|
||||||
for _, learningType := range params.AvailableLearningTypes {
|
for _, learningType := range params.AvailableLearningTypes {
|
||||||
<option
|
<option
|
||||||
selected?={params.ActiveLearningType.ID==learningType.ID}
|
selected?={params.ActiveLearningType.ID==learningType.ID}
|
||||||
@ -102,7 +102,7 @@ templ listCoursesSectionFilters(params FilterFormParams) {
|
|||||||
id="course-thematic-filter"
|
id="course-thematic-filter"
|
||||||
class={"form-select", templ.KV("d-none", len(params.AvailableCourseThematics) == 0)}
|
class={"form-select", templ.KV("d-none", len(params.AvailableCourseThematics) == 0)}
|
||||||
>
|
>
|
||||||
<option selected?={params.ActiveLearningType.ID==""}>All</option>
|
<option value="" selected?={params.ActiveLearningType.ID==""}>All</option>
|
||||||
for _, courseThematic := range params.AvailableCourseThematics {
|
for _, courseThematic := range params.AvailableCourseThematics {
|
||||||
<option
|
<option
|
||||||
selected?={params.ActiveCourseThematic.ID==courseThematic.ID}
|
selected?={params.ActiveCourseThematic.ID==courseThematic.ID}
|
||||||
@ -110,7 +110,7 @@ templ listCoursesSectionFilters(params FilterFormParams) {
|
|||||||
>{courseThematic.Name}</option>
|
>{courseThematic.Name}</option>
|
||||||
}
|
}
|
||||||
</select>
|
</select>
|
||||||
<button class="btn btn-outline-secondary" type="button">Go</button>
|
<button id="filter-course-thematic" class="btn btn-outline-secondary" type="submit">Go</button>
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
@ -120,8 +120,6 @@ templ listCoursesLearning(containers []CategoryContainer) {
|
|||||||
for _, container := range containers {
|
for _, container := range containers {
|
||||||
<section class="row first-class-group">
|
<section class="row first-class-group">
|
||||||
<h1 class="title">{container.Name}</h1>
|
<h1 class="title">{container.Name}</h1>
|
||||||
<p>{container.Description}</p>
|
|
||||||
<div class="block">Placeholder for filter</div>
|
|
||||||
|
|
||||||
for _, subcategory := range container.Subcategories {
|
for _, subcategory := range container.Subcategories {
|
||||||
@listCoursesThematicRow(subcategory)
|
@listCoursesThematicRow(subcategory)
|
||||||
@ -134,9 +132,9 @@ templ listCoursesLearning(containers []CategoryContainer) {
|
|||||||
templ listCoursesThematicRow(subcategory SubcategoryContainer) {
|
templ listCoursesThematicRow(subcategory SubcategoryContainer) {
|
||||||
<div class="block second-class-group">
|
<div class="block second-class-group">
|
||||||
<h2 class="title">{subcategory.Name}</h2>
|
<h2 class="title">{subcategory.Name}</h2>
|
||||||
<p>{subcategory.Description}</p>
|
<p>В категогрии {subcategory.Name} собраны {strconv.Itoa(subcategory.Count)} курсов. Раз в неделю мы обновляем информацию о всех курсах.</p>
|
||||||
|
|
||||||
<div class="row g-4">
|
<div class="row row-cols-1 row-cols-md-4 g-4">
|
||||||
for _, info := range subcategory.Courses {
|
for _, info := range subcategory.Courses {
|
||||||
@listCoursesCard(info)
|
@listCoursesCard(info)
|
||||||
}
|
}
|
||||||
@ -151,16 +149,17 @@ css myImg() {
|
|||||||
|
|
||||||
|
|
||||||
css cardTextSize() {
|
css cardTextSize() {
|
||||||
min-height: 16rem;
|
min-height: 12rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
templ listCoursesCard(info CourseInfo) {
|
templ listCoursesCard(info CourseInfo) {
|
||||||
<div class="col-12 col-md-6 col-lg-3">
|
// <div class="col-12 col-md-6 col-lg-3">
|
||||||
<div class="card">
|
<div class="col">
|
||||||
<img src={ GetOrFallback(info.ImageLink, "https://placehold.co/128x128")} alt="Course picture" class={"card-img-top", myImg()}/>
|
<div class="card h-100">
|
||||||
<div class={"card-body", cardTextSize()}>
|
<img src={ GetOrFallback(info.ImageLink, "https://placehold.co/128x128")} alt="Course picture" class={"card-img-top"}/>
|
||||||
|
<div class={"card-body", cardTextSize(), "row"}>
|
||||||
<h5 class="card-title">{info.Name}</h5>
|
<h5 class="card-title">{info.Name}</h5>
|
||||||
<div class="input-group d-flex">
|
<div class="input-group d-flex align-self-end">
|
||||||
<a
|
<a
|
||||||
href={ templ.URL(info.OriginLink) }
|
href={ templ.URL(info.OriginLink) }
|
||||||
class="btn text btn-outline-primary flex-grow-1"
|
class="btn text btn-outline-primary flex-grow-1"
|
||||||
|
|||||||
@ -273,7 +273,7 @@ func listCoursesSectionFilters(params FilterFormParams) templ.Component {
|
|||||||
if templ_7745c5c3_Err != nil {
|
if templ_7745c5c3_Err != nil {
|
||||||
return templ_7745c5c3_Err
|
return templ_7745c5c3_Err
|
||||||
}
|
}
|
||||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("\"><option")
|
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("\"><option value=\"\"")
|
||||||
if templ_7745c5c3_Err != nil {
|
if templ_7745c5c3_Err != nil {
|
||||||
return templ_7745c5c3_Err
|
return templ_7745c5c3_Err
|
||||||
}
|
}
|
||||||
@ -350,7 +350,7 @@ func listCoursesSectionFilters(params FilterFormParams) templ.Component {
|
|||||||
if templ_7745c5c3_Err != nil {
|
if templ_7745c5c3_Err != nil {
|
||||||
return templ_7745c5c3_Err
|
return templ_7745c5c3_Err
|
||||||
}
|
}
|
||||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("\"><option")
|
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("\"><option value=\"\"")
|
||||||
if templ_7745c5c3_Err != nil {
|
if templ_7745c5c3_Err != nil {
|
||||||
return templ_7745c5c3_Err
|
return templ_7745c5c3_Err
|
||||||
}
|
}
|
||||||
@ -410,7 +410,7 @@ func listCoursesSectionFilters(params FilterFormParams) templ.Component {
|
|||||||
return templ_7745c5c3_Err
|
return templ_7745c5c3_Err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("</select> <button class=\"btn btn-outline-secondary\" type=\"button\">")
|
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("</select> <button id=\"filter-course-thematic\" class=\"btn btn-outline-secondary\" type=\"submit\">")
|
||||||
if templ_7745c5c3_Err != nil {
|
if templ_7745c5c3_Err != nil {
|
||||||
return templ_7745c5c3_Err
|
return templ_7745c5c3_Err
|
||||||
}
|
}
|
||||||
@ -457,29 +457,7 @@ func listCoursesLearning(containers []CategoryContainer) templ.Component {
|
|||||||
if templ_7745c5c3_Err != nil {
|
if templ_7745c5c3_Err != nil {
|
||||||
return templ_7745c5c3_Err
|
return templ_7745c5c3_Err
|
||||||
}
|
}
|
||||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("</h1><p>")
|
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("</h1>")
|
||||||
if templ_7745c5c3_Err != nil {
|
|
||||||
return templ_7745c5c3_Err
|
|
||||||
}
|
|
||||||
var templ_7745c5c3_Var20 string
|
|
||||||
templ_7745c5c3_Var20, templ_7745c5c3_Err = templ.JoinStringErrs(container.Description)
|
|
||||||
if templ_7745c5c3_Err != nil {
|
|
||||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/kurious/ports/http/bootstrap/list.templ`, Line: 122, Col: 28}
|
|
||||||
}
|
|
||||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var20))
|
|
||||||
if templ_7745c5c3_Err != nil {
|
|
||||||
return templ_7745c5c3_Err
|
|
||||||
}
|
|
||||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("</p><div class=\"block\">")
|
|
||||||
if templ_7745c5c3_Err != nil {
|
|
||||||
return templ_7745c5c3_Err
|
|
||||||
}
|
|
||||||
templ_7745c5c3_Var21 := `Placeholder for filter`
|
|
||||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var21)
|
|
||||||
if templ_7745c5c3_Err != nil {
|
|
||||||
return templ_7745c5c3_Err
|
|
||||||
}
|
|
||||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("</div>")
|
|
||||||
if templ_7745c5c3_Err != nil {
|
if templ_7745c5c3_Err != nil {
|
||||||
return templ_7745c5c3_Err
|
return templ_7745c5c3_Err
|
||||||
}
|
}
|
||||||
@ -509,21 +487,21 @@ func listCoursesThematicRow(subcategory SubcategoryContainer) templ.Component {
|
|||||||
defer templ.ReleaseBuffer(templ_7745c5c3_Buffer)
|
defer templ.ReleaseBuffer(templ_7745c5c3_Buffer)
|
||||||
}
|
}
|
||||||
ctx = templ.InitializeContext(ctx)
|
ctx = templ.InitializeContext(ctx)
|
||||||
templ_7745c5c3_Var22 := templ.GetChildren(ctx)
|
templ_7745c5c3_Var20 := templ.GetChildren(ctx)
|
||||||
if templ_7745c5c3_Var22 == nil {
|
if templ_7745c5c3_Var20 == nil {
|
||||||
templ_7745c5c3_Var22 = templ.NopComponent
|
templ_7745c5c3_Var20 = templ.NopComponent
|
||||||
}
|
}
|
||||||
ctx = templ.ClearChildren(ctx)
|
ctx = templ.ClearChildren(ctx)
|
||||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("<div class=\"block second-class-group\"><h2 class=\"title\">")
|
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("<div class=\"block second-class-group\"><h2 class=\"title\">")
|
||||||
if templ_7745c5c3_Err != nil {
|
if templ_7745c5c3_Err != nil {
|
||||||
return templ_7745c5c3_Err
|
return templ_7745c5c3_Err
|
||||||
}
|
}
|
||||||
var templ_7745c5c3_Var23 string
|
var templ_7745c5c3_Var21 string
|
||||||
templ_7745c5c3_Var23, templ_7745c5c3_Err = templ.JoinStringErrs(subcategory.Name)
|
templ_7745c5c3_Var21, templ_7745c5c3_Err = templ.JoinStringErrs(subcategory.Name)
|
||||||
if templ_7745c5c3_Err != nil {
|
if templ_7745c5c3_Err != nil {
|
||||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/kurious/ports/http/bootstrap/list.templ`, Line: 135, Col: 37}
|
return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/kurious/ports/http/bootstrap/list.templ`, Line: 133, Col: 37}
|
||||||
}
|
}
|
||||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var23))
|
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var21))
|
||||||
if templ_7745c5c3_Err != nil {
|
if templ_7745c5c3_Err != nil {
|
||||||
return templ_7745c5c3_Err
|
return templ_7745c5c3_Err
|
||||||
}
|
}
|
||||||
@ -531,16 +509,48 @@ func listCoursesThematicRow(subcategory SubcategoryContainer) templ.Component {
|
|||||||
if templ_7745c5c3_Err != nil {
|
if templ_7745c5c3_Err != nil {
|
||||||
return templ_7745c5c3_Err
|
return templ_7745c5c3_Err
|
||||||
}
|
}
|
||||||
var templ_7745c5c3_Var24 string
|
templ_7745c5c3_Var22 := `В категогрии `
|
||||||
templ_7745c5c3_Var24, templ_7745c5c3_Err = templ.JoinStringErrs(subcategory.Description)
|
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var22)
|
||||||
if templ_7745c5c3_Err != nil {
|
|
||||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/kurious/ports/http/bootstrap/list.templ`, Line: 136, Col: 29}
|
|
||||||
}
|
|
||||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var24))
|
|
||||||
if templ_7745c5c3_Err != nil {
|
if templ_7745c5c3_Err != nil {
|
||||||
return templ_7745c5c3_Err
|
return templ_7745c5c3_Err
|
||||||
}
|
}
|
||||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("</p><div class=\"row g-4\">")
|
var templ_7745c5c3_Var23 string
|
||||||
|
templ_7745c5c3_Var23, templ_7745c5c3_Err = templ.JoinStringErrs(subcategory.Name)
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/kurious/ports/http/bootstrap/list.templ`, Line: 134, Col: 46}
|
||||||
|
}
|
||||||
|
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var23))
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(" ")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
templ_7745c5c3_Var24 := `собраны `
|
||||||
|
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var24)
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
var templ_7745c5c3_Var25 string
|
||||||
|
templ_7745c5c3_Var25, templ_7745c5c3_Err = templ.JoinStringErrs(strconv.Itoa(subcategory.Count))
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/kurious/ports/http/bootstrap/list.templ`, Line: 134, Col: 95}
|
||||||
|
}
|
||||||
|
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var25))
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(" ")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
templ_7745c5c3_Var26 := `курсов. Раз в неделю мы обновляем информацию о всех курсах.`
|
||||||
|
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var26)
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("</p><div class=\"row row-cols-1 row-cols-md-4 g-4\">")
|
||||||
if templ_7745c5c3_Err != nil {
|
if templ_7745c5c3_Err != nil {
|
||||||
return templ_7745c5c3_Err
|
return templ_7745c5c3_Err
|
||||||
}
|
}
|
||||||
@ -574,7 +584,7 @@ func myImg() templ.CSSClass {
|
|||||||
|
|
||||||
func cardTextSize() templ.CSSClass {
|
func cardTextSize() templ.CSSClass {
|
||||||
var templ_7745c5c3_CSSBuilder strings.Builder
|
var templ_7745c5c3_CSSBuilder strings.Builder
|
||||||
templ_7745c5c3_CSSBuilder.WriteString(`min-height:16rem;`)
|
templ_7745c5c3_CSSBuilder.WriteString(`min-height:12rem;`)
|
||||||
templ_7745c5c3_CSSID := templ.CSSID(`cardTextSize`, templ_7745c5c3_CSSBuilder.String())
|
templ_7745c5c3_CSSID := templ.CSSID(`cardTextSize`, templ_7745c5c3_CSSBuilder.String())
|
||||||
return templ.ComponentCSSClass{
|
return templ.ComponentCSSClass{
|
||||||
ID: templ_7745c5c3_CSSID,
|
ID: templ_7745c5c3_CSSID,
|
||||||
@ -590,17 +600,17 @@ func listCoursesCard(info CourseInfo) templ.Component {
|
|||||||
defer templ.ReleaseBuffer(templ_7745c5c3_Buffer)
|
defer templ.ReleaseBuffer(templ_7745c5c3_Buffer)
|
||||||
}
|
}
|
||||||
ctx = templ.InitializeContext(ctx)
|
ctx = templ.InitializeContext(ctx)
|
||||||
templ_7745c5c3_Var25 := templ.GetChildren(ctx)
|
templ_7745c5c3_Var27 := templ.GetChildren(ctx)
|
||||||
if templ_7745c5c3_Var25 == nil {
|
if templ_7745c5c3_Var27 == nil {
|
||||||
templ_7745c5c3_Var25 = templ.NopComponent
|
templ_7745c5c3_Var27 = templ.NopComponent
|
||||||
}
|
}
|
||||||
ctx = templ.ClearChildren(ctx)
|
ctx = templ.ClearChildren(ctx)
|
||||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("<div class=\"col-12 col-md-6 col-lg-3\"><div class=\"card\">")
|
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("<div class=\"col\"><div class=\"card h-100\">")
|
||||||
if templ_7745c5c3_Err != nil {
|
if templ_7745c5c3_Err != nil {
|
||||||
return templ_7745c5c3_Err
|
return templ_7745c5c3_Err
|
||||||
}
|
}
|
||||||
var templ_7745c5c3_Var26 = []any{"card-img-top", myImg()}
|
var templ_7745c5c3_Var28 = []any{"card-img-top"}
|
||||||
templ_7745c5c3_Err = templ.RenderCSSItems(ctx, templ_7745c5c3_Buffer, templ_7745c5c3_Var26...)
|
templ_7745c5c3_Err = templ.RenderCSSItems(ctx, templ_7745c5c3_Buffer, templ_7745c5c3_Var28...)
|
||||||
if templ_7745c5c3_Err != nil {
|
if templ_7745c5c3_Err != nil {
|
||||||
return templ_7745c5c3_Err
|
return templ_7745c5c3_Err
|
||||||
}
|
}
|
||||||
@ -616,7 +626,7 @@ func listCoursesCard(info CourseInfo) templ.Component {
|
|||||||
if templ_7745c5c3_Err != nil {
|
if templ_7745c5c3_Err != nil {
|
||||||
return templ_7745c5c3_Err
|
return templ_7745c5c3_Err
|
||||||
}
|
}
|
||||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ.CSSClasses(templ_7745c5c3_Var26).String()))
|
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ.CSSClasses(templ_7745c5c3_Var28).String()))
|
||||||
if templ_7745c5c3_Err != nil {
|
if templ_7745c5c3_Err != nil {
|
||||||
return templ_7745c5c3_Err
|
return templ_7745c5c3_Err
|
||||||
}
|
}
|
||||||
@ -624,8 +634,8 @@ func listCoursesCard(info CourseInfo) templ.Component {
|
|||||||
if templ_7745c5c3_Err != nil {
|
if templ_7745c5c3_Err != nil {
|
||||||
return templ_7745c5c3_Err
|
return templ_7745c5c3_Err
|
||||||
}
|
}
|
||||||
var templ_7745c5c3_Var27 = []any{"card-body", cardTextSize()}
|
var templ_7745c5c3_Var29 = []any{"card-body", cardTextSize(), "row"}
|
||||||
templ_7745c5c3_Err = templ.RenderCSSItems(ctx, templ_7745c5c3_Buffer, templ_7745c5c3_Var27...)
|
templ_7745c5c3_Err = templ.RenderCSSItems(ctx, templ_7745c5c3_Buffer, templ_7745c5c3_Var29...)
|
||||||
if templ_7745c5c3_Err != nil {
|
if templ_7745c5c3_Err != nil {
|
||||||
return templ_7745c5c3_Err
|
return templ_7745c5c3_Err
|
||||||
}
|
}
|
||||||
@ -633,7 +643,7 @@ func listCoursesCard(info CourseInfo) templ.Component {
|
|||||||
if templ_7745c5c3_Err != nil {
|
if templ_7745c5c3_Err != nil {
|
||||||
return templ_7745c5c3_Err
|
return templ_7745c5c3_Err
|
||||||
}
|
}
|
||||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ.CSSClasses(templ_7745c5c3_Var27).String()))
|
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ.CSSClasses(templ_7745c5c3_Var29).String()))
|
||||||
if templ_7745c5c3_Err != nil {
|
if templ_7745c5c3_Err != nil {
|
||||||
return templ_7745c5c3_Err
|
return templ_7745c5c3_Err
|
||||||
}
|
}
|
||||||
@ -641,21 +651,21 @@ func listCoursesCard(info CourseInfo) templ.Component {
|
|||||||
if templ_7745c5c3_Err != nil {
|
if templ_7745c5c3_Err != nil {
|
||||||
return templ_7745c5c3_Err
|
return templ_7745c5c3_Err
|
||||||
}
|
}
|
||||||
var templ_7745c5c3_Var28 string
|
var templ_7745c5c3_Var30 string
|
||||||
templ_7745c5c3_Var28, templ_7745c5c3_Err = templ.JoinStringErrs(info.Name)
|
templ_7745c5c3_Var30, templ_7745c5c3_Err = templ.JoinStringErrs(info.Name)
|
||||||
if templ_7745c5c3_Err != nil {
|
if templ_7745c5c3_Err != nil {
|
||||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/kurious/ports/http/bootstrap/list.templ`, Line: 161, Col: 38}
|
return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/kurious/ports/http/bootstrap/list.templ`, Line: 160, Col: 38}
|
||||||
}
|
}
|
||||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var28))
|
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var30))
|
||||||
if templ_7745c5c3_Err != nil {
|
if templ_7745c5c3_Err != nil {
|
||||||
return templ_7745c5c3_Err
|
return templ_7745c5c3_Err
|
||||||
}
|
}
|
||||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("</h5><div class=\"input-group d-flex\"><a href=\"")
|
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("</h5><div class=\"input-group d-flex align-self-end\"><a href=\"")
|
||||||
if templ_7745c5c3_Err != nil {
|
if templ_7745c5c3_Err != nil {
|
||||||
return templ_7745c5c3_Err
|
return templ_7745c5c3_Err
|
||||||
}
|
}
|
||||||
var templ_7745c5c3_Var29 templ.SafeURL = templ.URL(info.OriginLink)
|
var templ_7745c5c3_Var31 templ.SafeURL = templ.URL(info.OriginLink)
|
||||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(string(templ_7745c5c3_Var29)))
|
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(string(templ_7745c5c3_Var31)))
|
||||||
if templ_7745c5c3_Err != nil {
|
if templ_7745c5c3_Err != nil {
|
||||||
return templ_7745c5c3_Err
|
return templ_7745c5c3_Err
|
||||||
}
|
}
|
||||||
@ -663,8 +673,8 @@ func listCoursesCard(info CourseInfo) templ.Component {
|
|||||||
if templ_7745c5c3_Err != nil {
|
if templ_7745c5c3_Err != nil {
|
||||||
return templ_7745c5c3_Err
|
return templ_7745c5c3_Err
|
||||||
}
|
}
|
||||||
templ_7745c5c3_Var30 := `Go!`
|
templ_7745c5c3_Var32 := `Go!`
|
||||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var30)
|
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var32)
|
||||||
if templ_7745c5c3_Err != nil {
|
if templ_7745c5c3_Err != nil {
|
||||||
return templ_7745c5c3_Err
|
return templ_7745c5c3_Err
|
||||||
}
|
}
|
||||||
@ -672,12 +682,12 @@ func listCoursesCard(info CourseInfo) templ.Component {
|
|||||||
if templ_7745c5c3_Err != nil {
|
if templ_7745c5c3_Err != nil {
|
||||||
return templ_7745c5c3_Err
|
return templ_7745c5c3_Err
|
||||||
}
|
}
|
||||||
var templ_7745c5c3_Var31 string
|
var templ_7745c5c3_Var33 string
|
||||||
templ_7745c5c3_Var31, templ_7745c5c3_Err = templ.JoinStringErrs(strconv.Itoa(info.FullPrice))
|
templ_7745c5c3_Var33, templ_7745c5c3_Err = templ.JoinStringErrs(strconv.Itoa(info.FullPrice))
|
||||||
if templ_7745c5c3_Err != nil {
|
if templ_7745c5c3_Err != nil {
|
||||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/kurious/ports/http/bootstrap/list.templ`, Line: 168, Col: 36}
|
return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/kurious/ports/http/bootstrap/list.templ`, Line: 167, Col: 36}
|
||||||
}
|
}
|
||||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var31))
|
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var33))
|
||||||
if templ_7745c5c3_Err != nil {
|
if templ_7745c5c3_Err != nil {
|
||||||
return templ_7745c5c3_Err
|
return templ_7745c5c3_Err
|
||||||
}
|
}
|
||||||
@ -685,8 +695,8 @@ func listCoursesCard(info CourseInfo) templ.Component {
|
|||||||
if templ_7745c5c3_Err != nil {
|
if templ_7745c5c3_Err != nil {
|
||||||
return templ_7745c5c3_Err
|
return templ_7745c5c3_Err
|
||||||
}
|
}
|
||||||
templ_7745c5c3_Var32 := `rub.`
|
templ_7745c5c3_Var34 := `rub.`
|
||||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var32)
|
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var34)
|
||||||
if templ_7745c5c3_Err != nil {
|
if templ_7745c5c3_Err != nil {
|
||||||
return templ_7745c5c3_Err
|
return templ_7745c5c3_Err
|
||||||
}
|
}
|
||||||
@ -709,12 +719,12 @@ func ListCourses(pageType PageKind, s stats, params ListCoursesParams) templ.Com
|
|||||||
defer templ.ReleaseBuffer(templ_7745c5c3_Buffer)
|
defer templ.ReleaseBuffer(templ_7745c5c3_Buffer)
|
||||||
}
|
}
|
||||||
ctx = templ.InitializeContext(ctx)
|
ctx = templ.InitializeContext(ctx)
|
||||||
templ_7745c5c3_Var33 := templ.GetChildren(ctx)
|
templ_7745c5c3_Var35 := templ.GetChildren(ctx)
|
||||||
if templ_7745c5c3_Var33 == nil {
|
if templ_7745c5c3_Var35 == nil {
|
||||||
templ_7745c5c3_Var33 = templ.NopComponent
|
templ_7745c5c3_Var35 = templ.NopComponent
|
||||||
}
|
}
|
||||||
ctx = templ.ClearChildren(ctx)
|
ctx = templ.ClearChildren(ctx)
|
||||||
templ_7745c5c3_Var34 := templ.ComponentFunc(func(ctx context.Context, templ_7745c5c3_W io.Writer) (templ_7745c5c3_Err error) {
|
templ_7745c5c3_Var36 := templ.ComponentFunc(func(ctx context.Context, templ_7745c5c3_W io.Writer) (templ_7745c5c3_Err error) {
|
||||||
templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templ_7745c5c3_W.(*bytes.Buffer)
|
templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templ_7745c5c3_W.(*bytes.Buffer)
|
||||||
if !templ_7745c5c3_IsBuffer {
|
if !templ_7745c5c3_IsBuffer {
|
||||||
templ_7745c5c3_Buffer = templ.GetBuffer()
|
templ_7745c5c3_Buffer = templ.GetBuffer()
|
||||||
@ -745,7 +755,7 @@ func ListCourses(pageType PageKind, s stats, params ListCoursesParams) templ.Com
|
|||||||
}
|
}
|
||||||
return templ_7745c5c3_Err
|
return templ_7745c5c3_Err
|
||||||
})
|
})
|
||||||
templ_7745c5c3_Err = root(pageType, s).Render(templ.WithChildren(ctx, templ_7745c5c3_Var34), templ_7745c5c3_Buffer)
|
templ_7745c5c3_Err = root(pageType, s).Render(templ.WithChildren(ctx, templ_7745c5c3_Var36), templ_7745c5c3_Buffer)
|
||||||
if templ_7745c5c3_Err != nil {
|
if templ_7745c5c3_Err != nil {
|
||||||
return templ_7745c5c3_Err
|
return templ_7745c5c3_Err
|
||||||
}
|
}
|
||||||
|
|||||||
@ -78,6 +78,7 @@ type CategoryBaseInfo struct {
|
|||||||
ID string
|
ID string
|
||||||
Name string
|
Name string
|
||||||
Description string
|
Description string
|
||||||
|
Count int
|
||||||
}
|
}
|
||||||
|
|
||||||
type CategoryContainer struct {
|
type CategoryContainer struct {
|
||||||
@ -94,7 +95,6 @@ type SubcategoryContainer struct {
|
|||||||
|
|
||||||
type ListCoursesParams struct {
|
type ListCoursesParams struct {
|
||||||
FilterForm FilterFormParams
|
FilterForm FilterFormParams
|
||||||
|
|
||||||
Categories []CategoryContainer
|
Categories []CategoryContainer
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -17,7 +17,7 @@ type courseTemplServer struct {
|
|||||||
log *slog.Logger
|
log *slog.Logger
|
||||||
}
|
}
|
||||||
|
|
||||||
func makeTemplListCoursesParams(in ...domain.Course) xtempl.ListCoursesParams {
|
func makeTemplListCoursesParams(counts map[string]int, in ...domain.Course) xtempl.ListCoursesParams {
|
||||||
coursesBySubcategory := make(map[string][]xtempl.CourseInfo, len(in))
|
coursesBySubcategory := make(map[string][]xtempl.CourseInfo, len(in))
|
||||||
subcategoriesByCategories := make(map[string]map[string]struct{}, len(in))
|
subcategoriesByCategories := make(map[string]map[string]struct{}, len(in))
|
||||||
categoryByID := make(map[string]xtempl.CategoryBaseInfo, len(in))
|
categoryByID := make(map[string]xtempl.CategoryBaseInfo, len(in))
|
||||||
@ -46,8 +46,9 @@ func makeTemplListCoursesParams(in ...domain.Course) xtempl.ListCoursesParams {
|
|||||||
}
|
}
|
||||||
if _, ok := categoryByID[c.ThematicID]; !ok {
|
if _, ok := categoryByID[c.ThematicID]; !ok {
|
||||||
categoryByID[c.ThematicID] = xtempl.CategoryBaseInfo{
|
categoryByID[c.ThematicID] = xtempl.CategoryBaseInfo{
|
||||||
ID: c.ThematicID,
|
ID: c.ThematicID,
|
||||||
Name: c.Thematic,
|
Name: c.Thematic,
|
||||||
|
Count: counts[c.ThematicID],
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
@ -95,7 +96,7 @@ func (c courseTemplServer) List(w http.ResponseWriter, r *http.Request) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
params := makeTemplListCoursesParams(listCoursesResult.Courses...)
|
params := makeTemplListCoursesParams(listCoursesResult.AvailableCoursesOfSub, listCoursesResult.Courses...)
|
||||||
|
|
||||||
learningTypeResult, err := c.app.Queries.ListLearningTypes.Handle(ctx, query.ListLearningTypes{})
|
learningTypeResult, err := c.app.Queries.ListLearningTypes.Handle(ctx, query.ListLearningTypes{})
|
||||||
if handleError(ctx, err, w, c.log, "unable to list learning types") {
|
if handleError(ctx, err, w, c.log, "unable to list learning types") {
|
||||||
@ -118,7 +119,7 @@ func (c courseTemplServer) List(w http.ResponseWriter, r *http.Request) {
|
|||||||
courseThematicsResult, err := c.app.Queries.ListCourseThematics.Handle(ctx, query.ListCourseThematics{
|
courseThematicsResult, err := c.app.Queries.ListCourseThematics.Handle(ctx, query.ListCourseThematics{
|
||||||
LearningTypeID: pathParams.learningType,
|
LearningTypeID: pathParams.learningType,
|
||||||
})
|
})
|
||||||
if handleError(ctx, err, w, c.log, "unab;e to list course thematics") {
|
if handleError(ctx, err, w, c.log, "unable to list course thematics") {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -74,6 +74,7 @@ type CategoryBaseInfo struct {
|
|||||||
ID string
|
ID string
|
||||||
Name string
|
Name string
|
||||||
Description string
|
Description string
|
||||||
|
Count int
|
||||||
}
|
}
|
||||||
|
|
||||||
type CategoryContainer struct {
|
type CategoryContainer struct {
|
||||||
|
|||||||
@ -2,11 +2,13 @@ package service
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
"log/slog"
|
"log/slog"
|
||||||
|
|
||||||
"git.loyso.art/frx/kurious/internal/common/config"
|
"git.loyso.art/frx/kurious/internal/common/config"
|
||||||
|
"git.loyso.art/frx/kurious/internal/common/xcontext"
|
||||||
"git.loyso.art/frx/kurious/internal/kurious/adapters"
|
"git.loyso.art/frx/kurious/internal/kurious/adapters"
|
||||||
"git.loyso.art/frx/kurious/internal/kurious/app"
|
"git.loyso.art/frx/kurious/internal/kurious/app"
|
||||||
"git.loyso.art/frx/kurious/internal/kurious/app/command"
|
"git.loyso.art/frx/kurious/internal/kurious/app/command"
|
||||||
@ -14,9 +16,19 @@ import (
|
|||||||
"git.loyso.art/frx/kurious/internal/kurious/domain"
|
"git.loyso.art/frx/kurious/internal/kurious/domain"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
type RepositoryEngine uint8
|
||||||
|
|
||||||
|
const (
|
||||||
|
RepositoryEngineUnknown RepositoryEngine = iota
|
||||||
|
RepositoryEngineYDB
|
||||||
|
RepositoryEngineSqlite
|
||||||
|
)
|
||||||
|
|
||||||
type ApplicationConfig struct {
|
type ApplicationConfig struct {
|
||||||
LogConfig config.Log
|
LogConfig config.Log
|
||||||
YDB config.YDB
|
YDB config.YDB
|
||||||
|
Sqlite config.Sqlite
|
||||||
|
Engine RepositoryEngine
|
||||||
}
|
}
|
||||||
|
|
||||||
type Application struct {
|
type Application struct {
|
||||||
@ -26,14 +38,36 @@ type Application struct {
|
|||||||
closers []io.Closer
|
closers []io.Closer
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewApplication(ctx context.Context, cfg ApplicationConfig, mapper domain.CourseMapper) (Application, error) {
|
func NewApplication(ctx context.Context, cfg ApplicationConfig, mapper domain.CourseMapper) (out Application, err error) {
|
||||||
log := config.NewSLogger(cfg.LogConfig)
|
log := config.NewSLogger(cfg.LogConfig)
|
||||||
ydbConnection, err := adapters.NewYDBConnection(ctx, cfg.YDB, log.With(slog.String("db", "ydb")))
|
|
||||||
if err != nil {
|
var repoCloser io.Closer
|
||||||
return Application{}, fmt.Errorf("making ydb connection: %w", err)
|
var courseadapter domain.CourseRepository
|
||||||
|
switch cfg.Engine {
|
||||||
|
case RepositoryEngineSqlite:
|
||||||
|
sqliteConnection, err := adapters.NewSqliteConnection(ctx, cfg.Sqlite, log.With(slog.String("db", "sqlite")))
|
||||||
|
if err != nil {
|
||||||
|
return Application{}, fmt.Errorf("making sqlite connection: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
courseadapter = sqliteConnection.CourseRepository()
|
||||||
|
repoCloser = sqliteConnection
|
||||||
|
case RepositoryEngineYDB:
|
||||||
|
ydbConnection, err := adapters.NewYDBConnection(ctx, cfg.YDB, log.With(slog.String("db", "ydb")))
|
||||||
|
if err != nil {
|
||||||
|
return Application{}, fmt.Errorf("making ydb connection: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
courseadapter = ydbConnection.CourseRepository()
|
||||||
|
repoCloser = ydbConnection
|
||||||
|
default:
|
||||||
|
return Application{}, errors.New("unable to decide which engine to use")
|
||||||
}
|
}
|
||||||
|
|
||||||
courseadapter := ydbConnection.CourseRepository()
|
err = mapper.CollectCounts(ctx, courseadapter)
|
||||||
|
if err != nil {
|
||||||
|
xcontext.LogWithWarnError(ctx, log, err, "unable to properly collect counts")
|
||||||
|
}
|
||||||
|
|
||||||
application := app.Application{
|
application := app.Application{
|
||||||
Commands: app.Commands{
|
Commands: app.Commands{
|
||||||
@ -50,8 +84,8 @@ func NewApplication(ctx context.Context, cfg ApplicationConfig, mapper domain.Co
|
|||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
out := Application{Application: application}
|
out = Application{Application: application}
|
||||||
out.closers = append(out.closers, ydbConnection)
|
out.closers = append(out.closers, repoCloser)
|
||||||
out.log = log
|
out.log = log
|
||||||
|
|
||||||
return out, nil
|
return out, nil
|
||||||
|
|||||||
24
migrations/sqlite/001_initial.sql
Normal file
24
migrations/sqlite/001_initial.sql
Normal file
@ -0,0 +1,24 @@
|
|||||||
|
CREATE TABLE courses (
|
||||||
|
id TEXT PRIMARY KEY,
|
||||||
|
external_id TEXT,
|
||||||
|
source_type TEXT NOT NULL,
|
||||||
|
source_name TEXT,
|
||||||
|
course_thematic TEXT NOT NULL,
|
||||||
|
learning_type TEXT NOT NULL,
|
||||||
|
organization_id TEXT NOT NULL,
|
||||||
|
origin_link TEXT NOT NULL,
|
||||||
|
image_link TEXT,
|
||||||
|
name TEXT NOT NULL,
|
||||||
|
description TEXT NOT NULL,
|
||||||
|
full_price REAL NOT NULL,
|
||||||
|
discount REAL NOT NULL,
|
||||||
|
duration INTEGER NOT NULL,
|
||||||
|
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
starts_at DATETIME,
|
||||||
|
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
deleted_at DATETIME
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE INDEX idx_course_thematic ON courses (course_thematic);
|
||||||
|
CREATE INDEX idx_learning_type ON courses (learning_type);
|
||||||
|
CREATE INDEX idx_organization_id ON courses (organization_id);
|
||||||
18
migrations/sqlite/assets.go
Normal file
18
migrations/sqlite/assets.go
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
package sqlite
|
||||||
|
|
||||||
|
import (
|
||||||
|
"embed"
|
||||||
|
"io/fs"
|
||||||
|
)
|
||||||
|
|
||||||
|
//go:embed *.sql
|
||||||
|
var migrations embed.FS
|
||||||
|
|
||||||
|
func getMigrationEntries() ([]fs.DirEntry, error) {
|
||||||
|
entries, err := fs.ReadDir(migrations, ".")
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return entries, nil
|
||||||
|
}
|
||||||
240
migrations/sqlite/migrator.go
Normal file
240
migrations/sqlite/migrator.go
Normal file
@ -0,0 +1,240 @@
|
|||||||
|
package sqlite
|
||||||
|
|
||||||
|
import (
|
||||||
|
"cmp"
|
||||||
|
"context"
|
||||||
|
"database/sql"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"io/fs"
|
||||||
|
"log/slog"
|
||||||
|
"path"
|
||||||
|
"slices"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"git.loyso.art/frx/kurious/internal/common/xcontext"
|
||||||
|
)
|
||||||
|
|
||||||
|
type migrationUnit struct {
|
||||||
|
num int
|
||||||
|
name string
|
||||||
|
path string
|
||||||
|
}
|
||||||
|
|
||||||
|
func (u migrationUnit) apply(ctx context.Context, tx *sql.Tx) error {
|
||||||
|
content, err := fs.ReadFile(migrations, u.path)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("reading file: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
query := string(content)
|
||||||
|
_, err = tx.ExecContext(ctx, query)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("executing query: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func sortMigrationUnit(lhs, rhs migrationUnit) int {
|
||||||
|
if lhs.num < rhs.num {
|
||||||
|
return -1
|
||||||
|
} else if lhs.num > rhs.num {
|
||||||
|
return 1
|
||||||
|
} else {
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func RunMigrations(ctx context.Context, db *sql.DB, log *slog.Logger) error {
|
||||||
|
items, err := getMigrationEntries()
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("reading directory: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
units := make([]migrationUnit, 0, len(items))
|
||||||
|
for _, item := range items {
|
||||||
|
if item.IsDir() {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
itemName := item.Name()
|
||||||
|
splitted := strings.SplitN(itemName, "_", 2)
|
||||||
|
if len(splitted) != 2 {
|
||||||
|
return fmt.Errorf("bad number of parts, expected 2, got %d", len(splitted))
|
||||||
|
}
|
||||||
|
|
||||||
|
splittedNum, err := strconv.Atoi(splitted[0])
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("parsing migration number: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if splittedNum < 1 {
|
||||||
|
return fmt.Errorf("migration number expected to be greater than 0, but got %d", splittedNum)
|
||||||
|
}
|
||||||
|
|
||||||
|
unit := migrationUnit{
|
||||||
|
num: splittedNum,
|
||||||
|
name: strings.TrimSuffix(splitted[1], ".sql"),
|
||||||
|
path: path.Join(itemName),
|
||||||
|
}
|
||||||
|
|
||||||
|
xcontext.LogDebug(ctx, log, "found migration unit", slog.Any("unit", unit))
|
||||||
|
|
||||||
|
units = append(units, unit)
|
||||||
|
}
|
||||||
|
|
||||||
|
slices.SortFunc(units, sortMigrationUnit)
|
||||||
|
|
||||||
|
mr := &metaRepository{
|
||||||
|
db: db,
|
||||||
|
log: log,
|
||||||
|
}
|
||||||
|
err = mr.prepare(ctx)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("preparing meta repository: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
count, err := mr.run(ctx, units...)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("running transaction: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if count > 0 {
|
||||||
|
xcontext.LogInfo(ctx, log, "some new migrations has been applied", slog.Int("count", count))
|
||||||
|
} else {
|
||||||
|
xcontext.LogDebug(ctx, log, "no new migrations has been applied")
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
type metaRepository struct {
|
||||||
|
db *sql.DB
|
||||||
|
log *slog.Logger
|
||||||
|
lastAppliedNumber int
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *metaRepository) run(ctx context.Context, units ...migrationUnit) (count int, err error) {
|
||||||
|
idx, found := slices.BinarySearchFunc(units, r.lastAppliedNumber, func(mu migrationUnit, i int) int {
|
||||||
|
return cmp.Compare(mu.num, i)
|
||||||
|
})
|
||||||
|
if !found && r.lastAppliedNumber > 0 {
|
||||||
|
return 0, fmt.Errorf("migration %d stored in meta was not found in provided migrations", r.lastAppliedNumber)
|
||||||
|
} else if r.lastAppliedNumber > 0 {
|
||||||
|
idx++
|
||||||
|
}
|
||||||
|
|
||||||
|
xcontext.LogDebug(
|
||||||
|
ctx, r.log,
|
||||||
|
"starting to apply migrations",
|
||||||
|
slog.Int("last_applied_migration", r.lastAppliedNumber),
|
||||||
|
slog.Int("next_migration_idx", idx),
|
||||||
|
)
|
||||||
|
|
||||||
|
tx, err := r.db.BeginTx(ctx, &sql.TxOptions{
|
||||||
|
Isolation: sql.LevelDefault,
|
||||||
|
ReadOnly: false,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return 0, fmt.Errorf("starting transaction: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
defer func() {
|
||||||
|
var errtx error
|
||||||
|
if err != nil {
|
||||||
|
xcontext.LogError(ctx, r.log, "rolling back migration changes due to error")
|
||||||
|
errtx = tx.Rollback()
|
||||||
|
} else {
|
||||||
|
xcontext.LogDebug(ctx, r.log, "commiting migration changes")
|
||||||
|
errtx = tx.Commit()
|
||||||
|
}
|
||||||
|
|
||||||
|
err = errors.Join(err, errtx)
|
||||||
|
}()
|
||||||
|
|
||||||
|
for i := idx; i < len(units); i++ {
|
||||||
|
unit := units[i]
|
||||||
|
err = unit.apply(ctx, tx)
|
||||||
|
if err != nil {
|
||||||
|
return 0, fmt.Errorf("unable to apply migration %q: %w", unit.name, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
err = r.adjustMigrationApplied(ctx, tx, unit)
|
||||||
|
if err != nil {
|
||||||
|
return 0, fmt.Errorf("storing migration process info: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
xcontext.LogInfo(
|
||||||
|
ctx, r.log, "migration unit applied",
|
||||||
|
slog.Int("number", unit.num),
|
||||||
|
slog.String("name", unit.name),
|
||||||
|
)
|
||||||
|
count++
|
||||||
|
}
|
||||||
|
|
||||||
|
return count, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *metaRepository) prepare(ctx context.Context) error {
|
||||||
|
err := r.makeTable(ctx)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("making table: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
err = r.loadLastAppliedMigration(ctx)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("loading last applied migration: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *metaRepository) makeTable(ctx context.Context) error {
|
||||||
|
const query = `CREATE TABLE IF NOT EXISTS migration_meta (` +
|
||||||
|
` id INT PRIMARY KEY NOT NULL` +
|
||||||
|
`, name TEXT NOT NULL` +
|
||||||
|
`, applied_at INT NOT NULL` +
|
||||||
|
`);`
|
||||||
|
|
||||||
|
_, err := r.db.ExecContext(ctx, query)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("executing query: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *metaRepository) loadLastAppliedMigration(ctx context.Context) error {
|
||||||
|
const query = `SELECT COALESCE(MAX(id), 0) FROM migration_meta;`
|
||||||
|
|
||||||
|
err := r.db.QueryRowContext(ctx, query).Scan(&r.lastAppliedNumber)
|
||||||
|
if err != nil {
|
||||||
|
if !errors.Is(err, sql.ErrNoRows) {
|
||||||
|
return fmt.Errorf("executing query: %w", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *metaRepository) adjustMigrationApplied(ctx context.Context, tx *sql.Tx, unit migrationUnit) error {
|
||||||
|
const query = `INSERT INTO migration_meta (id, name, applied_at) VALUES (?, ?, ?)`
|
||||||
|
|
||||||
|
args := []any{
|
||||||
|
unit.num,
|
||||||
|
unit.name,
|
||||||
|
time.Now().Truncate(time.Second).Unix(),
|
||||||
|
}
|
||||||
|
|
||||||
|
_, err := tx.ExecContext(ctx, query, args...)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("executing query: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
r.lastAppliedNumber = unit.num
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user