able to get product
This commit is contained in:
1
.gitignore
vendored
1
.gitignore
vendored
@ -1,2 +1,3 @@
|
||||
*.json
|
||||
bin
|
||||
./tags
|
||||
|
||||
@ -23,7 +23,7 @@ tasks:
|
||||
- "$GOBIN/golangci-lint run ./..."
|
||||
test:
|
||||
cmds:
|
||||
- go test --count=1 ./internal/...
|
||||
- go test ./internal/...
|
||||
build:
|
||||
cmds:
|
||||
- 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 (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"log/slog"
|
||||
"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"
|
||||
)
|
||||
@ -19,6 +23,75 @@ const (
|
||||
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)
|
||||
|
||||
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 {
|
||||
level := slog.LevelInfo
|
||||
if _, ok := options[debugOptName]; ok {
|
||||
@ -48,3 +121,41 @@ func makeSravniClient(ctx context.Context, log *slog.Logger, options map[string]
|
||||
|
||||
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"
|
||||
)
|
||||
|
||||
const (
|
||||
defaultConfigPath = "config_cli.json"
|
||||
)
|
||||
|
||||
var defaultOutput = os.Stdout
|
||||
var currentConfig = cliConfig{}
|
||||
|
||||
func main() {
|
||||
ctx, cancel := signal.NotifyContext(context.Background(), os.Interrupt)
|
||||
defer cancel()
|
||||
|
||||
log := slog.New(slog.NewJSONHandler(os.Stdout, &slog.HandlerOptions{
|
||||
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)
|
||||
ec, err := app(ctx)
|
||||
if err != nil {
|
||||
slog.ErrorContext(ctx, "unable to run app", slog.Any("error", err))
|
||||
}
|
||||
@ -76,17 +70,20 @@ func setupCLI(ctx context.Context) cli.App {
|
||||
WithCommand(mainPageState)
|
||||
|
||||
apiCategory := setupAPICommand(ctx)
|
||||
ydbCategory := setupYDBCommand(ctx)
|
||||
description := fmt.Sprintf("sravni dev cli %s (%s)", kurious.Version(), kurious.Commit())
|
||||
cliApp := cli.New(description).
|
||||
WithOption(cli.NewOption("verbose", "Verbose execution").WithChar('v').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(apiCategory)
|
||||
WithCommand(apiCategory).
|
||||
WithCommand(ydbCategory)
|
||||
|
||||
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)
|
||||
exitCode = devCLI.Run(os.Args, defaultOutput)
|
||||
|
||||
|
||||
@ -6,8 +6,8 @@ import (
|
||||
"log/slog"
|
||||
"strconv"
|
||||
|
||||
"git.loyso.art/frx/kurious/internal/domain"
|
||||
"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/errors"
|
||||
|
||||
"github.com/teris-io/cli"
|
||||
)
|
||||
@ -25,12 +25,14 @@ func setupAPICommand(ctx context.Context) cli.Command {
|
||||
WithChar('t').
|
||||
WithType(cli.TypeString)
|
||||
|
||||
apiEducationListProducts := cli.NewCommand("list_products", "List products by some filters").
|
||||
WithOption(learningTypeOpt).
|
||||
WithOption(courseThematic).
|
||||
WithOption(limitOption).
|
||||
WithOption(offsetOption).
|
||||
WithAction(newListProductAction(ctx))
|
||||
apiEducationListProducts := buildCLICommand(func() cli.Command {
|
||||
return cli.NewCommand("list_products", "List products by some filters").
|
||||
WithOption(learningTypeOpt).
|
||||
WithOption(courseThematic).
|
||||
WithOption(limitOption).
|
||||
WithOption(offsetOption).
|
||||
WithAction(newListProductAction(ctx))
|
||||
})
|
||||
|
||||
apiEducation := cli.NewCommand("education", "Education related category").
|
||||
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 {
|
||||
learningType string
|
||||
courseThematic string
|
||||
@ -104,6 +76,7 @@ type listProductsActionParams struct {
|
||||
type listProductsAction struct {
|
||||
*baseAction
|
||||
|
||||
client sravni.Client
|
||||
params listProductsActionParams
|
||||
}
|
||||
|
||||
@ -125,11 +98,11 @@ func (a *listProductsAction) parse(args []string, options map[string]string) err
|
||||
|
||||
a.params.learningType, ok = options[learningTypeOptName]
|
||||
if !ok {
|
||||
return domain.SimpleError("learning_type is empty")
|
||||
return errors.SimpleError("learning_type is empty")
|
||||
}
|
||||
a.params.courseThematic, ok = options[courseThematicOptName]
|
||||
if !ok {
|
||||
return domain.SimpleError("course_thematic is empty")
|
||||
return errors.SimpleError("course_thematic is empty")
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
|
||||
client, err := makeSravniClient(a.ctx, a.log, options)
|
||||
if err != nil {
|
||||
return fmt.Errorf("making sravni client: %w", err)
|
||||
}
|
||||
|
||||
a.client = client
|
||||
|
||||
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 (
|
||||
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/robfig/cron/v3 v3.0.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
|
||||
)
|
||||
|
||||
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
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"log/slog"
|
||||
"os"
|
||||
|
||||
"git.loyso.art/frx/kurious/internal/common/errors"
|
||||
)
|
||||
|
||||
type LogFormat uint8
|
||||
@ -12,6 +15,19 @@ const (
|
||||
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
|
||||
|
||||
const (
|
||||
@ -21,12 +37,29 @@ const (
|
||||
LogLevelError
|
||||
)
|
||||
|
||||
type LogConfig struct {
|
||||
Level LogLevel
|
||||
Format LogFormat
|
||||
func (lvl *LogLevel) UnmarshalText(data []byte) error {
|
||||
switch level := string(bytes.ToLower(data)); level {
|
||||
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
|
||||
switch config.Level {
|
||||
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 (
|
||||
ErrNotFound SimpleError = "not found"
|
||||
ErrNotImplemented SimpleError = "not implemented"
|
||||
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 (
|
||||
"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/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) {
|
||||
return &ydbCourseRepository{}, nil
|
||||
const (
|
||||
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
|
||||
}
|
||||
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
|
||||
}
|
||||
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
|
||||
}
|
||||
func (ydbCourseRepository) Create(context.Context, domain.CreateCourseParams) (domain.Course, error) {
|
||||
return domain.Course{}, nil
|
||||
}
|
||||
func (ydbCourseRepository) Delete(ctx context.Context, id string) error {
|
||||
|
||||
func (r *ydbCourseRepository) Delete(ctx context.Context, id string) error {
|
||||
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
|
||||
// Discount for the course.
|
||||
Discount float64
|
||||
Keywords []string
|
||||
|
||||
// Duration for the course. It will be splitted in values like:
|
||||
// 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 {
|
||||
LogConfig config.LogConfig
|
||||
LogConfig config.Log
|
||||
YDB config.YDB
|
||||
}
|
||||
|
||||
type Application struct {
|
||||
@ -26,11 +27,13 @@ type Application struct {
|
||||
|
||||
func NewApplication(ctx context.Context, cfg ApplicationConfig) (Application, error) {
|
||||
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 {
|
||||
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{
|
||||
Commands: app.Commands{
|
||||
InsertCourse: command.NewCreateCourseHandler(courseadapter, log),
|
||||
@ -43,7 +46,7 @@ func NewApplication(ctx context.Context, cfg ApplicationConfig) (Application, er
|
||||
}
|
||||
|
||||
out := Application{Application: application}
|
||||
out.closers = append(out.closers, courseadapter)
|
||||
out.closers = append(out.closers, ydbConnection)
|
||||
out.log = log
|
||||
|
||||
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