package main import ( "context" stderrors "errors" "fmt" "log/slog" "strconv" "time" "git.loyso.art/frx/kurious/internal/common/errors" "git.loyso.art/frx/kurious/internal/common/xcontext" "git.loyso.art/frx/kurious/internal/kurious/domain" "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().WithType(cli.TypeString)). WithAction(newYDBCoursesGetAction(ctx)) }) coursesCreate := buildCLICommand(func() cli.Command { return cli.NewCommand("create", "Creates course"). WithOption(cli.NewOption("generate-id", "Generates id").WithType(cli.TypeBool)). WithOption(cli.NewOption("name", "Sets course name")). WithOption(cli.NewOption("source-name", "Sets source name")). WithOption(cli.NewOption("organization-id", "Sets organization id")). WithOption(cli.NewOption("origin-link", "Sets origin link")). WithOption(cli.NewOption("image-link", "Sets image link")). WithOption(cli.NewOption("desc", "Sets description link")). WithOption(cli.NewOption("full-price", "Sets full price").WithType(cli.TypeNumber)). WithOption(cli.NewOption("discount", "Sets discount").WithType(cli.TypeNumber)). WithOption(cli.NewOption("duration", "Sets duration")). WithOption(cli.NewOption("starts-at", "Sets starts at")). WithAction(NewYDBCoursesCreateAction(ctx)) }) courses := cli.NewCommand("courses", "Courses commands"). WithCommand(coursesGet). WithCommand(coursesCreate) 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 } func NewYDBCoursesCreateAction(ctx context.Context) cli.Action { action := &ydbCoursesCreateAction{ baseAction: newBaseAction(ctx), } return asCLIAction(action) } type ydbCoursesCreateAction struct { *baseAction generateID bool id string name string sourceName string organizationID string originLink string imageLink string description string fullPrice float64 discount float64 duration time.Duration startsAt time.Time } func (a *ydbCoursesCreateAction) getCreateParams() domain.CreateCourseParams { if a.generateID { a.id = strconv.FormatInt(time.Now().Unix(), 10) } out := domain.CreateCourseParams{ ID: a.id, Name: a.name, SourceType: domain.SourceTypeParsed, OrganizationID: a.organizationID, OriginLink: a.originLink, ImageLink: a.imageLink, Description: a.description, FullPrice: a.fullPrice, Discount: a.discount, Duration: a.duration, StartsAt: a.startsAt, } if a.sourceName != "" { out.SourceName.Set(a.sourceName) } return out } func (a *ydbCoursesCreateAction) parse(params []string, options map[string]string) error { err := a.baseAction.parse(params, options) if err != nil { return err } if err != nil { return fmt.Errorf("parsing full-price: %w", err) } a.discount, err = strconv.ParseFloat(options["discount"], 64) if err != nil { return fmt.Errorf("parsing discount: %w", err) } a.fullPrice, err = strconv.ParseFloat(options["full-price"], 64) if err != nil { return fmt.Errorf("parsing full-price: %w", err) } if genid, ok := options["generate-id"]; ok { a.generateID, err = strconv.ParseBool(genid) if err != nil { return fmt.Errorf("parsing generate-id: %w", err) } } if duration, ok := options["duration"]; ok { a.duration, err = time.ParseDuration(duration) if err != nil { return fmt.Errorf("parsing duration: %w", err) } } if startsAt, ok := options["starts-at"]; ok { a.startsAt, err = time.Parse(time.RFC3339, startsAt) if err != nil { return fmt.Errorf("parsing time: %w", err) } } a.name = options["name"] a.sourceName = options["source-name"] a.organizationID = options["organization-id"] a.originLink = options["origin-link"] a.imageLink = options["image-link"] a.description = options["desc"] return nil } func (a *ydbCoursesCreateAction) 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 } } }() params := a.getCreateParams() repository := ydbConn.CourseRepository() _, err = repository.Create(a.ctx, params) if err != nil { return fmt.Errorf("creating course: %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() courses := make([]domain.Course, 0, len(a.courseIDs)) 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.String("id", courseID)) courses = append(courses, course) } for _, course := range courses { err = a.out(course) if err != nil { xcontext.LogError(a.ctx, a.log, "unable to write course", slog.Any("error", err)) } } return nil }