able to get product

This commit is contained in:
Gitea
2023-11-30 00:39:51 +03:00
parent 606b94e35b
commit 414dc87091
19 changed files with 2204 additions and 77 deletions

1
.gitignore vendored
View File

@ -1,2 +1,3 @@
*.json
bin
./tags

View File

@ -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

View 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"`
}

View File

@ -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, &currentConfig)
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,
}
}

View File

@ -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)

View File

@ -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
View 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
View File

@ -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
)

1418
go.sum

File diff suppressed because it is too large Load Diff

View 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)
}

View File

@ -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:

View 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
}

View File

@ -5,6 +5,7 @@ import (
)
const (
ErrNotFound SimpleError = "not found"
ErrNotImplemented SimpleError = "not implemented"
ErrUnexpectedStatus SimpleError = "unexpected status"
)

View 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
}

View File

@ -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),
}
}

View File

@ -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.

View 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
}
}

View File

@ -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

View 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
}