able to get product
This commit is contained in:
1
.gitignore
vendored
1
.gitignore
vendored
@ -1,2 +1,3 @@
|
|||||||
|
*.json
|
||||||
bin
|
bin
|
||||||
./tags
|
./tags
|
||||||
|
|||||||
@ -23,7 +23,7 @@ tasks:
|
|||||||
- "$GOBIN/golangci-lint run ./..."
|
- "$GOBIN/golangci-lint run ./..."
|
||||||
test:
|
test:
|
||||||
cmds:
|
cmds:
|
||||||
- go test --count=1 ./internal/...
|
- go test ./internal/...
|
||||||
build:
|
build:
|
||||||
cmds:
|
cmds:
|
||||||
- go build -o $GOBIN/sravnicli -v -ldflags '{{.LDFLAGS}}' cmd/dev/sravnicli/*.go
|
- go build -o $GOBIN/sravnicli -v -ldflags '{{.LDFLAGS}}' cmd/dev/sravnicli/*.go
|
||||||
|
|||||||
13
cmd/dev/sravnicli/config.go
Normal file
13
cmd/dev/sravnicli/config.go
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"sync"
|
||||||
|
|
||||||
|
"git.loyso.art/frx/kurious/internal/common/config"
|
||||||
|
)
|
||||||
|
|
||||||
|
var parseConfigOnce = sync.Once{}
|
||||||
|
|
||||||
|
type cliConfig struct {
|
||||||
|
YDB config.YDB `json:"ydb"`
|
||||||
|
}
|
||||||
@ -2,11 +2,15 @@ package main
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
"log/slog"
|
"log/slog"
|
||||||
"os"
|
"os"
|
||||||
|
|
||||||
"git.loyso.art/frx/kurious/internal/infrastructure/interfaceadapters/courses/sravni"
|
"git.loyso.art/frx/kurious/internal/common/client/sravni"
|
||||||
|
"git.loyso.art/frx/kurious/internal/common/config"
|
||||||
|
"git.loyso.art/frx/kurious/internal/common/errors"
|
||||||
|
"git.loyso.art/frx/kurious/internal/kurious/adapters"
|
||||||
|
|
||||||
"github.com/teris-io/cli"
|
"github.com/teris-io/cli"
|
||||||
)
|
)
|
||||||
@ -19,6 +23,75 @@ const (
|
|||||||
var limitOption = cli.NewOption("limit", "Limits amount of items to return").WithType(cli.TypeInt)
|
var limitOption = cli.NewOption("limit", "Limits amount of items to return").WithType(cli.TypeInt)
|
||||||
var offsetOption = cli.NewOption("offset", "Offsets items to return").WithType(cli.TypeInt)
|
var offsetOption = cli.NewOption("offset", "Offsets items to return").WithType(cli.TypeInt)
|
||||||
|
|
||||||
|
type actionWrapper func(next cli.Action) cli.Action
|
||||||
|
|
||||||
|
func buildCLICommand(f func() cli.Command) cliCommand {
|
||||||
|
return makeCLICommand(f(), actionWrapperParseConfig)
|
||||||
|
}
|
||||||
|
|
||||||
|
func makeCLICommand(cmd cli.Command, wrappers ...actionWrapper) cliCommand {
|
||||||
|
return cliCommand{
|
||||||
|
Command: cmd,
|
||||||
|
actionWrappers: wrappers,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
type cliCommand struct {
|
||||||
|
cli.Command
|
||||||
|
|
||||||
|
actionWrappers []actionWrapper
|
||||||
|
}
|
||||||
|
|
||||||
|
// WithCommand is disabled since it adds action wrappers and
|
||||||
|
// not tested in other cases.
|
||||||
|
func (c cliCommand) WithCommand(cli.Command) cli.Command {
|
||||||
|
panic("wrapped in cliCommand is expected to be leaf command")
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c cliCommand) Action() cli.Action {
|
||||||
|
out := c.Command.Action()
|
||||||
|
if out == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, wrapper := range c.actionWrappers {
|
||||||
|
out = wrapper(out)
|
||||||
|
}
|
||||||
|
|
||||||
|
return out
|
||||||
|
}
|
||||||
|
|
||||||
|
func actionWrapperParseConfig(next cli.Action) cli.Action {
|
||||||
|
return func(args []string, options map[string]string) int {
|
||||||
|
var result int
|
||||||
|
parseConfigOnce.Do(func() {
|
||||||
|
cfgpath, ok := options["config"]
|
||||||
|
if !ok || cfgpath == "" {
|
||||||
|
cfgpath = defaultConfigPath
|
||||||
|
}
|
||||||
|
|
||||||
|
payload, err := os.ReadFile(cfgpath)
|
||||||
|
if err != nil {
|
||||||
|
slog.Error("unable to read config file", slog.Any("err", err))
|
||||||
|
result = -1
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
err = json.Unmarshal(payload, ¤tConfig)
|
||||||
|
if err != nil {
|
||||||
|
slog.Error("unable to unmarshal config file", slog.Any("err", err))
|
||||||
|
result = -1
|
||||||
|
return
|
||||||
|
}
|
||||||
|
})
|
||||||
|
if result != 0 {
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
return next(args, options)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func makeLogger(options map[string]string) *slog.Logger {
|
func makeLogger(options map[string]string) *slog.Logger {
|
||||||
level := slog.LevelInfo
|
level := slog.LevelInfo
|
||||||
if _, ok := options[debugOptName]; ok {
|
if _, ok := options[debugOptName]; ok {
|
||||||
@ -48,3 +121,41 @@ func makeSravniClient(ctx context.Context, log *slog.Logger, options map[string]
|
|||||||
|
|
||||||
return client, nil
|
return client, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type baseAction struct {
|
||||||
|
ctx context.Context
|
||||||
|
log *slog.Logger
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ba *baseAction) getYDBConnection() (*adapters.YDBConnection, error) {
|
||||||
|
if currentConfig.YDB == (config.YDB{}) {
|
||||||
|
return nil, errors.SimpleError("no ydb config set")
|
||||||
|
}
|
||||||
|
|
||||||
|
ydbConn, err := adapters.NewYDBConnection(ba.ctx, currentConfig.YDB, ba.log.With(slog.String("db", "ydb")))
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("making new ydb course repository: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return ydbConn, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ba *baseAction) parse(_ []string, options map[string]string) (err error) {
|
||||||
|
ba.log = makeLogger(options).With(slog.String("component", "action"))
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ba *baseAction) handle() error {
|
||||||
|
return errors.ErrNotImplemented
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ba *baseAction) context() context.Context {
|
||||||
|
return ba.ctx
|
||||||
|
}
|
||||||
|
|
||||||
|
func newBaseAction(ctx context.Context) *baseAction {
|
||||||
|
return &baseAction{
|
||||||
|
ctx: ctx,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@ -12,24 +12,18 @@ import (
|
|||||||
"github.com/teris-io/cli"
|
"github.com/teris-io/cli"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
defaultConfigPath = "config_cli.json"
|
||||||
|
)
|
||||||
|
|
||||||
var defaultOutput = os.Stdout
|
var defaultOutput = os.Stdout
|
||||||
|
var currentConfig = cliConfig{}
|
||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
ctx, cancel := signal.NotifyContext(context.Background(), os.Interrupt)
|
ctx, cancel := signal.NotifyContext(context.Background(), os.Interrupt)
|
||||||
defer cancel()
|
defer cancel()
|
||||||
|
|
||||||
log := slog.New(slog.NewJSONHandler(os.Stdout, &slog.HandlerOptions{
|
ec, err := app(ctx)
|
||||||
Level: slog.LevelDebug,
|
|
||||||
ReplaceAttr: func(_ []string, a slog.Attr) slog.Attr {
|
|
||||||
if a.Key == slog.TimeKey {
|
|
||||||
a.Value = slog.Int64Value(a.Value.Time().Unix())
|
|
||||||
}
|
|
||||||
|
|
||||||
return a
|
|
||||||
},
|
|
||||||
}))
|
|
||||||
|
|
||||||
ec, err := app(ctx, log)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
slog.ErrorContext(ctx, "unable to run app", slog.Any("error", err))
|
slog.ErrorContext(ctx, "unable to run app", slog.Any("error", err))
|
||||||
}
|
}
|
||||||
@ -76,17 +70,20 @@ func setupCLI(ctx context.Context) cli.App {
|
|||||||
WithCommand(mainPageState)
|
WithCommand(mainPageState)
|
||||||
|
|
||||||
apiCategory := setupAPICommand(ctx)
|
apiCategory := setupAPICommand(ctx)
|
||||||
|
ydbCategory := setupYDBCommand(ctx)
|
||||||
description := fmt.Sprintf("sravni dev cli %s (%s)", kurious.Version(), kurious.Commit())
|
description := fmt.Sprintf("sravni dev cli %s (%s)", kurious.Version(), kurious.Commit())
|
||||||
cliApp := cli.New(description).
|
cliApp := cli.New(description).
|
||||||
WithOption(cli.NewOption("verbose", "Verbose execution").WithChar('v').WithType(cli.TypeBool)).
|
WithOption(cli.NewOption("verbose", "Verbose execution").WithChar('v').WithType(cli.TypeBool)).
|
||||||
WithOption(cli.NewOption("json", "JSON outpu format").WithType(cli.TypeBool)).
|
WithOption(cli.NewOption("json", "JSON outpu format").WithType(cli.TypeBool)).
|
||||||
|
WithOption(cli.NewOption("config", "Path to config").WithChar('c').WithType(cli.TypeString)).
|
||||||
WithCommand(mainCategory).
|
WithCommand(mainCategory).
|
||||||
WithCommand(apiCategory)
|
WithCommand(apiCategory).
|
||||||
|
WithCommand(ydbCategory)
|
||||||
|
|
||||||
return cliApp
|
return cliApp
|
||||||
}
|
}
|
||||||
|
|
||||||
func app(ctx context.Context, log *slog.Logger) (exitCode int, err error) {
|
func app(ctx context.Context) (exitCode int, err error) {
|
||||||
devCLI := setupCLI(ctx)
|
devCLI := setupCLI(ctx)
|
||||||
exitCode = devCLI.Run(os.Args, defaultOutput)
|
exitCode = devCLI.Run(os.Args, defaultOutput)
|
||||||
|
|
||||||
|
|||||||
@ -6,8 +6,8 @@ import (
|
|||||||
"log/slog"
|
"log/slog"
|
||||||
"strconv"
|
"strconv"
|
||||||
|
|
||||||
"git.loyso.art/frx/kurious/internal/domain"
|
"git.loyso.art/frx/kurious/internal/common/client/sravni"
|
||||||
"git.loyso.art/frx/kurious/internal/infrastructure/interfaceadapters/courses/sravni"
|
"git.loyso.art/frx/kurious/internal/common/errors"
|
||||||
|
|
||||||
"github.com/teris-io/cli"
|
"github.com/teris-io/cli"
|
||||||
)
|
)
|
||||||
@ -25,12 +25,14 @@ func setupAPICommand(ctx context.Context) cli.Command {
|
|||||||
WithChar('t').
|
WithChar('t').
|
||||||
WithType(cli.TypeString)
|
WithType(cli.TypeString)
|
||||||
|
|
||||||
apiEducationListProducts := cli.NewCommand("list_products", "List products by some filters").
|
apiEducationListProducts := buildCLICommand(func() cli.Command {
|
||||||
WithOption(learningTypeOpt).
|
return cli.NewCommand("list_products", "List products by some filters").
|
||||||
WithOption(courseThematic).
|
WithOption(learningTypeOpt).
|
||||||
WithOption(limitOption).
|
WithOption(courseThematic).
|
||||||
WithOption(offsetOption).
|
WithOption(limitOption).
|
||||||
WithAction(newListProductAction(ctx))
|
WithOption(offsetOption).
|
||||||
|
WithAction(newListProductAction(ctx))
|
||||||
|
})
|
||||||
|
|
||||||
apiEducation := cli.NewCommand("education", "Education related category").
|
apiEducation := cli.NewCommand("education", "Education related category").
|
||||||
WithCommand(apiEducationListProducts)
|
WithCommand(apiEducationListProducts)
|
||||||
@ -64,36 +66,6 @@ func asCLIAction(a action) cli.Action {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
type baseAction struct {
|
|
||||||
ctx context.Context
|
|
||||||
client sravni.Client
|
|
||||||
log *slog.Logger
|
|
||||||
}
|
|
||||||
|
|
||||||
func (ba *baseAction) parse(_ []string, options map[string]string) (err error) {
|
|
||||||
ba.log = makeLogger(options).With(slog.String("component", "action"))
|
|
||||||
ba.client, err = makeSravniClient(ba.ctx, ba.log, options)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (ba *baseAction) handle() error {
|
|
||||||
return domain.ErrNotImplemented
|
|
||||||
}
|
|
||||||
|
|
||||||
func (ba baseAction) context() context.Context {
|
|
||||||
return ba.ctx
|
|
||||||
}
|
|
||||||
|
|
||||||
func newBaseAction(ctx context.Context) *baseAction {
|
|
||||||
return &baseAction{
|
|
||||||
ctx: ctx,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
type listProductsActionParams struct {
|
type listProductsActionParams struct {
|
||||||
learningType string
|
learningType string
|
||||||
courseThematic string
|
courseThematic string
|
||||||
@ -104,6 +76,7 @@ type listProductsActionParams struct {
|
|||||||
type listProductsAction struct {
|
type listProductsAction struct {
|
||||||
*baseAction
|
*baseAction
|
||||||
|
|
||||||
|
client sravni.Client
|
||||||
params listProductsActionParams
|
params listProductsActionParams
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -125,11 +98,11 @@ func (a *listProductsAction) parse(args []string, options map[string]string) err
|
|||||||
|
|
||||||
a.params.learningType, ok = options[learningTypeOptName]
|
a.params.learningType, ok = options[learningTypeOptName]
|
||||||
if !ok {
|
if !ok {
|
||||||
return domain.SimpleError("learning_type is empty")
|
return errors.SimpleError("learning_type is empty")
|
||||||
}
|
}
|
||||||
a.params.courseThematic, ok = options[courseThematicOptName]
|
a.params.courseThematic, ok = options[courseThematicOptName]
|
||||||
if !ok {
|
if !ok {
|
||||||
return domain.SimpleError("course_thematic is empty")
|
return errors.SimpleError("course_thematic is empty")
|
||||||
}
|
}
|
||||||
|
|
||||||
if value, ok := options[limitOption.Key()]; ok {
|
if value, ok := options[limitOption.Key()]; ok {
|
||||||
@ -139,6 +112,13 @@ func (a *listProductsAction) parse(args []string, options map[string]string) err
|
|||||||
a.params.offset, _ = strconv.Atoi(value)
|
a.params.offset, _ = strconv.Atoi(value)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
client, err := makeSravniClient(a.ctx, a.log, options)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("making sravni client: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
a.client = client
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
132
cmd/dev/sravnicli/ydb.go
Normal file
132
cmd/dev/sravnicli/ydb.go
Normal file
@ -0,0 +1,132 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
stderrors "errors"
|
||||||
|
"fmt"
|
||||||
|
"log/slog"
|
||||||
|
|
||||||
|
"git.loyso.art/frx/kurious/internal/common/errors"
|
||||||
|
"git.loyso.art/frx/kurious/internal/common/xcontext"
|
||||||
|
"github.com/teris-io/cli"
|
||||||
|
)
|
||||||
|
|
||||||
|
func setupYDBCommand(ctx context.Context) cli.Command {
|
||||||
|
migrationApply := buildCLICommand(func() cli.Command {
|
||||||
|
return cli.NewCommand("apply", "Applies all known migrations").
|
||||||
|
WithAction(newYDBMigrateApplyAction(ctx))
|
||||||
|
})
|
||||||
|
|
||||||
|
migration := cli.NewCommand("migration", "Migration commands").
|
||||||
|
WithCommand(migrationApply)
|
||||||
|
|
||||||
|
coursesGet := buildCLICommand(func() cli.Command {
|
||||||
|
return cli.NewCommand("get", "Fetches one or more courses").
|
||||||
|
WithArg(cli.NewArg("ids", "List of course ids").AsOptional()).
|
||||||
|
WithAction(newYDBCoursesGetAction(ctx))
|
||||||
|
})
|
||||||
|
|
||||||
|
courses := cli.NewCommand("courses", "Courses commands").
|
||||||
|
WithCommand(coursesGet)
|
||||||
|
|
||||||
|
return cli.NewCommand("ydb", "YDB related actions").
|
||||||
|
WithCommand(migration).
|
||||||
|
WithCommand(courses)
|
||||||
|
}
|
||||||
|
|
||||||
|
type ydbMigrateApplyAction struct {
|
||||||
|
*baseAction
|
||||||
|
}
|
||||||
|
|
||||||
|
func newYDBMigrateApplyAction(ctx context.Context) cli.Action {
|
||||||
|
action := &ydbMigrateApplyAction{
|
||||||
|
baseAction: newBaseAction(ctx),
|
||||||
|
}
|
||||||
|
|
||||||
|
return asCLIAction(action)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *ydbMigrateApplyAction) handle() error {
|
||||||
|
ydbConn, err := a.getYDBConnection()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
defer func() {
|
||||||
|
errClose := ydbConn.Close()
|
||||||
|
if errClose != nil {
|
||||||
|
xcontext.LogError(a.ctx, a.log, "unable to close repository", slog.Any("error", err))
|
||||||
|
if err == nil {
|
||||||
|
err = errClose
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
repository := ydbConn.CourseRepository()
|
||||||
|
err = repository.CreateCourseTable(a.ctx)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("creating course table: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
type ydbCoursesGetAction struct {
|
||||||
|
*baseAction
|
||||||
|
|
||||||
|
courseIDs []string
|
||||||
|
}
|
||||||
|
|
||||||
|
func newYDBCoursesGetAction(ctx context.Context) cli.Action {
|
||||||
|
action := &ydbCoursesGetAction{
|
||||||
|
baseAction: newBaseAction(ctx),
|
||||||
|
}
|
||||||
|
|
||||||
|
return asCLIAction(action)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *ydbCoursesGetAction) parse(params []string, options map[string]string) error {
|
||||||
|
err := a.baseAction.parse(params, options)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(params) == 0 {
|
||||||
|
return errors.NewValidationError("params", "no course ids provided")
|
||||||
|
}
|
||||||
|
|
||||||
|
a.courseIDs = make([]string, len(params))
|
||||||
|
copy(a.courseIDs, params)
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *ydbCoursesGetAction) handle() error {
|
||||||
|
ydbConn, err := a.getYDBConnection()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
defer func() {
|
||||||
|
errClose := ydbConn.Close()
|
||||||
|
if errClose != nil {
|
||||||
|
xcontext.LogError(a.ctx, a.log, "unable to close repository", slog.Any("error", err))
|
||||||
|
if err == nil {
|
||||||
|
err = errClose
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
repository := ydbConn.CourseRepository()
|
||||||
|
for _, courseID := range a.courseIDs {
|
||||||
|
course, err := repository.Get(a.ctx, courseID)
|
||||||
|
if err != nil && !stderrors.Is(err, errors.ErrNotFound) {
|
||||||
|
return fmt.Errorf("creating course table: %w", err)
|
||||||
|
} else if stderrors.Is(err, errors.ErrNotFound) {
|
||||||
|
xcontext.LogWarn(a.ctx, a.log, "course not found", slog.String("id", courseID))
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
xcontext.LogInfo(a.ctx, a.log, "fetched course", slog.Any("item", course))
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
18
go.mod
18
go.mod
@ -11,7 +11,25 @@ require (
|
|||||||
|
|
||||||
require (
|
require (
|
||||||
github.com/davecgh/go-spew v1.1.1 // indirect
|
github.com/davecgh/go-spew v1.1.1 // indirect
|
||||||
|
github.com/golang-jwt/jwt/v4 v4.5.0 // indirect
|
||||||
|
github.com/golang/protobuf v1.5.3 // indirect
|
||||||
|
github.com/google/uuid v1.4.0 // indirect
|
||||||
|
github.com/jonboulle/clockwork v0.4.0 // indirect
|
||||||
github.com/pmezard/go-difflib v1.0.0 // indirect
|
github.com/pmezard/go-difflib v1.0.0 // indirect
|
||||||
|
github.com/robfig/cron/v3 v3.0.0 // indirect
|
||||||
github.com/stretchr/objx v0.5.0 // indirect
|
github.com/stretchr/objx v0.5.0 // 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-sdk/v3 v3.54.2 // indirect
|
||||||
|
github.com/ydb-platform/ydb-go-yc v0.12.1 // indirect
|
||||||
|
github.com/ydb-platform/ydb-go-yc-metadata v0.6.1 // indirect
|
||||||
|
golang.org/x/sync v0.5.0 // indirect
|
||||||
|
golang.org/x/sys 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/googleapis/api v0.0.0-20231120223509-83a465c0220f // indirect
|
||||||
|
google.golang.org/genproto/googleapis/rpc v0.0.0-20231120223509-83a465c0220f // indirect
|
||||||
|
google.golang.org/grpc v1.59.0 // indirect
|
||||||
|
google.golang.org/protobuf v1.31.0 // indirect
|
||||||
gopkg.in/yaml.v3 v3.0.1 // indirect
|
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||||
)
|
)
|
||||||
|
|||||||
24
internal/common/config/duration.go
Normal file
24
internal/common/config/duration.go
Normal file
@ -0,0 +1,24 @@
|
|||||||
|
package config
|
||||||
|
|
||||||
|
import "time"
|
||||||
|
|
||||||
|
type Duration time.Duration
|
||||||
|
|
||||||
|
func (d *Duration) UnmarshalJSON(data []byte) error {
|
||||||
|
if len(data) == 0 {
|
||||||
|
*d = 0
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
duration, err := time.ParseDuration(string(data))
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
*d = Duration(duration)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d Duration) Std() time.Duration {
|
||||||
|
return time.Duration(d)
|
||||||
|
}
|
||||||
@ -1,8 +1,11 @@
|
|||||||
package config
|
package config
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"bytes"
|
||||||
"log/slog"
|
"log/slog"
|
||||||
"os"
|
"os"
|
||||||
|
|
||||||
|
"git.loyso.art/frx/kurious/internal/common/errors"
|
||||||
)
|
)
|
||||||
|
|
||||||
type LogFormat uint8
|
type LogFormat uint8
|
||||||
@ -12,6 +15,19 @@ const (
|
|||||||
LogFormatJSON
|
LogFormatJSON
|
||||||
)
|
)
|
||||||
|
|
||||||
|
func (f *LogFormat) UnmarshalText(data []byte) error {
|
||||||
|
switch format := string(bytes.ToLower(data)); format {
|
||||||
|
case "json":
|
||||||
|
*f = LogFormatJSON
|
||||||
|
case "text":
|
||||||
|
*f = LogFormatText
|
||||||
|
default:
|
||||||
|
return errors.NewValidationError("format", "unsupported value "+format)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
type LogLevel uint8
|
type LogLevel uint8
|
||||||
|
|
||||||
const (
|
const (
|
||||||
@ -21,12 +37,29 @@ const (
|
|||||||
LogLevelError
|
LogLevelError
|
||||||
)
|
)
|
||||||
|
|
||||||
type LogConfig struct {
|
func (lvl *LogLevel) UnmarshalText(data []byte) error {
|
||||||
Level LogLevel
|
switch level := string(bytes.ToLower(data)); level {
|
||||||
Format LogFormat
|
case "debug", "":
|
||||||
|
*lvl = LogLevelDebug
|
||||||
|
case "info":
|
||||||
|
*lvl = LogLevelInfo
|
||||||
|
case "warn":
|
||||||
|
*lvl = LogLevelWarn
|
||||||
|
case "error":
|
||||||
|
*lvl = LogLevelError
|
||||||
|
default:
|
||||||
|
return errors.NewValidationError("level", "unsupported value "+level)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewSLogger(config LogConfig) *slog.Logger {
|
type Log struct {
|
||||||
|
Level LogLevel `json:"level"`
|
||||||
|
Format LogFormat `json:"format"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewSLogger(config Log) *slog.Logger {
|
||||||
var level slog.Level
|
var level slog.Level
|
||||||
switch config.Level {
|
switch config.Level {
|
||||||
case LogLevelDebug:
|
case LogLevelDebug:
|
||||||
|
|||||||
63
internal/common/config/ydb.go
Normal file
63
internal/common/config/ydb.go
Normal file
@ -0,0 +1,63 @@
|
|||||||
|
package config
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"git.loyso.art/frx/kurious/internal/common/errors"
|
||||||
|
)
|
||||||
|
|
||||||
|
type YCAuth interface {
|
||||||
|
isYCAuth()
|
||||||
|
}
|
||||||
|
|
||||||
|
type YCAuthCAKeysFile struct{ Path string }
|
||||||
|
|
||||||
|
func (YCAuthCAKeysFile) isYCAuth() {}
|
||||||
|
|
||||||
|
type YCAuthIAMToken struct{ Token string }
|
||||||
|
|
||||||
|
func (YCAuthIAMToken) isYCAuth() {}
|
||||||
|
|
||||||
|
type YCAuthNone struct{}
|
||||||
|
|
||||||
|
func (YCAuthNone) isYCAuth() {}
|
||||||
|
|
||||||
|
type YDB struct {
|
||||||
|
DSN string
|
||||||
|
Auth YCAuth
|
||||||
|
ShutdownDuration time.Duration
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ydb *YDB) UnmarshalJSON(data []byte) error {
|
||||||
|
type ydbConfig struct {
|
||||||
|
DSN string `json:"dsn"`
|
||||||
|
CAKeysFile *string `json:"ca_keys_file_path"`
|
||||||
|
StaticIAMToken *string `json:"static_iam_token"`
|
||||||
|
ShutdownDuration Duration `json:"duration"`
|
||||||
|
}
|
||||||
|
|
||||||
|
var imcfg ydbConfig
|
||||||
|
err := json.Unmarshal(data, &imcfg)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
ydb.DSN = imcfg.DSN
|
||||||
|
ydb.ShutdownDuration = imcfg.ShutdownDuration.Std()
|
||||||
|
if imcfg.CAKeysFile != nil && imcfg.StaticIAMToken != nil {
|
||||||
|
return errors.NewValidationError("ca_keys_file_path", "could not be set together with static_iam_token field")
|
||||||
|
} else if imcfg.CAKeysFile != nil {
|
||||||
|
ydb.Auth = YCAuthCAKeysFile{
|
||||||
|
Path: *imcfg.CAKeysFile,
|
||||||
|
}
|
||||||
|
} else if imcfg.StaticIAMToken != nil {
|
||||||
|
ydb.Auth = YCAuthIAMToken{
|
||||||
|
Token: *imcfg.StaticIAMToken,
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
ydb.Auth = YCAuthNone{}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
@ -5,6 +5,7 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
|
ErrNotFound SimpleError = "not found"
|
||||||
ErrNotImplemented SimpleError = "not implemented"
|
ErrNotImplemented SimpleError = "not implemented"
|
||||||
ErrUnexpectedStatus SimpleError = "unexpected status"
|
ErrUnexpectedStatus SimpleError = "unexpected status"
|
||||||
)
|
)
|
||||||
|
|||||||
32
internal/common/xlog/cronlogger.go
Normal file
32
internal/common/xlog/cronlogger.go
Normal file
@ -0,0 +1,32 @@
|
|||||||
|
package xlog
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"log/slog"
|
||||||
|
)
|
||||||
|
|
||||||
|
type cronlogger struct {
|
||||||
|
basectx context.Context
|
||||||
|
log *slog.Logger
|
||||||
|
}
|
||||||
|
|
||||||
|
func WrapSLogger(ctx context.Context, log *slog.Logger) cronlogger {
|
||||||
|
return cronlogger{
|
||||||
|
basectx: ctx,
|
||||||
|
log: log,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (l cronlogger) Info(msg string, keysAndValues ...any) {
|
||||||
|
attrs := mapKeysAndValues(keysAndValues...)
|
||||||
|
l.log.LogAttrs(l.basectx, slog.LevelInfo, msg, attrs...)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (l cronlogger) Error(err error, msg string, keysAndValues ...any) {
|
||||||
|
attrs := append(mapKeysAndValues(keysAndValues...), slog.Any("err", err))
|
||||||
|
l.log.LogAttrs(l.basectx, slog.LevelError, msg, attrs...)
|
||||||
|
}
|
||||||
|
|
||||||
|
func mapKeysAndValues(keysAndValues ...any) []slog.Attr {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
@ -2,31 +2,279 @@ package adapters
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"log/slog"
|
||||||
|
"path"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"git.loyso.art/frx/kurious/internal/common/config"
|
||||||
|
"git.loyso.art/frx/kurious/internal/common/errors"
|
||||||
|
"git.loyso.art/frx/kurious/internal/common/nullable"
|
||||||
|
"git.loyso.art/frx/kurious/internal/common/xcontext"
|
||||||
"git.loyso.art/frx/kurious/internal/kurious/domain"
|
"git.loyso.art/frx/kurious/internal/kurious/domain"
|
||||||
|
"git.loyso.art/frx/kurious/pkg/xdefault"
|
||||||
|
|
||||||
|
"github.com/ydb-platform/ydb-go-sdk/v3"
|
||||||
|
"github.com/ydb-platform/ydb-go-sdk/v3/table"
|
||||||
|
"github.com/ydb-platform/ydb-go-sdk/v3/table/options"
|
||||||
|
"github.com/ydb-platform/ydb-go-sdk/v3/table/result/named"
|
||||||
|
"github.com/ydb-platform/ydb-go-sdk/v3/table/types"
|
||||||
|
yc "github.com/ydb-platform/ydb-go-yc"
|
||||||
)
|
)
|
||||||
|
|
||||||
func NewYDBCourseRepository() (*ydbCourseRepository, error) {
|
const (
|
||||||
return &ydbCourseRepository{}, nil
|
defaultShutdownTimeout = time.Second * 10
|
||||||
|
)
|
||||||
|
|
||||||
|
type YDBConnection struct {
|
||||||
|
*ydb.Driver
|
||||||
|
|
||||||
|
log *slog.Logger
|
||||||
|
shutdownTimeout time.Duration
|
||||||
}
|
}
|
||||||
|
|
||||||
type ydbCourseRepository struct{}
|
func NewYDBConnection(ctx context.Context, cfg config.YDB, log *slog.Logger) (*YDBConnection, error) {
|
||||||
|
opts := make([]ydb.Option, 0, 2)
|
||||||
|
switch auth := cfg.Auth.(type) {
|
||||||
|
case config.YCAuthIAMToken:
|
||||||
|
opts = append(opts, ydb.WithAccessTokenCredentials(auth.Token))
|
||||||
|
case config.YCAuthCAKeysFile:
|
||||||
|
opts = append(opts,
|
||||||
|
yc.WithInternalCA(),
|
||||||
|
yc.WithServiceAccountKeyFileCredentials(auth.Path),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
db, err := ydb.Open(
|
||||||
|
ctx,
|
||||||
|
cfg.DSN,
|
||||||
|
opts...,
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("opening connection: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
func (ydbCourseRepository) List(ctx context.Context, params domain.ListCoursesParams) ([]domain.Course, error) {
|
return &YDBConnection{
|
||||||
|
Driver: db,
|
||||||
|
shutdownTimeout: xdefault.WithFallback(cfg.ShutdownDuration, defaultShutdownTimeout),
|
||||||
|
log: log,
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (conn *YDBConnection) Close() error {
|
||||||
|
ctx, cancel := context.WithTimeout(context.Background(), conn.shutdownTimeout)
|
||||||
|
defer cancel()
|
||||||
|
|
||||||
|
return conn.Driver.Close(ctx)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (conn *YDBConnection) CourseRepository() *ydbCourseRepository {
|
||||||
|
return &ydbCourseRepository{
|
||||||
|
db: conn.Driver,
|
||||||
|
log: conn.log.With(slog.String("repository", "course")),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
type ydbCourseRepository struct {
|
||||||
|
db *ydb.Driver
|
||||||
|
log *slog.Logger
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *ydbCourseRepository) List(ctx context.Context, params domain.ListCoursesParams) ([]domain.Course, error) {
|
||||||
return nil, nil
|
return nil, nil
|
||||||
}
|
}
|
||||||
func (ydbCourseRepository) Get(ctx context.Context, id string) (domain.Course, error) {
|
|
||||||
|
func (r *ydbCourseRepository) Get(ctx context.Context, id string) (course domain.Course, err error) {
|
||||||
|
const queryName = "get"
|
||||||
|
|
||||||
|
courses := make([]domain.Course, 0, 1)
|
||||||
|
readTx := table.TxControl(
|
||||||
|
table.BeginTx(
|
||||||
|
table.WithOnlineReadOnly(),
|
||||||
|
),
|
||||||
|
table.CommitTx(),
|
||||||
|
)
|
||||||
|
err = r.db.Table().Do(
|
||||||
|
ctx,
|
||||||
|
func(ctx context.Context, s table.Session) error {
|
||||||
|
start := time.Now()
|
||||||
|
defer func() {
|
||||||
|
since := time.Since(start)
|
||||||
|
xcontext.LogInfo(
|
||||||
|
ctx, r.log,
|
||||||
|
"executed query",
|
||||||
|
slog.String("name", queryName),
|
||||||
|
slog.Duration("elapsed", since),
|
||||||
|
)
|
||||||
|
}()
|
||||||
|
|
||||||
|
_, res, err := s.Execute(
|
||||||
|
ctx,
|
||||||
|
readTx,
|
||||||
|
`
|
||||||
|
DECLARE $id AS Text;
|
||||||
|
SELECT
|
||||||
|
id,
|
||||||
|
external_id,
|
||||||
|
source_type,
|
||||||
|
source_name,
|
||||||
|
organization_id,
|
||||||
|
origin_link,
|
||||||
|
image_link,
|
||||||
|
name,
|
||||||
|
description,
|
||||||
|
full_price,
|
||||||
|
discount,
|
||||||
|
duration,
|
||||||
|
starts_at,
|
||||||
|
created_at,
|
||||||
|
updated_at,
|
||||||
|
deleted_at,
|
||||||
|
FROM
|
||||||
|
courses
|
||||||
|
WHERE
|
||||||
|
id = $id;
|
||||||
|
`,
|
||||||
|
table.NewQueryParameters(
|
||||||
|
table.ValueParam("$id", types.TextValue(id)),
|
||||||
|
),
|
||||||
|
options.WithCollectStatsModeBasic(),
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("executing: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
for res.NextResultSet(ctx) {
|
||||||
|
for res.NextRow() {
|
||||||
|
var cdb courseDB
|
||||||
|
_ = res.ScanNamed(cdb.getNamedValues()...)
|
||||||
|
courses = append(courses, mapCourseDB(cdb))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if err = res.Err(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
},
|
||||||
|
table.WithIdempotent())
|
||||||
|
if err != nil {
|
||||||
|
return domain.Course{}, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(courses) == 0 {
|
||||||
|
return course, errors.ErrNotFound
|
||||||
|
}
|
||||||
|
|
||||||
|
return courses[0], err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *ydbCourseRepository) GetByExternalID(ctx context.Context, id string) (domain.Course, error) {
|
||||||
return domain.Course{}, nil
|
return domain.Course{}, nil
|
||||||
}
|
}
|
||||||
func (ydbCourseRepository) GetByExternalID(ctx context.Context, id string) (domain.Course, error) {
|
|
||||||
|
func (r *ydbCourseRepository) Create(context.Context, domain.CreateCourseParams) (domain.Course, error) {
|
||||||
return domain.Course{}, nil
|
return domain.Course{}, nil
|
||||||
}
|
}
|
||||||
func (ydbCourseRepository) Create(context.Context, domain.CreateCourseParams) (domain.Course, error) {
|
|
||||||
return domain.Course{}, nil
|
func (r *ydbCourseRepository) Delete(ctx context.Context, id string) error {
|
||||||
}
|
|
||||||
func (ydbCourseRepository) Delete(ctx context.Context, id string) error {
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
func (ydbCourseRepository) Close() error {
|
|
||||||
return nil
|
func (r *ydbCourseRepository) CreateCourseTable(ctx context.Context) error {
|
||||||
|
return r.db.Table().Do(ctx, func(ctx context.Context, s table.Session) error {
|
||||||
|
return s.CreateTable(
|
||||||
|
ctx,
|
||||||
|
path.Join(r.db.Name(), "courses"),
|
||||||
|
options.WithColumn("id", types.TypeString),
|
||||||
|
options.WithColumn("external_id", types.Optional(types.TypeText)),
|
||||||
|
options.WithColumn("name", types.TypeText),
|
||||||
|
options.WithColumn("source_type", types.TypeText),
|
||||||
|
options.WithColumn("source_name", types.Optional(types.TypeText)),
|
||||||
|
options.WithColumn("organization_id", types.TypeText),
|
||||||
|
options.WithColumn("origin_link", types.TypeText),
|
||||||
|
options.WithColumn("image_link", types.TypeText),
|
||||||
|
options.WithColumn("description", types.TypeText),
|
||||||
|
options.WithColumn("full_price", types.TypeFloat),
|
||||||
|
options.WithColumn("discount", types.TypeFloat),
|
||||||
|
options.WithColumn("duration", types.TypeInterval),
|
||||||
|
options.WithColumn("starts_at", types.TypeDatetime),
|
||||||
|
options.WithColumn("created_at", types.TypeDatetime),
|
||||||
|
options.WithColumn("updated_at", types.TypeDatetime),
|
||||||
|
options.WithColumn("deleted_at", types.Optional(types.TypeDatetime)),
|
||||||
|
options.WithPrimaryKeyColumn("id"),
|
||||||
|
)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
type courseDB struct {
|
||||||
|
ID string
|
||||||
|
ExternalID *string
|
||||||
|
Name string
|
||||||
|
SourceType string
|
||||||
|
SourceName *string
|
||||||
|
OrganizationID string
|
||||||
|
OriginLink string
|
||||||
|
ImageLink string
|
||||||
|
Description string
|
||||||
|
FullPrice float64
|
||||||
|
Discount float64
|
||||||
|
Duration time.Duration
|
||||||
|
StartAt time.Time
|
||||||
|
CreatedAt time.Time
|
||||||
|
UpdatedAt time.Time
|
||||||
|
DeletedAt *time.Time
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *courseDB) getNamedValues() []named.Value {
|
||||||
|
return []named.Value{
|
||||||
|
named.Required("id", &c.ID),
|
||||||
|
named.Optional("external_id", &c.ExternalID),
|
||||||
|
named.Required("source_type", &c.SourceType),
|
||||||
|
named.Optional("source_name", &c.SourceName),
|
||||||
|
named.Required("organization_id", &c.OrganizationID),
|
||||||
|
named.Required("origin_link", &c.OriginLink),
|
||||||
|
named.Required("image_link", &c.ImageLink),
|
||||||
|
named.Required("description", &c.Description),
|
||||||
|
named.Required("duration", &c.Duration),
|
||||||
|
named.Required("starts_at", &c.StartAt),
|
||||||
|
named.Required("created_at", &c.CreatedAt),
|
||||||
|
named.Required("updated_at", &c.UpdatedAt),
|
||||||
|
named.Optional("deleted_at", &c.DeletedAt),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const (
|
||||||
|
sourceTypeUnknown = ""
|
||||||
|
sourceTypeManual = "m"
|
||||||
|
sourceTypeParsed = "p"
|
||||||
|
)
|
||||||
|
|
||||||
|
func mapCourseDB(cdb courseDB) domain.Course {
|
||||||
|
var st domain.SourceType
|
||||||
|
switch cdb.SourceType {
|
||||||
|
case sourceTypeUnknown:
|
||||||
|
st = domain.SourceTypeUnset
|
||||||
|
case sourceTypeManual:
|
||||||
|
st = domain.SourceTypeManual
|
||||||
|
case sourceTypeParsed:
|
||||||
|
st = domain.SourceTypeParsed
|
||||||
|
}
|
||||||
|
|
||||||
|
return domain.Course{
|
||||||
|
ID: cdb.ID,
|
||||||
|
ExternalID: nullable.NewValuePtr(cdb.ExternalID),
|
||||||
|
Name: cdb.Name,
|
||||||
|
SourceType: st,
|
||||||
|
SourceName: nullable.NewValuePtr(cdb.SourceName),
|
||||||
|
OrganizationID: cdb.OrganizationID,
|
||||||
|
OriginLink: cdb.OriginLink,
|
||||||
|
ImageLink: cdb.ImageLink,
|
||||||
|
Description: cdb.Description,
|
||||||
|
FullPrice: cdb.FullPrice,
|
||||||
|
Discount: cdb.Discount,
|
||||||
|
Duration: cdb.Duration,
|
||||||
|
StartsAt: cdb.StartAt,
|
||||||
|
CreatedAt: cdb.CreatedAt,
|
||||||
|
UpdatedAt: cdb.UpdatedAt,
|
||||||
|
DeletedAt: nullable.NewValuePtr(cdb.DeletedAt),
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -28,7 +28,6 @@ type Course struct {
|
|||||||
FullPrice float64
|
FullPrice float64
|
||||||
// Discount for the course.
|
// Discount for the course.
|
||||||
Discount float64
|
Discount float64
|
||||||
Keywords []string
|
|
||||||
|
|
||||||
// Duration for the course. It will be splitted in values like:
|
// Duration for the course. It will be splitted in values like:
|
||||||
// full month / full day / full hour.
|
// full month / full day / full hour.
|
||||||
|
|||||||
43
internal/kurious/ports/cron.go
Normal file
43
internal/kurious/ports/cron.go
Normal file
@ -0,0 +1,43 @@
|
|||||||
|
package ports
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"log/slog"
|
||||||
|
|
||||||
|
"git.loyso.art/frx/kurious/internal/common/xlog"
|
||||||
|
"git.loyso.art/frx/kurious/internal/kurious/service"
|
||||||
|
|
||||||
|
"github.com/robfig/cron/v3"
|
||||||
|
)
|
||||||
|
|
||||||
|
type BackgroundParser struct {
|
||||||
|
scheduler *cron.Cron
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewBackgroundParser(ctx context.Context, svc service.Application, log *slog.Logger) *BackgroundParser {
|
||||||
|
clog := xlog.WrapSLogger(ctx, log)
|
||||||
|
scheduler := cron.New(cron.WithSeconds(), cron.WithChain(
|
||||||
|
cron.Recover(clog),
|
||||||
|
))
|
||||||
|
|
||||||
|
bp := &BackgroundParser{
|
||||||
|
scheduler: scheduler,
|
||||||
|
}
|
||||||
|
|
||||||
|
return bp
|
||||||
|
}
|
||||||
|
|
||||||
|
func (bp *BackgroundParser) Run() {
|
||||||
|
bp.scheduler.Run()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (bp *BackgroundParser) Shutdown(ctx context.Context) error {
|
||||||
|
sdctx := bp.scheduler.Stop()
|
||||||
|
|
||||||
|
select {
|
||||||
|
case <-ctx.Done():
|
||||||
|
return ctx.Err()
|
||||||
|
case <-sdctx.Done():
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -14,7 +14,8 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
type ApplicationConfig struct {
|
type ApplicationConfig struct {
|
||||||
LogConfig config.LogConfig
|
LogConfig config.Log
|
||||||
|
YDB config.YDB
|
||||||
}
|
}
|
||||||
|
|
||||||
type Application struct {
|
type Application struct {
|
||||||
@ -26,11 +27,13 @@ type Application struct {
|
|||||||
|
|
||||||
func NewApplication(ctx context.Context, cfg ApplicationConfig) (Application, error) {
|
func NewApplication(ctx context.Context, cfg ApplicationConfig) (Application, error) {
|
||||||
log := config.NewSLogger(cfg.LogConfig)
|
log := config.NewSLogger(cfg.LogConfig)
|
||||||
courseadapter, err := adapters.NewYDBCourseRepository()
|
ydbConnection, err := adapters.NewYDBConnection(ctx, cfg.YDB, log.With(slog.String("db", "ydb")))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return Application{}, fmt.Errorf("making ydb course repository: %w", err)
|
return Application{}, fmt.Errorf("making ydb connection: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
courseadapter := ydbConnection.CourseRepository()
|
||||||
|
|
||||||
application := app.Application{
|
application := app.Application{
|
||||||
Commands: app.Commands{
|
Commands: app.Commands{
|
||||||
InsertCourse: command.NewCreateCourseHandler(courseadapter, log),
|
InsertCourse: command.NewCreateCourseHandler(courseadapter, log),
|
||||||
@ -43,7 +46,7 @@ func NewApplication(ctx context.Context, cfg ApplicationConfig) (Application, er
|
|||||||
}
|
}
|
||||||
|
|
||||||
out := Application{Application: application}
|
out := Application{Application: application}
|
||||||
out.closers = append(out.closers, courseadapter)
|
out.closers = append(out.closers, ydbConnection)
|
||||||
out.log = log
|
out.log = log
|
||||||
|
|
||||||
return out, nil
|
return out, nil
|
||||||
|
|||||||
11
pkg/xdefault/withdefault.go
Normal file
11
pkg/xdefault/withdefault.go
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
package xdefault
|
||||||
|
|
||||||
|
import "reflect"
|
||||||
|
|
||||||
|
func WithFallback[T comparable](value, fallback T) T {
|
||||||
|
if reflect.ValueOf(value).IsZero() {
|
||||||
|
return fallback
|
||||||
|
}
|
||||||
|
|
||||||
|
return value
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user