386 lines
9.0 KiB
Go
386 lines
9.0 KiB
Go
package commands
|
|
|
|
import (
|
|
"bufio"
|
|
"context"
|
|
"encoding/json"
|
|
"fmt"
|
|
"os"
|
|
"strconv"
|
|
"strings"
|
|
|
|
"git.loyso.art/frx/eway/cmd/cli/components"
|
|
"git.loyso.art/frx/eway/internal/entity"
|
|
"git.loyso.art/frx/eway/internal/matcher"
|
|
|
|
"github.com/rs/zerolog"
|
|
"github.com/urfave/cli/v3"
|
|
)
|
|
|
|
func ItemsCommandTree() *cli.Command {
|
|
var h itemsHandlers
|
|
return &cli.Command{
|
|
Name: "items",
|
|
Usage: "Interact with items stored inside db",
|
|
Commands: []*cli.Command{
|
|
newItemsGetCommand(h),
|
|
newItemsCountCommand(h),
|
|
newItemsUniqueParamsCommand(h),
|
|
newItemsAggregateParametersCommand(h),
|
|
newItemsFixSizesCommand(h),
|
|
},
|
|
}
|
|
}
|
|
|
|
func newItemsGetCommand(h itemsHandlers) *cli.Command {
|
|
cmd := cli.Command{
|
|
Name: "get",
|
|
Usage: "gets goods item by its id",
|
|
Flags: []cli.Flag{
|
|
&cli.StringFlag{
|
|
Name: "id",
|
|
Usage: "id of the goods item. Either id or cart-id should be set",
|
|
},
|
|
&cli.IntFlag{
|
|
Name: "cart-id",
|
|
Usage: "cart-id of the item. Either cart-id or id should be set",
|
|
},
|
|
},
|
|
}
|
|
|
|
return cmdWithAction(cmd, h.Get)
|
|
}
|
|
|
|
func newItemsCountCommand(h itemsHandlers) *cli.Command {
|
|
cmd := cli.Command{
|
|
Name: "count",
|
|
Usage: "iterates over collection and counts number of items",
|
|
Flags: []cli.Flag{
|
|
&cli.StringSliceFlag{
|
|
Name: "param-key-match",
|
|
Usage: "filters by parameters with AND logic",
|
|
},
|
|
},
|
|
}
|
|
|
|
return cmdWithAction(cmd, h.Count)
|
|
}
|
|
|
|
func newItemsUniqueParamsCommand(h itemsHandlers) *cli.Command {
|
|
cmd := cli.Command{
|
|
Name: "unique-params",
|
|
Usage: "Show all stored unique param values",
|
|
Description: "This command iterates over each item and collect keys of params in a dict and then" +
|
|
" print it to the output. It's useful to find all unique parameters",
|
|
}
|
|
|
|
return cmdWithAction(cmd, h.UniqueParameters)
|
|
}
|
|
|
|
func newItemsAggregateParametersCommand(h itemsHandlers) *cli.Command {
|
|
cmd := cli.Command{
|
|
Name: "aggregate-parameters",
|
|
Usage: "Show all values of requested parameters",
|
|
Flags: []cli.Flag{
|
|
&cli.BoolFlag{
|
|
Name: "case-insensitive",
|
|
Usage: "Ignores cases of keys",
|
|
},
|
|
&cli.StringSliceFlag{
|
|
Name: "regex",
|
|
Usage: "Registers regex to match",
|
|
},
|
|
&cli.BoolFlag{
|
|
Name: "keys-only",
|
|
Usage: "prints only keys",
|
|
},
|
|
},
|
|
}
|
|
|
|
return cmdWithAction(cmd, h.AggregateParameters)
|
|
}
|
|
|
|
func newItemsFixSizesCommand(h itemsHandlers) *cli.Command {
|
|
cmd := cli.Command{
|
|
Name: "fix-sizes",
|
|
Usage: "Iterates over params and sets sizes from parameters",
|
|
}
|
|
|
|
return cmdWithAction(cmd, h.FixSizes)
|
|
}
|
|
|
|
type itemsHandlers struct{}
|
|
|
|
func (itemsHandlers) Get(ctx context.Context, cmd *cli.Command) error {
|
|
r, err := components.GetRepository()
|
|
if err != nil {
|
|
return fmt.Errorf("getting repository: %w", err)
|
|
}
|
|
|
|
id := cmd.String("id")
|
|
cartID := cmd.Int("cart-id")
|
|
if id == "" && cartID == 0 {
|
|
return cli.Exit("oneof: id or cart-id should be set", 1)
|
|
} else if id != "" && cartID != 0 {
|
|
return cli.Exit("oneof: id or cart-id should be set", 1)
|
|
}
|
|
|
|
var item entity.GoodsItem
|
|
if id != "" {
|
|
item, err = r.GoodsItem().Get(ctx, id)
|
|
} else {
|
|
item, err = r.GoodsItem().GetByCart(ctx, cartID)
|
|
}
|
|
if err != nil {
|
|
return fmt.Errorf("getting item: %w", err)
|
|
}
|
|
|
|
enc := json.NewEncoder(os.Stdout)
|
|
enc.SetIndent("", " ")
|
|
|
|
err = enc.Encode(item)
|
|
if err != nil {
|
|
return fmt.Errorf("encoding item: %w", err)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func (itemsHandlers) Count(ctx context.Context, cmd *cli.Command) error {
|
|
r, err := components.GetRepository()
|
|
if err != nil {
|
|
return fmt.Errorf("getting repository: %w", err)
|
|
}
|
|
|
|
filters := cmd.StringSlice("param-key-match")
|
|
m := matcher.NewRadix()
|
|
patternMapped := make(map[string]empty, len(filters))
|
|
if len(filters) == 0 {
|
|
m.Register("*")
|
|
} else {
|
|
for _, f := range filters {
|
|
m.Register(f)
|
|
}
|
|
for _, pattern := range m.Patterns() {
|
|
patternMapped[pattern] = empty{}
|
|
}
|
|
}
|
|
|
|
var count int
|
|
items, err := r.GoodsItem().List(ctx)
|
|
if err != nil {
|
|
return fmt.Errorf("getting items: %w", err)
|
|
}
|
|
for _, item := range items {
|
|
seenPatterns := map[string]empty{}
|
|
|
|
for k := range item.Parameters {
|
|
pattern := m.MatchByPattern(k)
|
|
if pattern == "" {
|
|
continue
|
|
}
|
|
if _, ok := seenPatterns[pattern]; ok {
|
|
continue
|
|
}
|
|
seenPatterns[pattern] = empty{}
|
|
}
|
|
|
|
if len(seenPatterns) == len(patternMapped) {
|
|
count++
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func (itemsHandlers) UniqueParameters(ctx context.Context, cmd *cli.Command) error {
|
|
repository, err := components.GetRepository()
|
|
if err != nil {
|
|
return fmt.Errorf("getting repository: %w", err)
|
|
}
|
|
|
|
knownParams := map[string]empty{}
|
|
iter, err := repository.GoodsItem().ListIter(ctx, 1)
|
|
if err != nil {
|
|
return fmt.Errorf("getting list iter: %w", err)
|
|
}
|
|
for item := range iter {
|
|
for k := range item.Parameters {
|
|
knownParams[k] = empty{}
|
|
}
|
|
}
|
|
|
|
bw := bufio.NewWriter(cmd.Writer)
|
|
for paramName := range knownParams {
|
|
_, err = bw.WriteString(paramName + "\n")
|
|
if err != nil {
|
|
return fmt.Errorf("unable to write: %w", err)
|
|
}
|
|
}
|
|
|
|
return bw.Flush()
|
|
}
|
|
|
|
func (itemsHandlers) AggregateParameters(ctx context.Context, cmd *cli.Command) error {
|
|
repository, err := components.GetRepository()
|
|
if err != nil {
|
|
return fmt.Errorf("getting repository: %w", err)
|
|
}
|
|
log, err := components.GetLogger()
|
|
if err != nil {
|
|
return fmt.Errorf("getting logger: %w", err)
|
|
}
|
|
|
|
params := cmd.Args().Slice()
|
|
opts := make([]matcher.RadixOpt, 0, 1)
|
|
if cmd.Bool("case-insensitive") {
|
|
opts = append(opts, matcher.RadixCaseInsensitive())
|
|
}
|
|
|
|
m := matcher.NewRadix(opts...)
|
|
for _, param := range params {
|
|
log.Debug().Str("param", param).Msg("registering param")
|
|
m.Register(param)
|
|
}
|
|
for _, regexp := range cmd.StringSlice("regex") {
|
|
log.Debug().Str("regexp", regexp).Msg("registering regexp")
|
|
m.RegisterRegexp(regexp)
|
|
}
|
|
|
|
requestedValues := make(map[string]map[string]empty, len(params))
|
|
requestedValuesByPattern := make(map[string]map[string]empty, len(params))
|
|
iter := getItemsIter(ctx, repository.GoodsItem())
|
|
for iter.Next() {
|
|
item := iter.Get()
|
|
for k, v := range item.Parameters {
|
|
matchedPattern := m.MatchByPattern(k)
|
|
if matchedPattern == "" {
|
|
continue
|
|
}
|
|
|
|
values, ok := requestedValues[k]
|
|
if !ok {
|
|
values = make(map[string]empty)
|
|
}
|
|
values[v] = empty{}
|
|
requestedValues[k] = values
|
|
|
|
values, ok = requestedValuesByPattern[matchedPattern]
|
|
if !ok {
|
|
values = map[string]empty{}
|
|
}
|
|
values[v] = empty{}
|
|
requestedValuesByPattern[matchedPattern] = values
|
|
}
|
|
}
|
|
|
|
bw := bufio.NewWriter(cmd.Writer)
|
|
_, _ = bw.WriteString("Matches:\n")
|
|
|
|
if cmd.Bool("keys-only") {
|
|
for k := range requestedValues {
|
|
_, _ = bw.WriteString(k + "\n")
|
|
}
|
|
} else {
|
|
for k, v := range requestedValues {
|
|
_, _ = bw.WriteString(k + ": ")
|
|
values := make([]string, 0, len(v))
|
|
for item := range v {
|
|
values = append(values, strconv.Quote(item))
|
|
}
|
|
valuesStr := "[" + strings.Join(values, ",") + "]\n"
|
|
_, _ = bw.WriteString(valuesStr)
|
|
}
|
|
}
|
|
|
|
_, _ = bw.WriteString("\nPatterns:\n")
|
|
for _, pattern := range m.Patterns() {
|
|
_, _ = bw.WriteString(pattern + "\n")
|
|
}
|
|
|
|
return bw.Flush()
|
|
}
|
|
|
|
func (itemsHandlers) FixSizes(ctx context.Context, cmd *cli.Command) error {
|
|
repository, err := components.GetRepository()
|
|
if err != nil {
|
|
return fmt.Errorf("getting repository: %w", err)
|
|
}
|
|
|
|
matcher, err := components.GetDimensionMatcher()
|
|
if err != nil {
|
|
return fmt.Errorf("getting dimension matcher: %w", err)
|
|
}
|
|
|
|
dimensionDispatcher := dimensionDispatcher{m: matcher}
|
|
|
|
log := zerolog.Ctx(ctx)
|
|
|
|
toUpdate := make([]entity.GoodsItem, 0, 20_000)
|
|
bus := getItemsIter(ctx, repository.GoodsItem())
|
|
for bus.Next() {
|
|
item := bus.Get()
|
|
|
|
var valueBeenUpdated bool
|
|
for key, value := range item.Parameters {
|
|
trimmedValue := strings.TrimSpace(value)
|
|
trimmedKey := strings.TrimSpace(key)
|
|
|
|
if trimmedKey != key || trimmedValue != value {
|
|
log.Warn().
|
|
Str("old_key", key).
|
|
Str("new_key", trimmedKey).
|
|
Str("old_value", value).
|
|
Str("new_value", trimmedValue).
|
|
Msg("found mismatch")
|
|
|
|
delete(item.Parameters, key)
|
|
|
|
key = trimmedKey
|
|
value = trimmedValue
|
|
|
|
item.Parameters[key] = value
|
|
valueBeenUpdated = true
|
|
}
|
|
|
|
updateValue := strings.HasSuffix(key, ":")
|
|
if updateValue {
|
|
key = strings.TrimSuffix(key, ":")
|
|
item.Parameters[key] = value
|
|
delete(item.Parameters, key+":")
|
|
valueBeenUpdated = true
|
|
}
|
|
|
|
if dimensionDispatcher.applyDimensionValue(ctx, key, value, &item.Sizes) {
|
|
valueBeenUpdated = true
|
|
log.Debug().Str("key", key).Any("sizes", item.Sizes).Msg("been updated")
|
|
}
|
|
}
|
|
|
|
var updated bool
|
|
oldSizes := item.Sizes
|
|
item.Sizes, updated = entity.FixupSizes(oldSizes)
|
|
if updated {
|
|
log.Info().Int64("cart", item.Cart).Any("old", oldSizes).Any("new", item.Sizes).Msg("sizes been fixed")
|
|
}
|
|
|
|
valueBeenUpdated = valueBeenUpdated || updated
|
|
|
|
if valueBeenUpdated {
|
|
toUpdate = append(toUpdate, item)
|
|
}
|
|
}
|
|
if bus.Err() != nil {
|
|
return fmt.Errorf("iterating: %w", bus.Err())
|
|
}
|
|
|
|
_, err = repository.GoodsItem().UpsertMany(ctx, toUpdate...)
|
|
if err != nil {
|
|
return fmt.Errorf("updating items: %w", err)
|
|
}
|
|
|
|
log.Info().Int("count", len(toUpdate)).Msg("updated items")
|
|
|
|
return nil
|
|
}
|