create product at ydb

This commit is contained in:
Gitea
2023-12-04 01:24:24 +03:00
parent 414dc87091
commit 20107503e0
5 changed files with 300 additions and 25 deletions

View File

@ -15,14 +15,13 @@ import (
"github.com/teris-io/cli"
)
const (
debugOptName = "verbose"
jsonOptName = "json"
var (
limitOption = cli.NewOption("limit", "Limits amount of items to return").WithType(cli.TypeInt)
offsetOption = cli.NewOption("offset", "Offsets items to return").WithType(cli.TypeInt)
debugOption = cli.NewOption("verbose", "Enables debug logging").WithChar('v').WithType(cli.TypeBool)
jsonOption = cli.NewOption("json", "Sets output as json").WithType(cli.TypeBool)
)
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 {
@ -92,9 +91,29 @@ func actionWrapperParseConfig(next cli.Action) cli.Action {
}
}
func isJSONFormatEnabled(options map[string]string) bool {
_, ok := options[jsonOption.Key()]
return ok
}
type outputEncoderF func(any) error
func makeOutputEncoder(options map[string]string) outputEncoderF {
if isJSONFormatEnabled(options) {
out := json.NewEncoder(defaultOutput)
out.SetIndent("", " ")
return out.Encode
}
return outputEncoderF(func(a any) error {
_, err := fmt.Fprintf(defaultOutput, "%#v", a)
return err
})
}
func makeLogger(options map[string]string) *slog.Logger {
level := slog.LevelInfo
if _, ok := options[debugOptName]; ok {
if _, ok := options[debugOption.Key()]; ok {
level = slog.LevelDebug
}
@ -103,7 +122,7 @@ func makeLogger(options map[string]string) *slog.Logger {
}
var h slog.Handler
if _, ok := options[jsonOptName]; ok {
if isJSONFormatEnabled(options) {
h = slog.NewJSONHandler(os.Stdout, &opts)
} else {
h = slog.NewTextHandler(os.Stdout, &opts)
@ -113,7 +132,7 @@ func makeLogger(options map[string]string) *slog.Logger {
}
func makeSravniClient(ctx context.Context, log *slog.Logger, options map[string]string) (sravni.Client, error) {
_, isDebug := options[debugOptName]
_, isDebug := options[debugOption.Key()]
client, err := sravni.NewClient(ctx, log, isDebug)
if err != nil {
return nil, fmt.Errorf("making new client: %w", err)
@ -125,6 +144,8 @@ func makeSravniClient(ctx context.Context, log *slog.Logger, options map[string]
type baseAction struct {
ctx context.Context
log *slog.Logger
out outputEncoderF
}
func (ba *baseAction) getYDBConnection() (*adapters.YDBConnection, error) {
@ -142,6 +163,7 @@ func (ba *baseAction) getYDBConnection() (*adapters.YDBConnection, error) {
func (ba *baseAction) parse(_ []string, options map[string]string) (err error) {
ba.log = makeLogger(options).With(slog.String("component", "action"))
ba.out = makeOutputEncoder(options)
return nil
}

View File

@ -5,9 +5,12 @@ import (
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"
)
@ -22,12 +25,29 @@ func setupYDBCommand(ctx context.Context) cli.Command {
coursesGet := buildCLICommand(func() cli.Command {
return cli.NewCommand("get", "Fetches one or more courses").
WithArg(cli.NewArg("ids", "List of course ids").AsOptional()).
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(coursesGet).
WithCommand(coursesCreate)
return cli.NewCommand("ydb", "YDB related actions").
WithCommand(migration).
@ -70,6 +90,127 @@ func (a *ydbMigrateApplyAction) handle() error {
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
@ -116,6 +257,7 @@ func (a *ydbCoursesGetAction) handle() error {
}()
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) {
@ -125,7 +267,15 @@ func (a *ydbCoursesGetAction) handle() error {
continue
}
xcontext.LogInfo(a.ctx, a.log, "fetched course", slog.Any("item", course))
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

View File

@ -128,7 +128,7 @@ func (r *ydbCourseRepository) Get(ctx context.Context, id string) (course domain
starts_at,
created_at,
updated_at,
deleted_at,
deleted_at
FROM
courses
WHERE
@ -171,8 +171,89 @@ func (r *ydbCourseRepository) GetByExternalID(ctx context.Context, id string) (d
return domain.Course{}, nil
}
func (r *ydbCourseRepository) Create(context.Context, domain.CreateCourseParams) (domain.Course, error) {
return domain.Course{}, nil
func createCourseParamsAsStruct(params domain.CreateCourseParams) types.Value {
st := mapSourceTypeFromDomain(params.SourceType)
now := time.Now()
return types.StructValue(
types.StructFieldValue("id", types.TextValue(params.ID)),
types.StructFieldValue("name", types.TextValue(params.Name)),
types.StructFieldValue("source_type", types.TextValue(st)),
types.StructFieldValue("source_name", types.NullableTextValue(params.SourceName.ValutPtr())),
types.StructFieldValue("external_id", types.NullableTextValue(params.ExternalID.ValutPtr())),
types.StructFieldValue("organization_id", types.TextValue(params.OrganizationID)),
types.StructFieldValue("origin_link", types.TextValue(params.OriginLink)),
types.StructFieldValue("image_link", types.TextValue(params.ImageLink)),
types.StructFieldValue("description", types.TextValue(params.Description)),
types.StructFieldValue("full_price", types.DoubleValue(params.FullPrice)),
types.StructFieldValue("discount", types.DoubleValue(params.Discount)),
types.StructFieldValue("duration", types.IntervalValueFromDuration(params.Duration)),
types.StructFieldValue("starts_at", types.DatetimeValueFromTime(params.StartsAt)),
types.StructFieldValue("created_at", types.DatetimeValueFromTime(now)),
types.StructFieldValue("updated_at", types.DatetimeValueFromTime(now)),
types.StructFieldValue("deleted_at", types.NullableDatetimeValue(nil)),
)
}
func (r *ydbCourseRepository) Create(ctx context.Context, params domain.CreateCourseParams) (domain.Course, error) {
// -- PRAGMA TablePathPrefix("courses");
const upsertQuery = `DECLARE $courseData AS List<Struct<
id: Text,
external_id: Optional<Text>,
name: Text,
source_type: Text,
source_name: Optional<Text>,
organization_id: Text,
origin_link: Text,
image_link: Text,
description: Text,
full_price: Double,
discount: Double,
duration: Interval,
starts_at: Datetime,
created_at: Datetime,
updated_at: Datetime,
deleted_at: Optional<Datetime>>>;
REPLACE INTO
courses
SELECT
id,
external_id,
name,
source_type,
source_name,
organization_id,
origin_link,
image_link,
description,
full_price,
discount,
duration,
starts_at,
created_at,
updated_at,
deleted_at
FROM AS_TABLE($courseData);`
writeTx := table.TxControl(
table.BeginTx(
table.WithSerializableReadWrite(),
),
table.CommitTx(),
)
err := r.db.Table().Do(ctx, func(ctx context.Context, s table.Session) error {
queryParams := table.NewQueryParameters(
table.ValueParam("$courseData", types.ListValue(createCourseParamsAsStruct(params))),
)
_, _, err := s.Execute(ctx, writeTx, upsertQuery, queryParams)
if err != nil {
return fmt.Errorf("executing query: %w", err)
}
return nil
})
return domain.Course{}, err
}
func (r *ydbCourseRepository) Delete(ctx context.Context, id string) error {
@ -184,7 +265,7 @@ func (r *ydbCourseRepository) CreateCourseTable(ctx context.Context) error {
return s.CreateTable(
ctx,
path.Join(r.db.Name(), "courses"),
options.WithColumn("id", types.TypeString),
options.WithColumn("id", types.TypeText),
options.WithColumn("external_id", types.Optional(types.TypeText)),
options.WithColumn("name", types.TypeText),
options.WithColumn("source_type", types.TypeText),
@ -193,8 +274,8 @@ func (r *ydbCourseRepository) CreateCourseTable(ctx context.Context) error {
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("full_price", types.TypeDouble),
options.WithColumn("discount", types.TypeDouble),
options.WithColumn("duration", types.TypeInterval),
options.WithColumn("starts_at", types.TypeDatetime),
options.WithColumn("created_at", types.TypeDatetime),
@ -227,6 +308,7 @@ type courseDB struct {
func (c *courseDB) getNamedValues() []named.Value {
return []named.Value{
named.Required("id", &c.ID),
named.Required("name", &c.Name),
named.Optional("external_id", &c.ExternalID),
named.Required("source_type", &c.SourceType),
named.Optional("source_name", &c.SourceName),
@ -234,6 +316,8 @@ func (c *courseDB) getNamedValues() []named.Value {
named.Required("origin_link", &c.OriginLink),
named.Required("image_link", &c.ImageLink),
named.Required("description", &c.Description),
named.Required("full_price", &c.FullPrice),
named.Required("discount", &c.Discount),
named.Required("duration", &c.Duration),
named.Required("starts_at", &c.StartAt),
named.Required("created_at", &c.CreatedAt),
@ -248,9 +332,8 @@ const (
sourceTypeParsed = "p"
)
func mapCourseDB(cdb courseDB) domain.Course {
var st domain.SourceType
switch cdb.SourceType {
func mapSourceTypeToDomain(in string) (st domain.SourceType) {
switch in {
case sourceTypeUnknown:
st = domain.SourceTypeUnset
case sourceTypeManual:
@ -259,6 +342,26 @@ func mapCourseDB(cdb courseDB) domain.Course {
st = domain.SourceTypeParsed
}
return st
}
func mapSourceTypeFromDomain(in domain.SourceType) string {
var st string
switch in {
case domain.SourceTypeManual:
st = sourceTypeManual
case domain.SourceTypeParsed:
st = sourceTypeParsed
default:
st = sourceTypeUnknown
}
return st
}
func mapCourseDB(cdb courseDB) domain.Course {
st := mapSourceTypeToDomain(cdb.SourceType)
return domain.Course{
ID: cdb.ID,
ExternalID: nullable.NewValuePtr(cdb.ExternalID),

View File

@ -13,14 +13,14 @@ import (
type CreateCourse struct {
ID string
Name string
Description string
ExternalID nullable.Value[string]
Name string
SourceType domain.SourceType
SourceName nullable.Value[string]
OrganizationID string
OriginLink string
ImageLink string
Description string
FullPrice float64
Discount float64
Duration time.Duration

View File

@ -15,14 +15,14 @@ type ListCoursesParams struct {
type CreateCourseParams struct {
ID string
Name string
Description string
ExternalID nullable.Value[string]
Name string
SourceType SourceType
SourceName nullable.Value[string]
OrganizationID string
OriginLink string
ImageLink string
Description string
FullPrice float64
Discount float64
Duration time.Duration