From 4b4d515ade9d6003af03ce27f92d7726dfffde1f Mon Sep 17 00:00:00 2001 From: Leon Mika Date: Fri, 14 Apr 2023 15:35:43 +1000 Subject: [PATCH 01/10] Added a few changes to query expressions (#51) - Added the between operator to query expressions. - Added the using query expression suffix to specify which index to query (or force a scan). This is required if query planning has found multiple indices that can potentially be used. - Rewrote the types of the query expressions to allow for functions to be defined once, and be useful in queries that result in DynamoDB queries, and evaluation. - Added some test functions around time and summing numbers. - Fixed a bug in the del-attr which was not honouring marked rows in a similar way to set-attr: it was only deleting attributes from the first row. - Added the -to type flag to set-attr which will set the attribute to the value of a query expression. --- internal/common/maputils/map.go | 10 + internal/common/sliceutils/map.go | 19 + internal/dynamo-browse/controllers/scripts.go | 3 + .../dynamo-browse/controllers/tablewrite.go | 46 +- internal/dynamo-browse/models/itemtype.go | 2 + .../dynamo-browse/models/queryexpr/ast.go | 89 +++- .../dynamo-browse/models/queryexpr/atom.go | 16 +- .../dynamo-browse/models/queryexpr/between.go | 155 +++++++ .../dynamo-browse/models/queryexpr/boolnot.go | 7 +- .../models/queryexpr/builtins.go | 90 +++- .../dynamo-browse/models/queryexpr/comp.go | 38 +- .../dynamo-browse/models/queryexpr/conj.go | 26 +- .../dynamo-browse/models/queryexpr/context.go | 27 ++ .../dynamo-browse/models/queryexpr/disj.go | 9 +- .../dynamo-browse/models/queryexpr/dot.go | 11 +- .../models/queryexpr/equality.go | 43 +- .../dynamo-browse/models/queryexpr/errors.go | 35 ++ .../dynamo-browse/models/queryexpr/expr.go | 46 +- .../models/queryexpr/expr_test.go | 114 +++++ .../dynamo-browse/models/queryexpr/fncall.go | 58 ++- .../models/queryexpr/helpers_test.go | 16 + internal/dynamo-browse/models/queryexpr/in.go | 147 +++---- internal/dynamo-browse/models/queryexpr/ir.go | 6 +- internal/dynamo-browse/models/queryexpr/is.go | 50 ++- .../models/queryexpr/placeholder.go | 18 +- .../dynamo-browse/models/queryexpr/subref.go | 86 ++-- .../dynamo-browse/models/queryexpr/types.go | 399 ++++++++++++++++++ .../dynamo-browse/models/queryexpr/values.go | 43 +- .../services/scriptmanager/iface.go | 1 + .../services/scriptmanager/modsession.go | 5 + internal/dynamo-browse/ui/model.go | 2 + test/cmd/load-test-table/main.go | 6 +- 32 files changed, 1284 insertions(+), 339 deletions(-) create mode 100644 internal/dynamo-browse/models/queryexpr/between.go create mode 100644 internal/dynamo-browse/models/queryexpr/context.go create mode 100644 internal/dynamo-browse/models/queryexpr/helpers_test.go diff --git a/internal/common/maputils/map.go b/internal/common/maputils/map.go index bffa7a1..9708bbe 100644 --- a/internal/common/maputils/map.go +++ b/internal/common/maputils/map.go @@ -8,6 +8,16 @@ func Values[K comparable, T any](ts map[K]T) []T { return values } +func MapValues[K comparable, T, U any](ts map[K]T, fn func(t T) U) map[K]U { + us := make(map[K]U) + + for k, t := range ts { + us[k] = fn(t) + } + + return us +} + func MapValuesWithError[K comparable, T, U any](ts map[K]T, fn func(t T) (U, error)) (map[K]U, error) { us := make(map[K]U) diff --git a/internal/common/sliceutils/map.go b/internal/common/sliceutils/map.go index 43b69e7..ae2a1be 100644 --- a/internal/common/sliceutils/map.go +++ b/internal/common/sliceutils/map.go @@ -39,3 +39,22 @@ func Filter[T any](ts []T, fn func(t T) bool) []T { } return us } + +func FindFirst[T any](ts []T, fn func(t T) bool) (returnedT T, found bool) { + for _, t := range ts { + if fn(t) { + return t, true + } + } + return returnedT, false +} + +func FindLast[T any](ts []T, fn func(t T) bool) (returnedT T, found bool) { + for i := len(ts) - 1; i >= 0; i-- { + t := ts[i] + if fn(t) { + return t, true + } + } + return returnedT, false +} diff --git a/internal/dynamo-browse/controllers/scripts.go b/internal/dynamo-browse/controllers/scripts.go index 50daf23..35e66c2 100644 --- a/internal/dynamo-browse/controllers/scripts.go +++ b/internal/dynamo-browse/controllers/scripts.go @@ -182,6 +182,9 @@ func (s *sessionImpl) Query(ctx context.Context, query string, opts scriptmanage if opts.ValuePlaceholders != nil { expr = expr.WithValueParams(opts.ValuePlaceholders) } + if opts.IndexName != "" { + expr = expr.WithIndex(opts.IndexName) + } // Get the table info var tableInfo *models.TableInfo diff --git a/internal/dynamo-browse/controllers/tablewrite.go b/internal/dynamo-browse/controllers/tablewrite.go index cc1410a..2409c1f 100644 --- a/internal/dynamo-browse/controllers/tablewrite.go +++ b/internal/dynamo-browse/controllers/tablewrite.go @@ -122,6 +122,8 @@ func (twc *TableWriteController) SetAttributeValue(idx int, itemType models.Item return twc.setBoolValue(idx, path) case models.NullItemType: return twc.setNullValue(idx, path) + case models.ExprValueItemType: + return twc.setToExpressionValue(idx, path) default: return events.Error(errors.New("unsupported attribute type")) } @@ -151,6 +153,39 @@ func (twc *TableWriteController) setStringValue(idx int, attr *queryexpr.QueryEx } } +func (twc *TableWriteController) setToExpressionValue(idx int, attr *queryexpr.QueryExpr) tea.Msg { + return events.PromptForInputMsg{ + Prompt: "expr value: ", + OnDone: func(value string) tea.Msg { + valueExpr, err := queryexpr.Parse(value) + if err != nil { + return events.Error(err) + } + + if err := twc.state.withResultSetReturningError(func(set *models.ResultSet) error { + if err := applyToMarkedItems(set, idx, func(idx int, item models.Item) error { + newValue, err := valueExpr.EvalItem(item) + if err != nil { + return err + } + if err := attr.SetEvalItem(item, newValue); err != nil { + return err + } + set.SetDirty(idx, true) + return nil + }); err != nil { + return err + } + set.RefreshColumns() + return nil + }); err != nil { + return events.Error(err) + } + return ResultSetUpdated{} + }, + } +} + func (twc *TableWriteController) setNumberValue(idx int, attr *queryexpr.QueryExpr) tea.Msg { return events.PromptForInputMsg{ Prompt: "number value: ", @@ -239,12 +274,17 @@ func (twc *TableWriteController) DeleteAttribute(idx int, key string) tea.Msg { } if err := twc.state.withResultSetReturningError(func(set *models.ResultSet) error { - err := path.DeleteAttribute(set.Items()[idx]) - if err != nil { + if err := applyToMarkedItems(set, idx, func(idx int, item models.Item) error { + if err := path.DeleteAttribute(set.Items()[idx]); err != nil { + return err + } + + set.SetDirty(idx, true) + return nil + }); err != nil { return err } - set.SetDirty(idx, true) set.RefreshColumns() return nil }); err != nil { diff --git a/internal/dynamo-browse/models/itemtype.go b/internal/dynamo-browse/models/itemtype.go index 4174005..ab271c9 100644 --- a/internal/dynamo-browse/models/itemtype.go +++ b/internal/dynamo-browse/models/itemtype.go @@ -8,4 +8,6 @@ const ( NumberItemType ItemType = "N" BoolItemType ItemType = "BOOL" NullItemType ItemType = "NULL" + + ExprValueItemType ItemType = "exprvalue" ) diff --git a/internal/dynamo-browse/models/queryexpr/ast.go b/internal/dynamo-browse/models/queryexpr/ast.go index 7e8cf81..2c81161 100644 --- a/internal/dynamo-browse/models/queryexpr/ast.go +++ b/internal/dynamo-browse/models/queryexpr/ast.go @@ -4,17 +4,23 @@ import ( "github.com/alecthomas/participle/v2" "github.com/alecthomas/participle/v2/lexer" "github.com/aws/aws-sdk-go-v2/feature/dynamodb/expression" - "github.com/aws/aws-sdk-go-v2/service/dynamodb/types" "github.com/lmika/audax/internal/common/sliceutils" "github.com/lmika/audax/internal/dynamo-browse/models" "github.com/pkg/errors" + "strconv" ) // Modelled on the expression language here // https://docs.aws.amazon.com/amazondynamodb/latest/developerguide/Expressions.OperatorsAndFunctions.html type astExpr struct { - Root *astDisjunction `parser:"@@"` + Root *astDisjunction `parser:"@@"` + Options *astOptions `parser:"( 'using' @@ )?"` +} + +type astOptions struct { + Scan bool `parser:"@'scan'"` + Index string `parser:" | 'index' '(' @String ')'"` } type astDisjunction struct { @@ -38,9 +44,15 @@ type astIn struct { } type astComparisonOp struct { - Ref *astEqualityOp `parser:"@@"` - Op string `parser:"( @('<' | '<=' | '>' | '>=')"` - Value *astEqualityOp `parser:"@@ )?"` + Ref *astBetweenOp `parser:"@@"` + Op string `parser:"( @('<' | '<=' | '>' | '>=')"` + Value *astBetweenOp `parser:"@@ )?"` +} + +type astBetweenOp struct { + Ref *astEqualityOp `parser:"@@"` + From *astEqualityOp `parser:"( 'between' @@ "` + To *astEqualityOp `parser:" 'and' @@ )?"` } type astEqualityOp struct { @@ -58,7 +70,6 @@ type astIsOp struct { type astSubRef struct { Ref *astFunctionCall `parser:"@@"` SubRefs []*astSubRefType `parser:"@@*"` - //Quals []string `parser:"('.' @Ident)*"` } type astSubRefType struct { @@ -121,7 +132,58 @@ func Parse(expr string) (*QueryExpr, error) { return &QueryExpr{ast: ast}, nil } -func (a *astExpr) calcQuery(ctx *evalContext, info *models.TableInfo) (*models.QueryExecutionPlan, error) { +func (a *astExpr) calcQuery(ctx *evalContext, info *models.TableInfo, preferredIndex string) (*models.QueryExecutionPlan, error) { + plans, err := a.determinePlausibleExecutionPlans(ctx, info) + if err != nil { + return nil, err + } + + scanPlan, _ := sliceutils.FindLast(plans, func(p *models.QueryExecutionPlan) bool { + return !p.CanQuery + }) + queryPlans := sliceutils.Filter(plans, func(p *models.QueryExecutionPlan) bool { + if !p.CanQuery { + return false + } + return true + }) + + if len(queryPlans) == 0 || (a.Options != nil && a.Options.Scan) { + if preferredIndex != "" { + return nil, NoPlausiblePlanWithIndexError{ + PreferredIndex: preferredIndex, + PossibleIndices: sliceutils.Map(queryPlans, func(p *models.QueryExecutionPlan) string { return p.IndexName }), + } + } + return scanPlan, nil + } + + if preferredIndex == "" && (a.Options != nil && a.Options.Index != "") { + preferredIndex, err = strconv.Unquote(a.Options.Index) + if err != nil { + return nil, err + } + } + + if preferredIndex != "" { + queryPlans = sliceutils.Filter(queryPlans, func(p *models.QueryExecutionPlan) bool { return p.IndexName == preferredIndex }) + } + if len(queryPlans) == 1 { + return queryPlans[0], nil + } else if len(queryPlans) == 0 { + return nil, NoPlausiblePlanWithIndexError{ + PreferredIndex: preferredIndex, + } + } + + return nil, MultiplePlansWithIndexError{ + PossibleIndices: sliceutils.Map(queryPlans, func(p *models.QueryExecutionPlan) string { return p.IndexName }), + } +} + +func (a *astExpr) determinePlausibleExecutionPlans(ctx *evalContext, info *models.TableInfo) ([]*models.QueryExecutionPlan, error) { + plans := make([]*models.QueryExecutionPlan, 0) + type queryTestAttempt struct { index string keysUnderTest models.KeyAttribute @@ -153,11 +215,11 @@ func (a *astExpr) calcQuery(ctx *evalContext, info *models.TableInfo) (*models.Q return nil, err } - return &models.QueryExecutionPlan{ + plans = append(plans, &models.QueryExecutionPlan{ CanQuery: true, IndexName: attempt.index, Expression: expr, - }, nil + }) } } @@ -174,21 +236,22 @@ func (a *astExpr) calcQuery(ctx *evalContext, info *models.TableInfo) (*models.Q return nil, err } - return &models.QueryExecutionPlan{ + plans = append(plans, &models.QueryExecutionPlan{ CanQuery: false, Expression: expr, - }, nil + }) + return plans, nil } func (a *astExpr) evalToIR(ctx *evalContext, tableInfo *models.TableInfo) (irAtom, error) { return a.Root.evalToIR(ctx, tableInfo) } -func (a *astExpr) evalItem(ctx *evalContext, item models.Item) (types.AttributeValue, error) { +func (a *astExpr) evalItem(ctx *evalContext, item models.Item) (exprValue, error) { return a.Root.evalItem(ctx, item) } -func (a *astExpr) setEvalItem(ctx *evalContext, item models.Item, value types.AttributeValue) error { +func (a *astExpr) setEvalItem(ctx *evalContext, item models.Item, value exprValue) error { return a.Root.setEvalItem(ctx, item, value) } diff --git a/internal/dynamo-browse/models/queryexpr/atom.go b/internal/dynamo-browse/models/queryexpr/atom.go index 8b4487e..5a24d2e 100644 --- a/internal/dynamo-browse/models/queryexpr/atom.go +++ b/internal/dynamo-browse/models/queryexpr/atom.go @@ -1,7 +1,6 @@ package queryexpr import ( - "github.com/aws/aws-sdk-go-v2/service/dynamodb/types" "github.com/lmika/audax/internal/dynamo-browse/models" "github.com/pkg/errors" ) @@ -21,15 +20,6 @@ func (a *astAtom) evalToIR(ctx *evalContext, info *models.TableInfo) (irAtom, er return nil, errors.New("unhandled atom case") } -func (a *astAtom) rightOperandDynamoValue() (types.AttributeValue, error) { - switch { - case a.Literal != nil: - return a.Literal.dynamoValue() - } - - return nil, errors.New("unhandled atom case") -} - func (a *astAtom) unqualifiedName() (string, bool) { switch { case a.Ref != nil: @@ -39,12 +29,12 @@ func (a *astAtom) unqualifiedName() (string, bool) { return "", false } -func (a *astAtom) evalItem(ctx *evalContext, item models.Item) (types.AttributeValue, error) { +func (a *astAtom) evalItem(ctx *evalContext, item models.Item) (exprValue, error) { switch { case a.Ref != nil: return a.Ref.evalItem(ctx, item) case a.Literal != nil: - return a.Literal.dynamoValue() + return a.Literal.exprValue() case a.Placeholder != nil: return a.Placeholder.evalItem(ctx, item) case a.Paren != nil: @@ -66,7 +56,7 @@ func (a *astAtom) canModifyItem(ctx *evalContext, item models.Item) bool { return false } -func (a *astAtom) setEvalItem(ctx *evalContext, item models.Item, value types.AttributeValue) error { +func (a *astAtom) setEvalItem(ctx *evalContext, item models.Item, value exprValue) error { switch { case a.Ref != nil: return a.Ref.setEvalItem(ctx, item, value) diff --git a/internal/dynamo-browse/models/queryexpr/between.go b/internal/dynamo-browse/models/queryexpr/between.go new file mode 100644 index 0000000..cbb4c3d --- /dev/null +++ b/internal/dynamo-browse/models/queryexpr/between.go @@ -0,0 +1,155 @@ +package queryexpr + +import ( + "fmt" + "github.com/aws/aws-sdk-go-v2/feature/dynamodb/expression" + "github.com/lmika/audax/internal/dynamo-browse/models" +) + +func (a *astBetweenOp) evalToIR(ctx *evalContext, info *models.TableInfo) (irAtom, error) { + leftIR, err := a.Ref.evalToIR(ctx, info) + if err != nil { + return nil, err + } + + if a.From == nil { + return leftIR, nil + } + + nameIR, isNameIR := leftIR.(nameIRAtom) + if !isNameIR { + return nil, OperandNotANameError(a.Ref.String()) + } + + fromIR, err := a.From.evalToIR(ctx, info) + if err != nil { + return nil, err + } + toIR, err := a.To.evalToIR(ctx, info) + if err != nil { + return nil, err + } + + fromOprIR, isFromOprIR := fromIR.(valueIRAtom) + if !isFromOprIR { + return nil, OperandNotAnOperandError{} + } + toOprIR, isToOprIR := toIR.(valueIRAtom) + if !isToOprIR { + return nil, OperandNotAnOperandError{} + } + + return irBetween{name: nameIR, from: fromOprIR, to: toOprIR}, nil +} + +func (a *astBetweenOp) evalItem(ctx *evalContext, item models.Item) (exprValue, error) { + val, err := a.Ref.evalItem(ctx, item) + if a.From == nil { + return val, err + } + + fromIR, err := a.From.evalItem(ctx, item) + if err != nil { + return nil, err + } + + toIR, err := a.To.evalItem(ctx, item) + if err != nil { + return nil, err + } + + switch v := val.(type) { + case stringableExprValue: + fromNumVal, isFromNumVal := fromIR.(stringableExprValue) + if !isFromNumVal { + return nil, ValuesNotComparable{Left: val.asAttributeValue(), Right: fromIR.asAttributeValue()} + } + + toNumVal, isToNumVal := toIR.(stringableExprValue) + if !isToNumVal { + return nil, ValuesNotComparable{Left: val.asAttributeValue(), Right: toNumVal.asAttributeValue()} + } + + return boolExprValue(v.asString() >= fromNumVal.asString() && v.asString() <= toNumVal.asString()), nil + case numberableExprValue: + fromNumVal, isFromNumVal := fromIR.(numberableExprValue) + if !isFromNumVal { + return nil, ValuesNotComparable{Left: val.asAttributeValue(), Right: fromIR.asAttributeValue()} + } + + toNumVal, isToNumVal := toIR.(numberableExprValue) + if !isToNumVal { + return nil, ValuesNotComparable{Left: val.asAttributeValue(), Right: toNumVal.asAttributeValue()} + } + + fromCmp := v.asBigFloat().Cmp(fromNumVal.asBigFloat()) + toCmp := v.asBigFloat().Cmp(toNumVal.asBigFloat()) + + return boolExprValue(fromCmp >= 0 && toCmp <= 0), nil + } + return nil, InvalidTypeForBetweenError{TypeName: val.typeName()} +} + +func (a *astBetweenOp) canModifyItem(ctx *evalContext, item models.Item) bool { + if a.From != nil { + return false + } + return a.Ref.canModifyItem(ctx, item) +} + +func (a *astBetweenOp) setEvalItem(ctx *evalContext, item models.Item, value exprValue) error { + if a.From != nil { + return PathNotSettableError{} + } + return a.Ref.setEvalItem(ctx, item, value) +} + +func (a *astBetweenOp) deleteAttribute(ctx *evalContext, item models.Item) error { + if a.From != nil { + return PathNotSettableError{} + } + return a.Ref.deleteAttribute(ctx, item) +} + +func (a *astBetweenOp) String() string { + name := a.Ref.String() + if a.From != nil { + return fmt.Sprintf("%v between %v and %v", name, a.From.String(), a.To.String()) + } + return name +} + +type irBetween struct { + name nameIRAtom + from valueIRAtom + to valueIRAtom +} + +func (i irBetween) calcQueryForScan(info *models.TableInfo) (expression.ConditionBuilder, error) { + nb := i.name.calcName(info) + fb := i.from.calcOperand(info) + tb := i.to.calcOperand(info) + + return nb.Between(fb, tb), nil +} + +func (i irBetween) canBeExecutedAsQuery(qci *queryCalcInfo) bool { + keyName := i.name.keyName() + if keyName == "" { + return false + } + + if keyName == qci.keysUnderTest.SortKey { + return qci.addKey(keyName) + } + + return false +} + +func (i irBetween) calcQueryForQuery() (expression.KeyConditionBuilder, error) { + nb := i.name.keyName() + fb := i.from.exprValue() + tb := i.to.exprValue() + + return expression.Key(nb).Between(buildExpressionFromValue(fb), buildExpressionFromValue(tb)), nil +} diff --git a/internal/dynamo-browse/models/queryexpr/boolnot.go b/internal/dynamo-browse/models/queryexpr/boolnot.go index 9c71f79..79a11c7 100644 --- a/internal/dynamo-browse/models/queryexpr/boolnot.go +++ b/internal/dynamo-browse/models/queryexpr/boolnot.go @@ -2,7 +2,6 @@ package queryexpr import ( "github.com/aws/aws-sdk-go-v2/feature/dynamodb/expression" - "github.com/aws/aws-sdk-go-v2/service/dynamodb/types" "github.com/lmika/audax/internal/dynamo-browse/models" "strings" ) @@ -20,7 +19,7 @@ func (a *astBooleanNot) evalToIR(ctx *evalContext, tableInfo *models.TableInfo) return &irBoolNot{atom: irNode}, nil } -func (a *astBooleanNot) evalItem(ctx *evalContext, item models.Item) (types.AttributeValue, error) { +func (a *astBooleanNot) evalItem(ctx *evalContext, item models.Item) (exprValue, error) { val, err := a.Operand.evalItem(ctx, item) if err != nil { return nil, err @@ -30,7 +29,7 @@ func (a *astBooleanNot) evalItem(ctx *evalContext, item models.Item) (types.Attr return val, nil } - return &types.AttributeValueMemberBOOL{Value: !isAttributeTrue(val)}, nil + return boolExprValue(!isAttributeTrue(val)), nil } func (a *astBooleanNot) canModifyItem(ctx *evalContext, item models.Item) bool { @@ -40,7 +39,7 @@ func (a *astBooleanNot) canModifyItem(ctx *evalContext, item models.Item) bool { return a.Operand.canModifyItem(ctx, item) } -func (a *astBooleanNot) setEvalItem(ctx *evalContext, item models.Item, value types.AttributeValue) error { +func (a *astBooleanNot) setEvalItem(ctx *evalContext, item models.Item, value exprValue) error { if a.HasNot { return PathNotSettableError{} } diff --git a/internal/dynamo-browse/models/queryexpr/builtins.go b/internal/dynamo-browse/models/queryexpr/builtins.go index a42f0cc..a3e9322 100644 --- a/internal/dynamo-browse/models/queryexpr/builtins.go +++ b/internal/dynamo-browse/models/queryexpr/builtins.go @@ -2,38 +2,90 @@ package queryexpr import ( "context" - "github.com/aws/aws-sdk-go-v2/service/dynamodb/types" "github.com/pkg/errors" - "strconv" ) -type nativeFunc func(ctx context.Context, args []types.AttributeValue) (types.AttributeValue, error) +type nativeFunc func(ctx context.Context, args []exprValue) (exprValue, error) var nativeFuncs = map[string]nativeFunc{ - "size": func(ctx context.Context, args []types.AttributeValue) (types.AttributeValue, error) { + "size": func(ctx context.Context, args []exprValue) (exprValue, error) { if len(args) != 1 { return nil, InvalidArgumentNumberError{Name: "size", Expected: 1, Actual: len(args)} } var l int switch t := args[0].(type) { - case *types.AttributeValueMemberB: - l = len(t.Value) - case *types.AttributeValueMemberS: - l = len(t.Value) - case *types.AttributeValueMemberL: - l = len(t.Value) - case *types.AttributeValueMemberM: - l = len(t.Value) - case *types.AttributeValueMemberSS: - l = len(t.Value) - case *types.AttributeValueMemberNS: - l = len(t.Value) - case *types.AttributeValueMemberBS: - l = len(t.Value) + case stringExprValue: + l = len(t) + case mappableExprValue: + l = t.len() + case slicableExprValue: + l = t.len() default: return nil, errors.New("cannot take size of arg") } - return &types.AttributeValueMemberN{Value: strconv.Itoa(l)}, nil + return int64ExprValue(l), nil + }, + + "range": func(ctx context.Context, args []exprValue) (exprValue, error) { + if len(args) != 2 { + return nil, InvalidArgumentNumberError{Name: "range", Expected: 2, Actual: len(args)} + } + + xVal, isXNum := args[0].(numberableExprValue) + if !isXNum { + return nil, InvalidArgumentTypeError{Name: "range", ArgIndex: 0, Expected: "N"} + } + yVal, isYNum := args[1].(numberableExprValue) + if !isYNum { + return nil, InvalidArgumentTypeError{Name: "range", ArgIndex: 1, Expected: "N"} + } + + xInt, _ := xVal.asBigFloat().Int64() + yInt, _ := yVal.asBigFloat().Int64() + xs := make([]exprValue, 0) + for x := xInt; x <= yInt; x++ { + xs = append(xs, int64ExprValue(x)) + } + return listExprValue(xs), nil + }, + + "_x_now": func(ctx context.Context, args []exprValue) (exprValue, error) { + now := timeSourceFromContext(ctx).now().Unix() + return int64ExprValue(now), nil + }, + + "_x_add": func(ctx context.Context, args []exprValue) (exprValue, error) { + if len(args) != 2 { + return nil, InvalidArgumentNumberError{Name: "_x_add", Expected: 2, Actual: len(args)} + } + + xVal, isXNum := args[0].(numberableExprValue) + if !isXNum { + return nil, InvalidArgumentTypeError{Name: "_x_add", ArgIndex: 0, Expected: "N"} + } + yVal, isYNum := args[1].(numberableExprValue) + if !isYNum { + return nil, InvalidArgumentTypeError{Name: "_x_add", ArgIndex: 1, Expected: "N"} + } + + return bigNumExprValue{num: xVal.asBigFloat().Add(xVal.asBigFloat(), yVal.asBigFloat())}, nil + }, + + "_x_concat": func(ctx context.Context, args []exprValue) (exprValue, error) { + if len(args) != 2 { + return nil, InvalidArgumentNumberError{Name: "_x_concat", Expected: 2, Actual: len(args)} + } + + xVal, isXNum := args[0].(stringableExprValue) + if !isXNum { + return nil, InvalidArgumentTypeError{Name: "_x_concat", ArgIndex: 0, Expected: "S"} + } + yVal, isYNum := args[1].(stringableExprValue) + if !isYNum { + return nil, InvalidArgumentTypeError{Name: "_x_concat", ArgIndex: 1, Expected: "S"} + } + + return stringExprValue(xVal.asString() + yVal.asString()), nil }, } diff --git a/internal/dynamo-browse/models/queryexpr/comp.go b/internal/dynamo-browse/models/queryexpr/comp.go index 31e9e4d..3e19472 100644 --- a/internal/dynamo-browse/models/queryexpr/comp.go +++ b/internal/dynamo-browse/models/queryexpr/comp.go @@ -2,7 +2,6 @@ package queryexpr import ( "github.com/aws/aws-sdk-go-v2/feature/dynamodb/expression" - "github.com/aws/aws-sdk-go-v2/service/dynamodb/types" "github.com/lmika/audax/internal/dynamo-browse/models" "github.com/lmika/audax/internal/dynamo-browse/models/attrutils" "github.com/pkg/errors" @@ -47,7 +46,7 @@ func (a *astComparisonOp) evalToIR(ctx *evalContext, info *models.TableInfo) (ir return irGenericCmp{leftOpr, rightOpr, cmpType}, nil } -func (a *astComparisonOp) evalItem(ctx *evalContext, item models.Item) (types.AttributeValue, error) { +func (a *astComparisonOp) evalItem(ctx *evalContext, item models.Item) (exprValue, error) { left, err := a.Ref.evalItem(ctx, item) if err != nil { return nil, err @@ -61,20 +60,21 @@ func (a *astComparisonOp) evalItem(ctx *evalContext, item models.Item) (types.At return nil, err } - cmp, isComparable := attrutils.CompareScalarAttributes(left, right) + // TODO: use expr value here + cmp, isComparable := attrutils.CompareScalarAttributes(left.asAttributeValue(), right.asAttributeValue()) if !isComparable { - return nil, ValuesNotComparable{Left: left, Right: right} + return nil, ValuesNotComparable{Left: left.asAttributeValue(), Right: right.asAttributeValue()} } switch opToCmdType[a.Op] { case cmpTypeLt: - return &types.AttributeValueMemberBOOL{Value: cmp < 0}, nil + return boolExprValue(cmp < 0), nil case cmpTypeLe: - return &types.AttributeValueMemberBOOL{Value: cmp <= 0}, nil + return boolExprValue(cmp <= 0), nil case cmpTypeGt: - return &types.AttributeValueMemberBOOL{Value: cmp > 0}, nil + return boolExprValue(cmp > 0), nil case cmpTypeGe: - return &types.AttributeValueMemberBOOL{Value: cmp >= 0}, nil + return boolExprValue(cmp >= 0), nil } return nil, errors.Errorf("unrecognised operator: %v", a.Op) } @@ -86,7 +86,7 @@ func (a *astComparisonOp) canModifyItem(ctx *evalContext, item models.Item) bool return a.Ref.canModifyItem(ctx, item) } -func (a *astComparisonOp) setEvalItem(ctx *evalContext, item models.Item, value types.AttributeValue) error { +func (a *astComparisonOp) setEvalItem(ctx *evalContext, item models.Item, value exprValue) error { if a.Op != "" { return PathNotSettableError{} } @@ -143,34 +143,34 @@ func (a irKeyFieldCmp) canBeExecutedAsQuery(qci *queryCalcInfo) bool { func (a irKeyFieldCmp) calcQueryForScan(info *models.TableInfo) (expression.ConditionBuilder, error) { nb := a.name.calcName(info) - vb := a.value.goValue() + vb := a.value.exprValue() switch a.cmpType { case cmpTypeLt: - return nb.LessThan(expression.Value(vb)), nil + return nb.LessThan(buildExpressionFromValue(vb)), nil case cmpTypeLe: - return nb.LessThanEqual(expression.Value(vb)), nil + return nb.LessThanEqual(buildExpressionFromValue(vb)), nil case cmpTypeGt: - return nb.GreaterThan(expression.Value(vb)), nil + return nb.GreaterThan(buildExpressionFromValue(vb)), nil case cmpTypeGe: - return nb.GreaterThanEqual(expression.Value(vb)), nil + return nb.GreaterThanEqual(buildExpressionFromValue(vb)), nil } return expression.ConditionBuilder{}, errors.New("unsupported cmp type") } func (a irKeyFieldCmp) calcQueryForQuery() (expression.KeyConditionBuilder, error) { keyName := a.name.keyName() - vb := a.value.goValue() + vb := a.value.exprValue() switch a.cmpType { case cmpTypeLt: - return expression.Key(keyName).LessThan(expression.Value(vb)), nil + return expression.Key(keyName).LessThan(buildExpressionFromValue(vb)), nil case cmpTypeLe: - return expression.Key(keyName).LessThanEqual(expression.Value(vb)), nil + return expression.Key(keyName).LessThanEqual(buildExpressionFromValue(vb)), nil case cmpTypeGt: - return expression.Key(keyName).GreaterThan(expression.Value(vb)), nil + return expression.Key(keyName).GreaterThan(buildExpressionFromValue(vb)), nil case cmpTypeGe: - return expression.Key(keyName).GreaterThanEqual(expression.Value(vb)), nil + return expression.Key(keyName).GreaterThanEqual(buildExpressionFromValue(vb)), nil } return expression.KeyConditionBuilder{}, errors.New("unsupported cmp type") } diff --git a/internal/dynamo-browse/models/queryexpr/conj.go b/internal/dynamo-browse/models/queryexpr/conj.go index dd7f4a2..d54bb4b 100644 --- a/internal/dynamo-browse/models/queryexpr/conj.go +++ b/internal/dynamo-browse/models/queryexpr/conj.go @@ -2,8 +2,8 @@ package queryexpr import ( "github.com/aws/aws-sdk-go-v2/feature/dynamodb/expression" - "github.com/aws/aws-sdk-go-v2/service/dynamodb/types" "github.com/lmika/audax/internal/dynamo-browse/models" + "math/big" "strings" ) @@ -36,7 +36,7 @@ func (a *astConjunction) evalToIR(ctx *evalContext, tableInfo *models.TableInfo) return &irMultiConjunction{atoms: atoms}, nil } -func (a *astConjunction) evalItem(ctx *evalContext, item models.Item) (types.AttributeValue, error) { +func (a *astConjunction) evalItem(ctx *evalContext, item models.Item) (exprValue, error) { val, err := a.Operands[0].evalItem(ctx, item) if err != nil { return nil, err @@ -47,7 +47,7 @@ func (a *astConjunction) evalItem(ctx *evalContext, item models.Item) (types.Att for _, opr := range a.Operands[1:] { if !isAttributeTrue(val) { - return &types.AttributeValueMemberBOOL{Value: false}, nil + return boolExprValue(false), nil } val, err = opr.evalItem(ctx, item) @@ -56,7 +56,7 @@ func (a *astConjunction) evalItem(ctx *evalContext, item models.Item) (types.Att } } - return &types.AttributeValueMemberBOOL{Value: isAttributeTrue(val)}, nil + return boolExprValue(isAttributeTrue(val)), nil } func (a *astConjunction) canModifyItem(ctx *evalContext, item models.Item) bool { @@ -67,7 +67,7 @@ func (a *astConjunction) canModifyItem(ctx *evalContext, item models.Item) bool return false } -func (a *astConjunction) setEvalItem(ctx *evalContext, item models.Item, value types.AttributeValue) error { +func (a *astConjunction) setEvalItem(ctx *evalContext, item models.Item, value exprValue) error { if len(a.Operands) == 1 { return a.Operands[0].setEvalItem(ctx, item, value) } @@ -168,16 +168,16 @@ func (d *irMultiConjunction) calcQueryForScan(info *models.TableInfo) (expressio return conjExpr, nil } -func isAttributeTrue(attr types.AttributeValue) bool { +func isAttributeTrue(attr exprValue) bool { switch val := attr.(type) { - case *types.AttributeValueMemberS: - return val.Value != "" - case *types.AttributeValueMemberN: - return val.Value != "0" - case *types.AttributeValueMemberBOOL: - return val.Value - case *types.AttributeValueMemberNULL: + case nullExprValue: return false + case boolExprValue: + return bool(val) + case stringableExprValue: + return val.asString() != "" + case numberableExprValue: + return val.asBigFloat().Cmp(&big.Float{}) != 0 } return true } diff --git a/internal/dynamo-browse/models/queryexpr/context.go b/internal/dynamo-browse/models/queryexpr/context.go new file mode 100644 index 0000000..18c56ef --- /dev/null +++ b/internal/dynamo-browse/models/queryexpr/context.go @@ -0,0 +1,27 @@ +package queryexpr + +import ( + "context" + "time" +) + +type timeSource interface { + now() time.Time +} + +type defaultTimeSource struct{} + +func (tds defaultTimeSource) now() time.Time { + return time.Now() +} + +type timeSourceContextKeyType struct{} + +var timeSourceContextKey = timeSourceContextKeyType{} + +func timeSourceFromContext(ctx context.Context) timeSource { + if tts, ok := ctx.Value(timeSourceContextKey).(timeSource); ok { + return tts + } + return defaultTimeSource{} +} diff --git a/internal/dynamo-browse/models/queryexpr/disj.go b/internal/dynamo-browse/models/queryexpr/disj.go index 913a503..8819587 100644 --- a/internal/dynamo-browse/models/queryexpr/disj.go +++ b/internal/dynamo-browse/models/queryexpr/disj.go @@ -2,7 +2,6 @@ package queryexpr import ( "github.com/aws/aws-sdk-go-v2/feature/dynamodb/expression" - "github.com/aws/aws-sdk-go-v2/service/dynamodb/types" "github.com/lmika/audax/internal/dynamo-browse/models" "strings" ) @@ -24,7 +23,7 @@ func (a *astDisjunction) evalToIR(ctx *evalContext, tableInfo *models.TableInfo) return &irDisjunction{conj: conj}, nil } -func (a *astDisjunction) evalItem(ctx *evalContext, item models.Item) (types.AttributeValue, error) { +func (a *astDisjunction) evalItem(ctx *evalContext, item models.Item) (exprValue, error) { val, err := a.Operands[0].evalItem(ctx, item) if err != nil { return nil, err @@ -35,7 +34,7 @@ func (a *astDisjunction) evalItem(ctx *evalContext, item models.Item) (types.Att for _, opr := range a.Operands[1:] { if isAttributeTrue(val) { - return &types.AttributeValueMemberBOOL{Value: true}, nil + return boolExprValue(true), nil } val, err = opr.evalItem(ctx, item) @@ -44,7 +43,7 @@ func (a *astDisjunction) evalItem(ctx *evalContext, item models.Item) (types.Att } } - return &types.AttributeValueMemberBOOL{Value: isAttributeTrue(val)}, nil + return boolExprValue(isAttributeTrue(val)), nil } func (a *astDisjunction) canModifyItem(ctx *evalContext, item models.Item) bool { @@ -55,7 +54,7 @@ func (a *astDisjunction) canModifyItem(ctx *evalContext, item models.Item) bool return false } -func (a *astDisjunction) setEvalItem(ctx *evalContext, item models.Item, value types.AttributeValue) error { +func (a *astDisjunction) setEvalItem(ctx *evalContext, item models.Item, value exprValue) error { if len(a.Operands) == 1 { return a.Operands[0].setEvalItem(ctx, item, value) } diff --git a/internal/dynamo-browse/models/queryexpr/dot.go b/internal/dynamo-browse/models/queryexpr/dot.go index d2c18fc..97063c9 100644 --- a/internal/dynamo-browse/models/queryexpr/dot.go +++ b/internal/dynamo-browse/models/queryexpr/dot.go @@ -3,7 +3,6 @@ package queryexpr import ( "fmt" "github.com/aws/aws-sdk-go-v2/feature/dynamodb/expression" - "github.com/aws/aws-sdk-go-v2/service/dynamodb/types" "github.com/lmika/audax/internal/dynamo-browse/models" "strings" ) @@ -16,21 +15,21 @@ func (dt *astRef) unqualifiedName() (string, bool) { return dt.Name, true } -func (dt *astRef) evalItem(ctx *evalContext, item models.Item) (types.AttributeValue, error) { +func (dt *astRef) evalItem(ctx *evalContext, item models.Item) (exprValue, error) { res, hasV := item[dt.Name] if !hasV { return nil, nil } - return res, nil + return newExprValueFromAttributeValue(res) } func (dt *astRef) canModifyItem(ctx *evalContext, item models.Item) bool { return true } -func (dt *astRef) setEvalItem(ctx *evalContext, item models.Item, value types.AttributeValue) error { - item[dt.Name] = value +func (dt *astRef) setEvalItem(ctx *evalContext, item models.Item, value exprValue) error { + item[dt.Name] = value.asAttributeValue() return nil } @@ -71,7 +70,7 @@ func (i irNamePath) calcName(info *models.TableInfo) expression.NameBuilder { switch v := qual.(type) { case string: fullName.WriteString("." + v) - case int: + case int64: fullName.WriteString(fmt.Sprintf("[%v]", qual)) } } diff --git a/internal/dynamo-browse/models/queryexpr/equality.go b/internal/dynamo-browse/models/queryexpr/equality.go index 702754b..ffa26cd 100644 --- a/internal/dynamo-browse/models/queryexpr/equality.go +++ b/internal/dynamo-browse/models/queryexpr/equality.go @@ -2,7 +2,6 @@ package queryexpr import ( "github.com/aws/aws-sdk-go-v2/feature/dynamodb/expression" - "github.com/aws/aws-sdk-go-v2/service/dynamodb/types" "github.com/lmika/audax/internal/dynamo-browse/models" "github.com/lmika/audax/internal/dynamo-browse/models/attrutils" "github.com/pkg/errors" @@ -59,7 +58,7 @@ func (a *astEqualityOp) evalToIR(ctx *evalContext, info *models.TableInfo) (irAt return nil, errors.Errorf("unrecognised operator: %v", a.Op) } -func (a *astEqualityOp) evalItem(ctx *evalContext, item models.Item) (types.AttributeValue, error) { +func (a *astEqualityOp) evalItem(ctx *evalContext, item models.Item) (exprValue, error) { left, err := a.Ref.evalItem(ctx, item) if err != nil { return nil, err @@ -74,30 +73,32 @@ func (a *astEqualityOp) evalItem(ctx *evalContext, item models.Item) (types.Attr return nil, err } + // TODO: use expr values here + switch a.Op { case "=": - cmp, isComparable := attrutils.CompareScalarAttributes(left, right) + cmp, isComparable := attrutils.CompareScalarAttributes(left.asAttributeValue(), right.asAttributeValue()) if !isComparable { - return nil, ValuesNotComparable{Left: left, Right: right} + return nil, ValuesNotComparable{Left: left.asAttributeValue(), Right: right.asAttributeValue()} } - return &types.AttributeValueMemberBOOL{Value: cmp == 0}, nil + return boolExprValue(cmp == 0), nil case "!=": - cmp, isComparable := attrutils.CompareScalarAttributes(left, right) + cmp, isComparable := attrutils.CompareScalarAttributes(left.asAttributeValue(), right.asAttributeValue()) if !isComparable { - return nil, ValuesNotComparable{Left: left, Right: right} + return nil, ValuesNotComparable{Left: left.asAttributeValue(), Right: right.asAttributeValue()} } - return &types.AttributeValueMemberBOOL{Value: cmp != 0}, nil + return boolExprValue(cmp != 0), nil case "^=": - strValue, isStrValue := right.(*types.AttributeValueMemberS) + strValue, isStrValue := right.(stringableExprValue) if !isStrValue { return nil, errors.New("operand '^=' must be string") } - leftAsStr, canBeString := attrutils.AttributeToString(left) + leftAsStr, canBeString := left.(stringableExprValue) if !canBeString { - return nil, ValueNotConvertableToString{Val: left} + return nil, ValueNotConvertableToString{Val: leftAsStr.asAttributeValue()} } - return &types.AttributeValueMemberBOOL{Value: strings.HasPrefix(leftAsStr, strValue.Value)}, nil + return boolExprValue(strings.HasPrefix(leftAsStr.asString(), strValue.asString())), nil } return nil, errors.Errorf("unrecognised operator: %v", a.Op) @@ -110,7 +111,7 @@ func (a *astEqualityOp) canModifyItem(ctx *evalContext, item models.Item) bool { return a.Ref.canModifyItem(ctx, item) } -func (a *astEqualityOp) setEvalItem(ctx *evalContext, item models.Item, value types.AttributeValue) error { +func (a *astEqualityOp) setEvalItem(ctx *evalContext, item models.Item, value exprValue) error { if a.Op != "" { return PathNotSettableError{} } @@ -157,8 +158,8 @@ func (a irKeyFieldEq) calcQueryForScan(info *models.TableInfo) (expression.Condi } func (a irKeyFieldEq) calcQueryForQuery() (expression.KeyConditionBuilder, error) { - vb := a.value.goValue() - return expression.Key(a.name.keyName()).Equal(expression.Value(vb)), nil + vb := a.value.exprValue() + return expression.Key(a.name.keyName()).Equal(buildExpressionFromValue(vb)), nil } type irGenericEq struct { @@ -203,21 +204,21 @@ func (a irFieldBeginsWith) canBeExecutedAsQuery(qci *queryCalcInfo) bool { func (a irFieldBeginsWith) calcQueryForScan(info *models.TableInfo) (expression.ConditionBuilder, error) { nb := a.name.calcName(info) - vb := a.value.goValue() - strValue, isStrValue := vb.(string) + vb := a.value.exprValue() + strValue, isStrValue := vb.(stringableExprValue) if !isStrValue { return expression.ConditionBuilder{}, errors.New("operand '^=' must be string") } - return nb.BeginsWith(strValue), nil + return nb.BeginsWith(strValue.asString()), nil } func (a irFieldBeginsWith) calcQueryForQuery() (expression.KeyConditionBuilder, error) { - vb := a.value.goValue() - strValue, isStrValue := vb.(string) + vb := a.value.exprValue() + strValue, isStrValue := vb.(stringableExprValue) if !isStrValue { return expression.KeyConditionBuilder{}, errors.New("operand '^=' must be string") } - return expression.Key(a.name.keyName()).BeginsWith(strValue), nil + return expression.Key(a.name.keyName()).BeginsWith(strValue.asString()), nil } diff --git a/internal/dynamo-browse/models/queryexpr/errors.go b/internal/dynamo-browse/models/queryexpr/errors.go index 59f1ddb..8c48bbf 100644 --- a/internal/dynamo-browse/models/queryexpr/errors.go +++ b/internal/dynamo-browse/models/queryexpr/errors.go @@ -98,6 +98,14 @@ func (n InvalidTypeForIsError) Error() string { return "invalid type for 'is': " + n.TypeName } +type InvalidTypeForBetweenError struct { + TypeName string +} + +func (n InvalidTypeForBetweenError) Error() string { + return "invalid type for 'between': " + n.TypeName +} + type InvalidArgumentNumberError struct { Name string Expected int @@ -108,6 +116,16 @@ func (e InvalidArgumentNumberError) Error() string { return fmt.Sprintf("function '%v' expected %v args but received %v", e.Name, e.Expected, e.Actual) } +type InvalidArgumentTypeError struct { + Name string + ArgIndex int + Expected string +} + +func (e InvalidArgumentTypeError) Error() string { + return fmt.Sprintf("function '%v' expected arg %v to be of type %v", e.Name, e.ArgIndex, e.Expected) +} + type UnrecognisedFunctionError struct { Name string } @@ -137,3 +155,20 @@ type ValueNotUsableAsASubref struct { func (e ValueNotUsableAsASubref) Error() string { return "value cannot be used as a subref" } + +type MultiplePlansWithIndexError struct { + PossibleIndices []string +} + +func (e MultiplePlansWithIndexError) Error() string { + return fmt.Sprintf("multiple plans with index found. Specify index or scan with 'using' clause: possible indices are %v", e.PossibleIndices) +} + +type NoPlausiblePlanWithIndexError struct { + PreferredIndex string + PossibleIndices []string +} + +func (e NoPlausiblePlanWithIndexError) Error() string { + return fmt.Sprintf("no plan with index '%v' found: possible indices are %v", e.PreferredIndex, e.PossibleIndices) +} diff --git a/internal/dynamo-browse/models/queryexpr/expr.go b/internal/dynamo-browse/models/queryexpr/expr.go index aed149f..a68a275 100644 --- a/internal/dynamo-browse/models/queryexpr/expr.go +++ b/internal/dynamo-browse/models/queryexpr/expr.go @@ -16,12 +16,17 @@ import ( type QueryExpr struct { ast *astExpr + index string names map[string]string values map[string]types.AttributeValue + + // tests fields only + timeSource timeSource } type serializedExpr struct { Expr string + Index string Names map[string]string Values []byte } @@ -39,6 +44,7 @@ func DeserializeFrom(r io.Reader) (*QueryExpr, error) { } qe.names = se.Names + qe.index = se.Index if len(se.Values) > 0 { vals, err := attrcodec.NewDecoder(bytes.NewReader(se.Values)).Decode() @@ -56,7 +62,7 @@ func DeserializeFrom(r io.Reader) (*QueryExpr, error) { } func (md *QueryExpr) SerializeTo(w io.Writer) error { - se := serializedExpr{Expr: md.String(), Names: md.names} + se := serializedExpr{Expr: md.String(), Index: md.index, Names: md.names} if md.values != nil { var bts bytes.Buffer if err := attrcodec.NewEncoder(&bts).Encode(&types.AttributeValueMemberM{Value: md.values}); err != nil { @@ -90,6 +96,7 @@ func (md *QueryExpr) Equal(other *QueryExpr) bool { } return md.ast.String() == other.ast.String() && + md.index == other.index && maps.Equal(md.names, other.names) && maps.EqualFunc(md.values, md.values, attrutils.Equals) } @@ -104,6 +111,7 @@ func (md *QueryExpr) HashCode() uint64 { h := fnv.New64a() h.Write([]byte(md.ast.String())) + h.Write([]byte(md.index)) // the names must be in sorted order to maintain consistant key ordering if len(md.names) > 0 { @@ -134,6 +142,7 @@ func (md *QueryExpr) HashCode() uint64 { func (md *QueryExpr) WithNameParams(value map[string]string) *QueryExpr { return &QueryExpr{ ast: md.ast, + index: md.index, names: value, values: md.values, } @@ -158,17 +167,34 @@ func (md *QueryExpr) ValueParamOrNil(name string) types.AttributeValue { func (md *QueryExpr) WithValueParams(value map[string]types.AttributeValue) *QueryExpr { return &QueryExpr{ ast: md.ast, + index: md.index, names: md.names, values: value, } } +func (md *QueryExpr) WithIndex(index string) *QueryExpr { + return &QueryExpr{ + ast: md.ast, + index: index, + names: md.names, + values: md.values, + } +} + func (md *QueryExpr) Plan(tableInfo *models.TableInfo) (*models.QueryExecutionPlan, error) { - return md.ast.calcQuery(md.evalContext(), tableInfo) + return md.ast.calcQuery(md.evalContext(), tableInfo, md.index) } func (md *QueryExpr) EvalItem(item models.Item) (types.AttributeValue, error) { - return md.ast.evalItem(md.evalContext(), item) + val, err := md.ast.evalItem(md.evalContext(), item) + if err != nil { + return nil, err + } + if val == nil { + return nil, nil + } + return val.asAttributeValue(), nil } func (md *QueryExpr) DeleteAttribute(item models.Item) error { @@ -176,7 +202,11 @@ func (md *QueryExpr) DeleteAttribute(item models.Item) error { } func (md *QueryExpr) SetEvalItem(item models.Item, newValue types.AttributeValue) error { - return md.ast.setEvalItem(md.evalContext(), item, newValue) + val, err := newExprValueFromAttributeValue(newValue) + if err != nil { + return err + } + return md.ast.setEvalItem(md.evalContext(), item, val) } func (md *QueryExpr) IsModifiablePath(item models.Item) bool { @@ -237,6 +267,7 @@ type evalContext struct { nameLookup func(string) (string, bool) valuePlaceholders map[string]types.AttributeValue valueLookup func(string) (types.AttributeValue, bool) + timeSource timeSource } func (ec *evalContext) lookupName(name string) (string, bool) { @@ -264,3 +295,10 @@ func (ec *evalContext) lookupValue(name string) (types.AttributeValue, bool) { return nil, false } + +func (ec *evalContext) getTimeSource() timeSource { + if ts := ec.timeSource; ts != nil { + return ts + } + return defaultTimeSource{} +} diff --git a/internal/dynamo-browse/models/queryexpr/expr_test.go b/internal/dynamo-browse/models/queryexpr/expr_test.go index 5575f14..eb3a52e 100644 --- a/internal/dynamo-browse/models/queryexpr/expr_test.go +++ b/internal/dynamo-browse/models/queryexpr/expr_test.go @@ -7,6 +7,7 @@ import ( "github.com/aws/aws-sdk-go-v2/service/dynamodb/types" "github.com/lmika/audax/internal/dynamo-browse/models/queryexpr" "testing" + "time" "github.com/lmika/audax/internal/dynamo-browse/models" "github.com/stretchr/testify/assert" @@ -34,6 +35,13 @@ func TestModExpr_Query(t *testing.T) { SortKey: "sk", }, }, + { + Name: "with-apples-and-oranges", + Keys: models.KeyAttribute{ + PartitionKey: "apples", + SortKey: "oranges", + }, + }, }, } @@ -112,6 +120,13 @@ func TestModExpr_Query(t *testing.T) { exprNameIsString(0, 0, "pk", "prefix"), exprNameIsNumber(1, 1, "sk", "100"), ), + scanCase("when request pk is equals and sk is greater or equal to", + `pk="prefix" and sk between 100 and 200`, + `(#0 = :0) AND (#1 BETWEEN :1 AND :2)`, + exprNameIsString(0, 0, "pk", "prefix"), + exprNameIsNumber(1, 1, "sk", "100"), + exprValueIsNumber(2, "200"), + ), scanCase("with placeholders", `:partition=$valuePrefix and :sort=$valueAnother`, @@ -149,6 +164,13 @@ func TestModExpr_Query(t *testing.T) { exprNameIsString(0, 0, "color", "yellow"), exprNameIsString(1, 1, "shade", "dark"), ), + + // Function calls + scanCase("use the value of fn call in query", + `pk = _x_concat("Hello ", "world")`, + `#0 = :0`, + exprNameIsString(0, 0, "pk", "Hello world"), + ), } for _, scenario := range scenarios { @@ -238,6 +260,13 @@ func TestModExpr_Query(t *testing.T) { exprNameIsString(0, 0, "pk", "prefix"), ), + scanCase("with between", `pk between "a" and "z"`, + `#0 BETWEEN :0 AND :1`, + exprName(0, "pk"), + exprValueIsString(0, "a"), + exprValueIsString(1, "z"), + ), + scanCase("with in", `pk in ("alpha", "bravo", "charlie")`, `#0 IN (:0, :1, :2)`, exprName(0, "pk"), @@ -374,6 +403,53 @@ func TestModExpr_Query(t *testing.T) { }) } }) + + t.Run("with index clash", func(t *testing.T) { + t.Run("should return error if attempt to run query with two indices that can be chosen", func(t *testing.T) { + modExpr, err := queryexpr.Parse(`apples="this"`) + assert.NoError(t, err) + + _, err = modExpr.Plan(tableInfo) + assert.Error(t, err) + }) + + t.Run("should run as scan if explicitly forced to", func(t *testing.T) { + modExpr, err := queryexpr.Parse(`apples="this" using scan`) + assert.NoError(t, err) + + plan, err := modExpr.Plan(tableInfo) + assert.NoError(t, err) + assert.False(t, plan.CanQuery) + }) + + t.Run("should run as query with the 'with-apples' index", func(t *testing.T) { + modExpr, err := queryexpr.Parse(`apples="this" using index("with-apples")`) + assert.NoError(t, err) + + plan, err := modExpr.Plan(tableInfo) + assert.NoError(t, err) + assert.True(t, plan.CanQuery) + assert.Equal(t, "with-apples", plan.IndexName) + }) + + t.Run("should run as query with the 'with-apples-and-oranges' index", func(t *testing.T) { + modExpr, err := queryexpr.Parse(`apples="this" using index("with-apples-and-oranges")`) + assert.NoError(t, err) + + plan, err := modExpr.Plan(tableInfo) + assert.NoError(t, err) + assert.True(t, plan.CanQuery) + assert.Equal(t, "with-apples-and-oranges", plan.IndexName) + }) + + t.Run("should return error if the chosen index can't be used", func(t *testing.T) { + modExpr, err := queryexpr.Parse(`apples="this" using index("with-missing")`) + assert.NoError(t, err) + + _, err = modExpr.Plan(tableInfo) + assert.Error(t, err) + }) + }) } func TestQueryExpr_EvalItem(t *testing.T) { @@ -395,7 +471,9 @@ func TestQueryExpr_EvalItem(t *testing.T) { &types.AttributeValueMemberN{Value: "7"}, }, }, + "one": &types.AttributeValueMemberN{Value: "1"}, "three": &types.AttributeValueMemberN{Value: "3"}, + "five": &types.AttributeValueMemberN{Value: "5"}, } ) @@ -433,6 +511,19 @@ func TestQueryExpr_EvalItem(t *testing.T) { {expr: "three < 2", expected: &types.AttributeValueMemberBOOL{Value: false}}, {expr: "three <= 2", expected: &types.AttributeValueMemberBOOL{Value: false}}, + // Between + {expr: "three between 1 and 5", expected: &types.AttributeValueMemberBOOL{Value: true}}, + {expr: "three between one and five", expected: &types.AttributeValueMemberBOOL{Value: true}}, + {expr: "three between 10 and 15", expected: &types.AttributeValueMemberBOOL{Value: false}}, + {expr: "three between 1 and 2", expected: &types.AttributeValueMemberBOOL{Value: false}}, + {expr: "8 between five and 10", expected: &types.AttributeValueMemberBOOL{Value: true}}, + {expr: "three between 1 and 3", expected: &types.AttributeValueMemberBOOL{Value: true}}, + {expr: "three between 3 and 5", expected: &types.AttributeValueMemberBOOL{Value: true}}, + + {expr: `"e" between "a" and "z"`, expected: &types.AttributeValueMemberBOOL{Value: true}}, + {expr: `"eee" between "aaa" and "zzz"`, expected: &types.AttributeValueMemberBOOL{Value: true}}, + {expr: `"e" between "between" and "beyond"`, expected: &types.AttributeValueMemberBOOL{Value: false}}, + // In {expr: "three in (2, 3, 4, 5)", expected: &types.AttributeValueMemberBOOL{Value: true}}, {expr: "three in (20, 30, 40)", expected: &types.AttributeValueMemberBOOL{Value: false}}, @@ -509,6 +600,29 @@ func TestQueryExpr_EvalItem(t *testing.T) { } }) + t.Run("functions", func(t *testing.T) { + timeNow := time.Now() + + scenarios := []struct { + expr string + expected types.AttributeValue + }{ + // _x_now() -- unreleased version of now + {expr: `_x_now()`, expected: &types.AttributeValueMemberN{Value: fmt.Sprint(timeNow.Unix())}}, + } + for _, scenario := range scenarios { + t.Run(scenario.expr, func(t *testing.T) { + modExpr, err := queryexpr.Parse(scenario.expr) + assert.NoError(t, err) + + res, err := modExpr.WithTestTimeSource(timeNow).EvalItem(item) + assert.NoError(t, err) + + assert.Equal(t, scenario.expected, res) + }) + } + }) + t.Run("unparsed expression", func(t *testing.T) { scenarios := []struct { expr string diff --git a/internal/dynamo-browse/models/queryexpr/fncall.go b/internal/dynamo-browse/models/queryexpr/fncall.go index 358071e..1eff92d 100644 --- a/internal/dynamo-browse/models/queryexpr/fncall.go +++ b/internal/dynamo-browse/models/queryexpr/fncall.go @@ -3,7 +3,6 @@ package queryexpr import ( "context" "github.com/aws/aws-sdk-go-v2/feature/dynamodb/expression" - "github.com/aws/aws-sdk-go-v2/service/dynamodb/types" "github.com/lmika/audax/internal/common/sliceutils" "github.com/lmika/audax/internal/dynamo-browse/models" "github.com/pkg/errors" @@ -29,7 +28,7 @@ func (a *astFunctionCall) evalToIR(ctx *evalContext, info *models.TableInfo) (ir return nil, err } - // TODO: do this properly + // Special handling of functions that have IR nodes switch nameIr.keyName() { case "size": if len(irNodes) != 1 { @@ -40,20 +39,34 @@ func (a *astFunctionCall) evalToIR(ctx *evalContext, info *models.TableInfo) (ir return nil, OperandNotANameError(a.Args[0].String()) } return irSizeFn{name}, nil - case "range": - if len(irNodes) != 2 { - return nil, InvalidArgumentNumberError{Name: "range", Expected: 2, Actual: len(irNodes)} - } - - // TEMP - fromVal := irNodes[0].(valueIRAtom).goValue().(int64) - toVal := irNodes[1].(valueIRAtom).goValue().(int64) - return irRangeFn{fromVal, toVal}, nil } - return nil, UnrecognisedFunctionError{Name: nameIr.keyName()} + + builtinFn, hasBuiltin := nativeFuncs[nameIr.keyName()] + if !hasBuiltin { + return nil, UnrecognisedFunctionError{Name: nameIr.keyName()} + } + + // Normal functions which are evaluated to regular values + irValues, err := sliceutils.MapWithError(irNodes, func(a irAtom) (exprValue, error) { + v, isV := a.(valueIRAtom) + if !isV { + return nil, errors.New("cannot use value") + } + return v.exprValue(), nil + }) + if err != nil { + return nil, err + } + + val, err := builtinFn(context.Background(), irValues) + if err != nil { + return nil, err + } + + return irValue{value: val}, nil } -func (a *astFunctionCall) evalItem(ctx *evalContext, item models.Item) (types.AttributeValue, error) { +func (a *astFunctionCall) evalItem(ctx *evalContext, item models.Item) (exprValue, error) { if !a.IsCall { return a.Caller.evalItem(ctx, item) } @@ -67,14 +80,15 @@ func (a *astFunctionCall) evalItem(ctx *evalContext, item models.Item) (types.At return nil, UnrecognisedFunctionError{Name: name} } - args, err := sliceutils.MapWithError(a.Args, func(a *astExpr) (types.AttributeValue, error) { + args, err := sliceutils.MapWithError(a.Args, func(a *astExpr) (exprValue, error) { return a.evalItem(ctx, item) }) if err != nil { return nil, err } - return fn(context.Background(), args) + cCtx := context.WithValue(context.Background(), timeSourceContextKey, ctx.timeSource) + return fn(cCtx, args) } func (a *astFunctionCall) canModifyItem(ctx *evalContext, item models.Item) bool { @@ -85,7 +99,7 @@ func (a *astFunctionCall) canModifyItem(ctx *evalContext, item models.Item) bool return a.Caller.canModifyItem(ctx, item) } -func (a *astFunctionCall) setEvalItem(ctx *evalContext, item models.Item, value types.AttributeValue) error { +func (a *astFunctionCall) setEvalItem(ctx *evalContext, item models.Item, value exprValue) error { // TODO: Should a function vall return an item? if a.IsCall { return PathNotSettableError{} @@ -147,3 +161,15 @@ func (i irRangeFn) calcGoValues(info *models.TableInfo) ([]any, error) { } return xs, nil } + +type multiValueFnResult struct { + items []any +} + +func (i multiValueFnResult) calcQueryForScan(info *models.TableInfo) (expression.ConditionBuilder, error) { + return expression.ConditionBuilder{}, errors.New("cannot run as scan") +} + +func (i multiValueFnResult) calcGoValues(info *models.TableInfo) ([]any, error) { + return i.items, nil +} diff --git a/internal/dynamo-browse/models/queryexpr/helpers_test.go b/internal/dynamo-browse/models/queryexpr/helpers_test.go new file mode 100644 index 0000000..e856529 --- /dev/null +++ b/internal/dynamo-browse/models/queryexpr/helpers_test.go @@ -0,0 +1,16 @@ +package queryexpr + +import ( + "time" +) + +type testTimeSource time.Time + +func (tds testTimeSource) now() time.Time { + return time.Time(tds) +} + +func (a *QueryExpr) WithTestTimeSource(timeNow time.Time) *QueryExpr { + a.timeSource = testTimeSource(timeNow) + return a +} diff --git a/internal/dynamo-browse/models/queryexpr/in.go b/internal/dynamo-browse/models/queryexpr/in.go index 6a169d3..18d25dd 100644 --- a/internal/dynamo-browse/models/queryexpr/in.go +++ b/internal/dynamo-browse/models/queryexpr/in.go @@ -1,10 +1,7 @@ package queryexpr import ( - "bytes" - "fmt" "github.com/aws/aws-sdk-go-v2/feature/dynamodb/expression" - "github.com/aws/aws-sdk-go-v2/service/dynamodb/types" "github.com/lmika/audax/internal/common/sliceutils" "github.com/lmika/audax/internal/dynamo-browse/models" "github.com/lmika/audax/internal/dynamo-browse/models/attrutils" @@ -71,6 +68,13 @@ func (a *astIn) evalToIR(ctx *evalContext, info *models.TableInfo) (irAtom, erro return nil, OperandNotANameError(a.Ref.String()) } ir = irContains{needle: lit, haystack: t} + case valueIRAtom: + nameIR, isNameIR := leftIR.(irNamePath) + if !isNameIR { + return nil, OperandNotANameError(a.Ref.String()) + } + + ir = irLiteralValues{name: nameIR, values: t} case oprIRAtom: nameIR, isNameIR := leftIR.(irNamePath) if !isNameIR { @@ -78,13 +82,6 @@ func (a *astIn) evalToIR(ctx *evalContext, info *models.TableInfo) (irAtom, erro } ir = irIn{name: nameIR, values: []oprIRAtom{t}} - case multiValueIRAtom: - nameIR, isNameIR := leftIR.(irNamePath) - if !isNameIR { - return nil, OperandNotANameError(a.Ref.String()) - } - - ir = irLiteralValues{name: nameIR, values: t} default: return nil, OperandNotAnOperandError{} } @@ -96,7 +93,7 @@ func (a *astIn) evalToIR(ctx *evalContext, info *models.TableInfo) (irAtom, erro return ir, nil } -func (a *astIn) evalItem(ctx *evalContext, item models.Item) (types.AttributeValue, error) { +func (a *astIn) evalItem(ctx *evalContext, item models.Item) (exprValue, error) { val, err := a.Ref.evalItem(ctx, item) if err != nil { return nil, err @@ -112,14 +109,15 @@ func (a *astIn) evalItem(ctx *evalContext, item models.Item) (types.AttributeVal if err != nil { return nil, err } - cmp, isComparable := attrutils.CompareScalarAttributes(val, evalOp) + // TODO: use native types here + cmp, isComparable := attrutils.CompareScalarAttributes(val.asAttributeValue(), evalOp.asAttributeValue()) if !isComparable { continue } else if cmp == 0 { - return &types.AttributeValueMemberBOOL{Value: true}, nil + return boolExprValue(true), nil } } - return &types.AttributeValueMemberBOOL{Value: false}, nil + return boolExprValue(false), nil case a.SingleOperand != nil: evalOp, err := a.SingleOperand.evalItem(ctx, item) if err != nil { @@ -127,69 +125,38 @@ func (a *astIn) evalItem(ctx *evalContext, item models.Item) (types.AttributeVal } switch t := evalOp.(type) { - case *types.AttributeValueMemberS: - str, canToStr := attrutils.AttributeToString(val) + case stringableExprValue: + str, canToStr := val.(stringableExprValue) if !canToStr { - return &types.AttributeValueMemberBOOL{Value: false}, nil + return boolExprValue(false), nil } - return &types.AttributeValueMemberBOOL{Value: strings.Contains(t.Value, str)}, nil - case *types.AttributeValueMemberL: - for _, listItem := range t.Value { - cmp, isComparable := attrutils.CompareScalarAttributes(val, listItem) + return boolExprValue(strings.Contains(t.asString(), str.asString())), nil + case slicableExprValue: + for i := 0; i < t.len(); i++ { + va, err := t.valueAt(i) + if err != nil { + return nil, err + } + + // TODO: use expr value types here + cmp, isComparable := attrutils.CompareScalarAttributes(val.asAttributeValue(), va.asAttributeValue()) if !isComparable { continue } else if cmp == 0 { - return &types.AttributeValueMemberBOOL{Value: true}, nil + return boolExprValue(true), nil } } - return &types.AttributeValueMemberBOOL{Value: false}, nil - case *types.AttributeValueMemberSS: - str, canToStr := attrutils.AttributeToString(val) + return boolExprValue(false), nil + case mappableExprValue: + str, canToStr := val.(stringableExprValue) if !canToStr { - return &types.AttributeValueMemberBOOL{Value: false}, nil + return boolExprValue(false), nil } - - for _, listItem := range t.Value { - if str != listItem { - return &types.AttributeValueMemberBOOL{Value: false}, nil - } - } - return &types.AttributeValueMemberBOOL{Value: true}, nil - case *types.AttributeValueMemberBS: - b, isB := val.(*types.AttributeValueMemberB) - if !isB { - return &types.AttributeValueMemberBOOL{Value: false}, nil - } - - for _, listItem := range t.Value { - if !bytes.Equal(b.Value, listItem) { - return &types.AttributeValueMemberBOOL{Value: false}, nil - } - } - return &types.AttributeValueMemberBOOL{Value: true}, nil - case *types.AttributeValueMemberNS: - n, isN := val.(*types.AttributeValueMemberN) - if !isN { - return &types.AttributeValueMemberBOOL{Value: false}, nil - } - - for _, listItem := range t.Value { - // TODO: this is not actually right - if n.Value != listItem { - return &types.AttributeValueMemberBOOL{Value: false}, nil - } - } - return &types.AttributeValueMemberBOOL{Value: true}, nil - case *types.AttributeValueMemberM: - str, canToStr := attrutils.AttributeToString(val) - if !canToStr { - return &types.AttributeValueMemberBOOL{Value: false}, nil - } - _, hasItem := t.Value[str] - return &types.AttributeValueMemberBOOL{Value: hasItem}, nil + hasKey := t.hasKey(str.asString()) + return boolExprValue(hasKey), nil } - return nil, ValuesNotInnableError{Val: evalOp} + return nil, ValuesNotInnableError{Val: evalOp.asAttributeValue()} } return nil, errors.New("internal error: unhandled 'in' case") } @@ -201,7 +168,7 @@ func (a *astIn) canModifyItem(ctx *evalContext, item models.Item) bool { return a.Ref.canModifyItem(ctx, item) } -func (a *astIn) setEvalItem(ctx *evalContext, item models.Item, value types.AttributeValue) error { +func (a *astIn) setEvalItem(ctx *evalContext, item models.Item, value exprValue) error { if len(a.Operand) != 0 || a.SingleOperand != nil { return PathNotSettableError{} } @@ -263,19 +230,38 @@ func (i irIn) calcQueryForScan(info *models.TableInfo) (expression.ConditionBuil type irLiteralValues struct { name nameIRAtom - values multiValueIRAtom + values valueIRAtom } -func (i irLiteralValues) calcQueryForScan(info *models.TableInfo) (expression.ConditionBuilder, error) { - vals, err := i.values.calcGoValues(info) - if err != nil { - return expression.ConditionBuilder{}, err +func (iv irLiteralValues) calcQueryForScan(info *models.TableInfo) (expression.ConditionBuilder, error) { + if sliceable, isSliceable := iv.values.exprValue().(slicableExprValue); isSliceable { + if sliceable.len() == 1 { + va, err := sliceable.valueAt(0) + if err != nil { + return expression.ConditionBuilder{}, err + } + + return iv.name.calcName(info).In(buildExpressionFromValue(va)), nil + } else if sliceable.len() == 0 { + // name is not in an empty slice, so this branch always evaluates to false + // TODO: would be better to not even include this branch in some way? + return expression.Equal(expression.Value(false), expression.Value(true)), nil + } + + items := make([]expression.OperandBuilder, sliceable.len()) + for i := 0; i < sliceable.len(); i++ { + va, err := sliceable.valueAt(i) + if err != nil { + return expression.ConditionBuilder{}, err + } + + items[i] = buildExpressionFromValue(va) + } + + return iv.name.calcName(info).In(items[0], items[1:]...), nil } - oprValues := sliceutils.Map(vals, func(t any) expression.OperandBuilder { - return expression.Value(t) - }) - return i.name.calcName(info).In(oprValues[0], oprValues[1:]...), nil + return iv.name.calcName(info).In(buildExpressionFromValue(iv.values.exprValue())), nil } type irContains struct { @@ -284,8 +270,11 @@ type irContains struct { } func (i irContains) calcQueryForScan(info *models.TableInfo) (expression.ConditionBuilder, error) { - needle := i.needle.goValue() - haystack := i.haystack.calcName(info) + strNeedle, isString := i.needle.exprValue().(stringableExprValue) + if !isString { + return expression.ConditionBuilder{}, errors.New("value cannot be converted to string") + } - return haystack.Contains(fmt.Sprint(needle)), nil + haystack := i.haystack.calcName(info) + return haystack.Contains(strNeedle.asString()), nil } diff --git a/internal/dynamo-browse/models/queryexpr/ir.go b/internal/dynamo-browse/models/queryexpr/ir.go index db9c218..a8dddbd 100644 --- a/internal/dynamo-browse/models/queryexpr/ir.go +++ b/internal/dynamo-browse/models/queryexpr/ir.go @@ -36,11 +36,7 @@ type nameIRAtom interface { type valueIRAtom interface { oprIRAtom - goValue() any -} - -type multiValueIRAtom interface { - calcGoValues(info *models.TableInfo) ([]any, error) + exprValue() exprValue } func canExecuteAsQuery(ir irAtom, qci *queryCalcInfo) bool { diff --git a/internal/dynamo-browse/models/queryexpr/is.go b/internal/dynamo-browse/models/queryexpr/is.go index 50daf6b..18e40d5 100644 --- a/internal/dynamo-browse/models/queryexpr/is.go +++ b/internal/dynamo-browse/models/queryexpr/is.go @@ -2,9 +2,7 @@ package queryexpr import ( "github.com/aws/aws-sdk-go-v2/feature/dynamodb/expression" - "github.com/aws/aws-sdk-go-v2/service/dynamodb/types" "github.com/lmika/audax/internal/dynamo-browse/models" - "github.com/lmika/audax/internal/dynamo-browse/models/attrutils" "reflect" "strings" ) @@ -12,50 +10,50 @@ import ( type isTypeInfo struct { isAny bool attributeType expression.DynamoDBAttributeType - goType reflect.Type + goTypes []reflect.Type } var validIsTypeNames = map[string]isTypeInfo{ "ANY": {isAny: true}, "B": { attributeType: expression.Binary, - goType: reflect.TypeOf(&types.AttributeValueMemberB{}), + // TODO }, "BOOL": { attributeType: expression.Boolean, - goType: reflect.TypeOf(&types.AttributeValueMemberBOOL{}), + goTypes: []reflect.Type{reflect.TypeOf(boolExprValue(false))}, }, "S": { attributeType: expression.String, - goType: reflect.TypeOf(&types.AttributeValueMemberS{}), + goTypes: []reflect.Type{reflect.TypeOf(stringExprValue(""))}, }, "N": { attributeType: expression.Number, - goType: reflect.TypeOf(&types.AttributeValueMemberN{}), + goTypes: []reflect.Type{reflect.TypeOf(int64ExprValue(0)), reflect.TypeOf(bigNumExprValue{})}, }, "NULL": { attributeType: expression.Null, - goType: reflect.TypeOf(&types.AttributeValueMemberNULL{}), + goTypes: []reflect.Type{reflect.TypeOf(nullExprValue{})}, }, "L": { attributeType: expression.List, - goType: reflect.TypeOf(&types.AttributeValueMemberL{}), + goTypes: []reflect.Type{reflect.TypeOf(listExprValue{}), reflect.TypeOf(listProxyValue{})}, }, "M": { attributeType: expression.Map, - goType: reflect.TypeOf(&types.AttributeValueMemberM{}), + goTypes: []reflect.Type{reflect.TypeOf(mapExprValue{}), reflect.TypeOf(mapProxyValue{})}, }, "BS": { attributeType: expression.BinarySet, - goType: reflect.TypeOf(&types.AttributeValueMemberBS{}), + // TODO }, "NS": { attributeType: expression.NumberSet, - goType: reflect.TypeOf(&types.AttributeValueMemberNS{}), + // TODO }, "SS": { attributeType: expression.StringSet, - goType: reflect.TypeOf(&types.AttributeValueMemberSS{}), + // TODO }, } @@ -83,14 +81,14 @@ func (a *astIsOp) evalToIR(ctx *evalContext, info *models.TableInfo) (irAtom, er if !isValueIR { return nil, ValueMustBeLiteralError{} } - strValue, isStringValue := valueIR.goValue().(string) + strValue, isStringValue := valueIR.exprValue().(stringableExprValue) if !isStringValue { return nil, ValueMustBeStringError{} } - typeInfo, isValidType := validIsTypeNames[strings.ToUpper(strValue)] + typeInfo, isValidType := validIsTypeNames[strings.ToUpper(strValue.asString())] if !isValidType { - return nil, InvalidTypeForIsError{TypeName: strValue} + return nil, InvalidTypeForIsError{TypeName: strValue.asString()} } var ir = irIs{name: nameIR, typeInfo: typeInfo} @@ -104,7 +102,7 @@ func (a *astIsOp) evalToIR(ctx *evalContext, info *models.TableInfo) (irAtom, er return ir, nil } -func (a *astIsOp) evalItem(ctx *evalContext, item models.Item) (types.AttributeValue, error) { +func (a *astIsOp) evalItem(ctx *evalContext, item models.Item) (exprValue, error) { ref, err := a.Ref.evalItem(ctx, item) if err != nil { return nil, err @@ -118,13 +116,13 @@ func (a *astIsOp) evalItem(ctx *evalContext, item models.Item) (types.AttributeV if err != nil { return nil, err } - str, canToStr := attrutils.AttributeToString(expTypeVal) + str, canToStr := expTypeVal.(stringableExprValue) if !canToStr { return nil, ValueMustBeStringError{} } - typeInfo, hasTypeInfo := validIsTypeNames[strings.ToUpper(str)] + typeInfo, hasTypeInfo := validIsTypeNames[strings.ToUpper(str.asString())] if !hasTypeInfo { - return nil, InvalidTypeForIsError{TypeName: str} + return nil, InvalidTypeForIsError{TypeName: str.asString()} } var resultOfIs bool @@ -132,12 +130,18 @@ func (a *astIsOp) evalItem(ctx *evalContext, item models.Item) (types.AttributeV resultOfIs = ref != nil } else { refType := reflect.TypeOf(ref) - resultOfIs = typeInfo.goType.AssignableTo(refType) + + for _, t := range typeInfo.goTypes { + if t.AssignableTo(refType) { + resultOfIs = true + break + } + } } if a.HasNot { resultOfIs = !resultOfIs } - return &types.AttributeValueMemberBOOL{Value: resultOfIs}, nil + return boolExprValue(resultOfIs), nil } func (a *astIsOp) canModifyItem(ctx *evalContext, item models.Item) bool { @@ -147,7 +151,7 @@ func (a *astIsOp) canModifyItem(ctx *evalContext, item models.Item) bool { return a.Ref.canModifyItem(ctx, item) } -func (a *astIsOp) setEvalItem(ctx *evalContext, item models.Item, value types.AttributeValue) error { +func (a *astIsOp) setEvalItem(ctx *evalContext, item models.Item, value exprValue) error { if a.Value != nil { return PathNotSettableError{} } diff --git a/internal/dynamo-browse/models/queryexpr/placeholder.go b/internal/dynamo-browse/models/queryexpr/placeholder.go index ec94a83..1f6ffdb 100644 --- a/internal/dynamo-browse/models/queryexpr/placeholder.go +++ b/internal/dynamo-browse/models/queryexpr/placeholder.go @@ -1,7 +1,6 @@ package queryexpr import ( - "github.com/aws/aws-sdk-go-v2/service/dynamodb/types" "github.com/lmika/audax/internal/dynamo-browse/models" "github.com/pkg/errors" ) @@ -21,7 +20,12 @@ func (p *astPlaceholder) evalToIR(ctx *evalContext, info *models.TableInfo) (irA return nil, MissingPlaceholderError{Placeholder: p.Placeholder} } - return irValue{value: val}, nil + ev, err := newExprValueFromAttributeValue(val) + if err != nil { + return nil, err + } + + return irValue{value: ev}, nil } else if placeholderType == namePlaceholderPrefix { name, hasName := ctx.lookupName(placeholder) if !hasName { @@ -34,7 +38,7 @@ func (p *astPlaceholder) evalToIR(ctx *evalContext, info *models.TableInfo) (irA return nil, errors.New("unrecognised placeholder") } -func (p *astPlaceholder) evalItem(ctx *evalContext, item models.Item) (types.AttributeValue, error) { +func (p *astPlaceholder) evalItem(ctx *evalContext, item models.Item) (exprValue, error) { placeholderType := p.Placeholder[0] placeholder := p.Placeholder[1:] @@ -43,7 +47,7 @@ func (p *astPlaceholder) evalItem(ctx *evalContext, item models.Item) (types.Att if !hasVal { return nil, MissingPlaceholderError{Placeholder: p.Placeholder} } - return val, nil + return newExprValueFromAttributeValue(val) } else if placeholderType == namePlaceholderPrefix { name, hasName := ctx.lookupName(placeholder) if !hasName { @@ -55,7 +59,7 @@ func (p *astPlaceholder) evalItem(ctx *evalContext, item models.Item) (types.Att return nil, nil } - return res, nil + return newExprValueFromAttributeValue(res) } return nil, errors.New("unrecognised placeholder") @@ -66,7 +70,7 @@ func (p *astPlaceholder) canModifyItem(ctx *evalContext, item models.Item) bool return placeholderType == namePlaceholderPrefix } -func (p *astPlaceholder) setEvalItem(ctx *evalContext, item models.Item, value types.AttributeValue) error { +func (p *astPlaceholder) setEvalItem(ctx *evalContext, item models.Item, value exprValue) error { placeholderType := p.Placeholder[0] placeholder := p.Placeholder[1:] @@ -78,7 +82,7 @@ func (p *astPlaceholder) setEvalItem(ctx *evalContext, item models.Item, value t return MissingPlaceholderError{Placeholder: p.Placeholder} } - item[name] = value + item[name] = value.asAttributeValue() return nil } diff --git a/internal/dynamo-browse/models/queryexpr/subref.go b/internal/dynamo-browse/models/queryexpr/subref.go index 0674fcb..0e3c8f9 100644 --- a/internal/dynamo-browse/models/queryexpr/subref.go +++ b/internal/dynamo-browse/models/queryexpr/subref.go @@ -1,10 +1,8 @@ package queryexpr import ( - "github.com/aws/aws-sdk-go-v2/service/dynamodb/types" "github.com/lmika/audax/internal/common/sliceutils" "github.com/lmika/audax/internal/dynamo-browse/models" - "strconv" "strings" ) @@ -34,7 +32,7 @@ func (r *astSubRef) evalToIR(ctx *evalContext, info *models.TableInfo) (irAtom, return irNamePath{name: namePath.name, quals: quals}, nil } -func (r *astSubRef) evalItem(ctx *evalContext, item models.Item) (types.AttributeValue, error) { +func (r *astSubRef) evalItem(ctx *evalContext, item models.Item) (exprValue, error) { res, err := r.Ref.evalItem(ctx, item) if err != nil { return nil, err @@ -48,7 +46,7 @@ func (r *astSubRef) evalItem(ctx *evalContext, item models.Item) (types.Attribut return res, nil } -func (r *astSubRef) evalSubRefs(ctx *evalContext, item models.Item, res types.AttributeValue, subRefs []*astSubRefType) (types.AttributeValue, error) { +func (r *astSubRef) evalSubRefs(ctx *evalContext, item models.Item, res exprValue, subRefs []*astSubRefType) (exprValue, error) { for i, sr := range subRefs { sv, err := sr.evalToStrOrInt(ctx, nil) if err != nil { @@ -57,24 +55,30 @@ func (r *astSubRef) evalSubRefs(ctx *evalContext, item models.Item, res types.At switch val := sv.(type) { case string: - var hasV bool - mapRes, isMapRes := res.(*types.AttributeValueMemberM) + mapRes, isMapRes := res.(mappableExprValue) if !isMapRes { return nil, newValueNotAMapError(r, subRefs[:i+1]) } - res, hasV = mapRes.Value[val] - if !hasV { - return nil, nil + if mapRes.hasKey(val) { + res, err = mapRes.valueOf(val) + if err != nil { + return nil, err + } + } else { + res = nil } - case int: - listRes, isMapRes := res.(*types.AttributeValueMemberL) + case int64: + listRes, isMapRes := res.(slicableExprValue) if !isMapRes { return nil, newValueNotAListError(r, subRefs[:i+1]) } - // TODO - deal with index properly - res = listRes.Value[val] + // TODO - deal with index properly (i.e. error handling) + res, err = listRes.valueAt(int(val)) + if err != nil { + return nil, err + } } } return res, nil @@ -84,7 +88,7 @@ func (r *astSubRef) canModifyItem(ctx *evalContext, item models.Item) bool { return r.Ref.canModifyItem(ctx, item) } -func (r *astSubRef) setEvalItem(ctx *evalContext, item models.Item, value types.AttributeValue) error { +func (r *astSubRef) setEvalItem(ctx *evalContext, item models.Item, value exprValue) error { if len(r.SubRefs) == 0 { return r.Ref.setEvalItem(ctx, item, value) } @@ -108,20 +112,19 @@ func (r *astSubRef) setEvalItem(ctx *evalContext, item models.Item, value types. switch val := sv.(type) { case string: - mapRes, isMapRes := parentItem.(*types.AttributeValueMemberM) + mapRes, isMapRes := parentItem.(modifiableMapExprValue) if !isMapRes { return newValueNotAMapError(r, r.SubRefs) } - mapRes.Value[val] = value - case int: - listRes, isMapRes := parentItem.(*types.AttributeValueMemberL) + mapRes.setValueOf(val, value) + case int64: + listRes, isMapRes := parentItem.(modifiableSliceExprValue) if !isMapRes { return newValueNotAListError(r, r.SubRefs) } - // TODO: handle indexes - listRes.Value[val] = value + listRes.setValueAt(int(val), value) } return nil } @@ -136,20 +139,6 @@ func (r *astSubRef) deleteAttribute(ctx *evalContext, item models.Item) error { return err } - /* - for i, key := range r.Quals { - mapItem, isMapItem := parentItem.(*types.AttributeValueMemberM) - if !isMapItem { - return PathNotSettableError{} - } - - if isLast := i == len(r.Quals)-1; isLast { - delete(mapItem.Value, key) - } else { - parentItem = mapItem.Value[key] - } - } - */ if len(r.SubRefs) > 1 { parentItem, err = r.evalSubRefs(ctx, item, parentItem, r.SubRefs[0:len(r.SubRefs)-1]) if err != nil { @@ -164,23 +153,20 @@ func (r *astSubRef) deleteAttribute(ctx *evalContext, item models.Item) error { switch val := sv.(type) { case string: - mapRes, isMapRes := parentItem.(*types.AttributeValueMemberM) + mapRes, isMapRes := parentItem.(modifiableMapExprValue) if !isMapRes { return newValueNotAMapError(r, r.SubRefs) } - delete(mapRes.Value, val) - case int: - listRes, isMapRes := parentItem.(*types.AttributeValueMemberL) + mapRes.deleteValueOf(val) + case int64: + listRes, isMapRes := parentItem.(modifiableSliceExprValue) if !isMapRes { return newValueNotAListError(r, r.SubRefs) } // TODO: handle indexes out of bounds - oldList := listRes.Value - newList := append([]types.AttributeValue{}, oldList[:val]...) - newList = append(newList, oldList[val+1:]...) - listRes.Value = newList + listRes.deleteValueAt(int(val)) } return nil } @@ -214,18 +200,10 @@ func (sr *astSubRefType) evalToStrOrInt(ctx *evalContext, item models.Item) (any return nil, err } switch v := subEvalItem.(type) { - case *types.AttributeValueMemberS: - return v.Value, nil - case *types.AttributeValueMemberN: - intVal, err := strconv.Atoi(v.Value) - if err == nil { - return intVal, nil - } - flVal, err := strconv.ParseFloat(v.Value, 64) - if err == nil { - return int(flVal), nil - } - return nil, err + case stringableExprValue: + return v.asString(), nil + case numberableExprValue: + return v.asInt(), nil } return nil, ValueNotUsableAsASubref{} } diff --git a/internal/dynamo-browse/models/queryexpr/types.go b/internal/dynamo-browse/models/queryexpr/types.go index 7175665..1a4106a 100644 --- a/internal/dynamo-browse/models/queryexpr/types.go +++ b/internal/dynamo-browse/models/queryexpr/types.go @@ -1 +1,400 @@ package queryexpr + +import ( + "github.com/aws/aws-sdk-go-v2/feature/dynamodb/expression" + "github.com/aws/aws-sdk-go-v2/service/dynamodb/types" + "github.com/lmika/audax/internal/common/maputils" + "github.com/lmika/audax/internal/common/sliceutils" + "github.com/pkg/errors" + "math/big" + "strconv" +) + +type exprValue interface { + typeName() string + asGoValue() any + asAttributeValue() types.AttributeValue +} + +type stringableExprValue interface { + exprValue + asString() string +} + +type numberableExprValue interface { + exprValue + asBigFloat() *big.Float + asInt() int64 +} + +type slicableExprValue interface { + exprValue + len() int + valueAt(idx int) (exprValue, error) +} + +type modifiableSliceExprValue interface { + setValueAt(idx int, value exprValue) + deleteValueAt(idx int) +} + +type mappableExprValue interface { + len() int + hasKey(name string) bool + valueOf(name string) (exprValue, error) +} + +type modifiableMapExprValue interface { + setValueOf(name string, value exprValue) + deleteValueOf(name string) +} + +func buildExpressionFromValue(ev exprValue) expression.ValueBuilder { + return expression.Value(ev) +} + +func newExprValueFromAttributeValue(ev types.AttributeValue) (exprValue, error) { + if ev == nil { + return nil, nil + } + + switch xVal := ev.(type) { + case *types.AttributeValueMemberS: + return stringExprValue(xVal.Value), nil + case *types.AttributeValueMemberN: + xNumVal, _, err := big.ParseFloat(xVal.Value, 10, 63, big.ToNearestEven) + if err != nil { + return nil, err + } + return bigNumExprValue{num: xNumVal}, nil + case *types.AttributeValueMemberBOOL: + return boolExprValue(xVal.Value), nil + case *types.AttributeValueMemberNULL: + return nullExprValue{}, nil + case *types.AttributeValueMemberL: + return listProxyValue{list: xVal}, nil + case *types.AttributeValueMemberM: + return mapProxyValue{mapValue: xVal}, nil + case *types.AttributeValueMemberSS: + return stringSetProxyValue{stringSet: xVal}, nil + case *types.AttributeValueMemberNS: + return numberSetProxyValue{numberSet: xVal}, nil + } + return nil, errors.New("cannot convert to expr value") +} + +type stringExprValue string + +func (s stringExprValue) asGoValue() any { + return string(s) +} + +func (s stringExprValue) asAttributeValue() types.AttributeValue { + return &types.AttributeValueMemberS{Value: string(s)} +} + +func (s stringExprValue) asString() string { + return string(s) +} + +func (s stringExprValue) typeName() string { + return "S" +} + +type int64ExprValue int64 + +func (i int64ExprValue) asGoValue() any { + return int(i) +} + +func (i int64ExprValue) asAttributeValue() types.AttributeValue { + return &types.AttributeValueMemberN{Value: strconv.Itoa(int(i))} +} + +func (i int64ExprValue) asInt() int64 { + return int64(i) +} + +func (i int64ExprValue) asBigFloat() *big.Float { + var f big.Float + f.SetInt64(int64(i)) + return &f +} + +func (s int64ExprValue) typeName() string { + return "N" +} + +type bigNumExprValue struct { + num *big.Float +} + +func (i bigNumExprValue) asGoValue() any { + return i.num +} + +func (i bigNumExprValue) asAttributeValue() types.AttributeValue { + return &types.AttributeValueMemberN{Value: i.num.String()} +} + +func (i bigNumExprValue) asInt() int64 { + x, _ := i.num.Int64() + return x +} + +func (i bigNumExprValue) asBigFloat() *big.Float { + return i.num +} + +func (s bigNumExprValue) typeName() string { + return "N" +} + +type boolExprValue bool + +func (b boolExprValue) asGoValue() any { + return bool(b) +} + +func (b boolExprValue) asAttributeValue() types.AttributeValue { + return &types.AttributeValueMemberBOOL{Value: bool(b)} +} + +func (s boolExprValue) typeName() string { + return "BOOL" +} + +type nullExprValue struct{} + +func (b nullExprValue) asGoValue() any { + return nil +} + +func (b nullExprValue) asAttributeValue() types.AttributeValue { + return &types.AttributeValueMemberNULL{Value: true} +} + +func (s nullExprValue) typeName() string { + return "NULL" +} + +type listExprValue []exprValue + +func (bs listExprValue) asGoValue() any { + return sliceutils.Map(bs, func(t exprValue) any { + return t.asGoValue() + }) +} + +func (bs listExprValue) asAttributeValue() types.AttributeValue { + return &types.AttributeValueMemberL{Value: sliceutils.Map(bs, func(t exprValue) types.AttributeValue { + return t.asAttributeValue() + })} +} + +func (bs listExprValue) len() int { + return len(bs) +} + +func (bs listExprValue) valueAt(i int) (exprValue, error) { + return bs[i], nil +} + +func (s listExprValue) typeName() string { + return "L" +} + +type mapExprValue map[string]exprValue + +func (bs mapExprValue) asGoValue() any { + return maputils.MapValues(bs, func(t exprValue) any { + return t.asGoValue() + }) +} + +func (bs mapExprValue) asAttributeValue() types.AttributeValue { + return &types.AttributeValueMemberM{Value: maputils.MapValues(bs, func(t exprValue) types.AttributeValue { + return t.asAttributeValue() + })} +} + +func (bs mapExprValue) len() int { + return len(bs) +} + +func (bs mapExprValue) hasKey(name string) bool { + _, ok := bs[name] + return ok +} + +func (bs mapExprValue) valueOf(name string) (exprValue, error) { + return bs[name], nil +} + +func (s mapExprValue) typeName() string { + return "M" +} + +type listProxyValue struct { + list *types.AttributeValueMemberL +} + +func (bs listProxyValue) asGoValue() any { + resultingList := make([]any, len(bs.list.Value)) + for i, item := range bs.list.Value { + if av, _ := newExprValueFromAttributeValue(item); av != nil { + resultingList[i] = av.asGoValue() + } else { + resultingList[i] = nil + } + } + return resultingList +} + +func (bs listProxyValue) asAttributeValue() types.AttributeValue { + return bs.list +} + +func (bs listProxyValue) len() int { + return len(bs.list.Value) +} + +func (bs listProxyValue) valueAt(i int) (exprValue, error) { + return newExprValueFromAttributeValue(bs.list.Value[i]) +} + +func (bs listProxyValue) setValueAt(i int, newVal exprValue) { + bs.list.Value[i] = newVal.asAttributeValue() +} + +func (bs listProxyValue) deleteValueAt(idx int) { + newList := append([]types.AttributeValue{}, bs.list.Value[:idx]...) + newList = append(newList, bs.list.Value[idx+1:]...) + bs.list = &types.AttributeValueMemberL{Value: newList} +} + +func (s listProxyValue) typeName() string { + return "L" +} + +type mapProxyValue struct { + mapValue *types.AttributeValueMemberM +} + +func (bs mapProxyValue) asGoValue() any { + resultingMap := make(map[string]any) + for k, item := range bs.mapValue.Value { + if av, _ := newExprValueFromAttributeValue(item); av != nil { + resultingMap[k] = av.asGoValue() + } else { + resultingMap[k] = nil + } + } + return resultingMap +} + +func (bs mapProxyValue) asAttributeValue() types.AttributeValue { + return bs.mapValue +} + +func (bs mapProxyValue) len() int { + return len(bs.mapValue.Value) +} + +func (bs mapProxyValue) hasKey(name string) bool { + _, ok := bs.mapValue.Value[name] + return ok +} + +func (bs mapProxyValue) valueOf(name string) (exprValue, error) { + return newExprValueFromAttributeValue(bs.mapValue.Value[name]) +} + +func (bs mapProxyValue) setValueOf(name string, newVal exprValue) { + bs.mapValue.Value[name] = newVal.asAttributeValue() +} + +func (bs mapProxyValue) deleteValueOf(name string) { + delete(bs.mapValue.Value, name) +} + +func (s mapProxyValue) typeName() string { + return "M" +} + +type stringSetProxyValue struct { + stringSet *types.AttributeValueMemberSS +} + +func (bs stringSetProxyValue) asGoValue() any { + return bs.stringSet.Value +} + +func (bs stringSetProxyValue) asAttributeValue() types.AttributeValue { + return bs.stringSet +} + +func (bs stringSetProxyValue) len() int { + return len(bs.stringSet.Value) +} + +func (bs stringSetProxyValue) valueAt(i int) (exprValue, error) { + return stringExprValue(bs.stringSet.Value[i]), nil +} + +func (bs stringSetProxyValue) setValueAt(i int, newVal exprValue) { + if str, isStr := newVal.(stringableExprValue); isStr { + bs.stringSet.Value[i] = str.asString() + } +} + +func (bs stringSetProxyValue) deleteValueAt(idx int) { + newList := append([]string{}, bs.stringSet.Value[:idx]...) + newList = append(newList, bs.stringSet.Value[idx+1:]...) + bs.stringSet = &types.AttributeValueMemberSS{Value: newList} +} + +func (s stringSetProxyValue) typeName() string { + return "SS" +} + +type numberSetProxyValue struct { + numberSet *types.AttributeValueMemberNS +} + +func (bs numberSetProxyValue) asGoValue() any { + return bs.numberSet.Value +} + +func (bs numberSetProxyValue) asAttributeValue() types.AttributeValue { + return bs.numberSet +} + +func (bs numberSetProxyValue) len() int { + return len(bs.numberSet.Value) +} + +func (bs numberSetProxyValue) valueAt(i int) (exprValue, error) { + fs, _, err := big.ParseFloat(bs.numberSet.Value[i], 10, 63, big.ToNearestEven) + if err != nil { + return nil, err + } + + return bigNumExprValue{fs}, nil +} + +func (bs numberSetProxyValue) setValueAt(i int, newVal exprValue) { + if str, isStr := newVal.(numberableExprValue); isStr { + bs.numberSet.Value[i] = str.asBigFloat().String() + } +} + +func (bs numberSetProxyValue) deleteValueAt(idx int) { + newList := append([]string{}, bs.numberSet.Value[:idx]...) + newList = append(newList, bs.numberSet.Value[idx+1:]...) + bs.numberSet = &types.AttributeValueMemberNS{Value: newList} +} + +func (s numberSetProxyValue) typeName() string { + return "NS" +} diff --git a/internal/dynamo-browse/models/queryexpr/values.go b/internal/dynamo-browse/models/queryexpr/values.go index 4a81c45..9e97ba2 100644 --- a/internal/dynamo-browse/models/queryexpr/values.go +++ b/internal/dynamo-browse/models/queryexpr/values.go @@ -5,56 +5,31 @@ import ( "github.com/lmika/audax/internal/dynamo-browse/models" "strconv" - "github.com/aws/aws-sdk-go-v2/service/dynamodb/types" "github.com/pkg/errors" ) func (a *astLiteralValue) evalToIR(ctx *evalContext, info *models.TableInfo) (irAtom, error) { - v, err := a.goValue() + v, err := a.exprValue() if err != nil { return nil, err } return irValue{value: v}, nil } -func (a *astLiteralValue) dynamoValue() (types.AttributeValue, error) { - if a == nil { - return nil, nil - } - - goValue, err := a.goValue() - if err != nil { - return nil, err - } - - switch v := goValue.(type) { - case string: - return &types.AttributeValueMemberS{Value: v}, nil - case int64: - return &types.AttributeValueMemberN{Value: strconv.FormatInt(v, 10)}, nil - } - - return nil, errors.New("unrecognised type") -} - -func (a *astLiteralValue) goValue() (any, error) { - if a == nil { - return nil, nil - } - +func (a *astLiteralValue) exprValue() (exprValue, error) { switch { case a.StringVal != nil: s, err := strconv.Unquote(*a.StringVal) if err != nil { return nil, errors.Wrap(err, "cannot unquote string") } - return s, nil + return stringExprValue(s), nil case a.IntVal != nil: - return *a.IntVal, nil + return int64ExprValue(*a.IntVal), nil case a.TrueBoolValue: - return true, nil + return boolExprValue(true), nil case a.FalseBoolValue: - return false, nil + return boolExprValue(false), nil } return nil, errors.New("unrecognised type") } @@ -78,17 +53,17 @@ func (a *astLiteralValue) String() string { } type irValue struct { - value any + value exprValue } func (i irValue) calcQueryForScan(info *models.TableInfo) (expression.ConditionBuilder, error) { return expression.ConditionBuilder{}, NodeCannotBeConvertedToQueryError{} } -func (i irValue) goValue() any { +func (i irValue) exprValue() exprValue { return i.value } func (a irValue) calcOperand(info *models.TableInfo) expression.OperandBuilder { - return expression.Value(a.goValue()) + return expression.Value(a.value.asGoValue()) } diff --git a/internal/dynamo-browse/services/scriptmanager/iface.go b/internal/dynamo-browse/services/scriptmanager/iface.go index a717057..35dce04 100644 --- a/internal/dynamo-browse/services/scriptmanager/iface.go +++ b/internal/dynamo-browse/services/scriptmanager/iface.go @@ -32,6 +32,7 @@ type SessionService interface { type QueryOptions struct { TableName string + IndexName string NamePlaceholders map[string]string ValuePlaceholders map[string]types.AttributeValue } diff --git a/internal/dynamo-browse/services/scriptmanager/modsession.go b/internal/dynamo-browse/services/scriptmanager/modsession.go index 034b73f..d8e65b6 100644 --- a/internal/dynamo-browse/services/scriptmanager/modsession.go +++ b/internal/dynamo-browse/services/scriptmanager/modsession.go @@ -44,6 +44,11 @@ func (um *sessionModule) query(ctx context.Context, args ...object.Object) objec } } + // Index name + if val, isStr := objMap.Get("index").(*object.String); isStr { + options.IndexName = val.Value() + } + // Placeholders if argsVal, isArgsValMap := objMap.Get("args").(*object.Map); isArgsValMap { options.NamePlaceholders = make(map[string]string) diff --git a/internal/dynamo-browse/ui/model.go b/internal/dynamo-browse/ui/model.go index ef5d5da..43cf146 100644 --- a/internal/dynamo-browse/ui/model.go +++ b/internal/dynamo-browse/ui/model.go @@ -144,6 +144,8 @@ func NewModel( itemType = models.BoolItemType case "-NULL": itemType = models.NullItemType + case "-TO": + itemType = models.ExprValueItemType default: return events.Error(errors.New("unrecognised item type")) } diff --git a/test/cmd/load-test-table/main.go b/test/cmd/load-test-table/main.go index 3b730a4..5665378 100644 --- a/test/cmd/load-test-table/main.go +++ b/test/cmd/load-test-table/main.go @@ -91,10 +91,14 @@ func main() { } } - key := gofakeit.UUID() + var key = gofakeit.UUID() for i := 0; i < totalItems; i++ { + if i%50 == 0 { + key = gofakeit.UUID() + } if err := tableService.Put(ctx, inventoryTableInfo, models.Item{ "pk": &types.AttributeValueMemberS{Value: key}, + "sk": &types.AttributeValueMemberN{Value: fmt.Sprint(i % 50)}, "uuid": &types.AttributeValueMemberS{Value: gofakeit.UUID()}, }); err != nil { log.Fatalln(err) From f65c5778a9a321d3edc52cb8de020c3e0bc63f69 Mon Sep 17 00:00:00 2001 From: Leon Mika Date: Mon, 17 Apr 2023 08:31:03 +1000 Subject: [PATCH 02/10] issue-50: fixed package name (#52) Changed package name from github.com/lmika/audax to github.com/lmika/dynamo-browse --- cmd/dynamo-browse/main.go | 38 +++++++++---------- go.mod | 2 +- internal/common/ui/commandctrl/commandctrl.go | 2 +- .../common/ui/commandctrl/commandctrl_test.go | 6 +-- internal/common/ui/commandctrl/iface.go | 2 +- internal/common/ui/dispatcher/context.go | 2 +- internal/common/ui/dispatcher/dispatcher.go | 4 +- internal/common/ui/events/commands.go | 2 +- internal/common/ui/events/errors.go | 2 +- internal/dynamo-browse/controllers/columns.go | 8 ++-- .../dynamo-browse/controllers/commands.go | 2 +- internal/dynamo-browse/controllers/events.go | 2 +- internal/dynamo-browse/controllers/export.go | 4 +- internal/dynamo-browse/controllers/iface.go | 2 +- .../dynamo-browse/controllers/jobbuilder.go | 4 +- internal/dynamo-browse/controllers/jobs.go | 4 +- .../dynamo-browse/controllers/keybinding.go | 4 +- internal/dynamo-browse/controllers/scripts.go | 10 ++--- .../dynamo-browse/controllers/scripts_test.go | 4 +- .../dynamo-browse/controllers/settings.go | 2 +- .../controllers/settings_test.go | 4 +- internal/dynamo-browse/controllers/state.go | 2 +- .../dynamo-browse/controllers/tableread.go | 16 ++++---- .../controllers/tableread_test.go | 6 +-- .../dynamo-browse/controllers/tablewrite.go | 10 ++--- .../controllers/tablewrite_test.go | 30 +++++++-------- internal/dynamo-browse/controllers/utils.go | 2 +- .../models/attrcodec/codec_test.go | 2 +- .../dynamo-browse/models/columns/columns.go | 4 +- internal/dynamo-browse/models/items.go | 2 +- .../dynamo-browse/models/modexpr/astmods.go | 2 +- internal/dynamo-browse/models/modexpr/expr.go | 2 +- .../dynamo-browse/models/modexpr/expr_test.go | 4 +- internal/dynamo-browse/models/modexpr/mods.go | 2 +- internal/dynamo-browse/models/query.go | 2 +- .../dynamo-browse/models/queryexpr/ast.go | 4 +- .../dynamo-browse/models/queryexpr/atom.go | 2 +- .../dynamo-browse/models/queryexpr/between.go | 2 +- .../dynamo-browse/models/queryexpr/boolnot.go | 2 +- .../dynamo-browse/models/queryexpr/comp.go | 4 +- .../dynamo-browse/models/queryexpr/conj.go | 2 +- .../dynamo-browse/models/queryexpr/disj.go | 2 +- .../dynamo-browse/models/queryexpr/dot.go | 2 +- .../models/queryexpr/equality.go | 4 +- .../dynamo-browse/models/queryexpr/errors.go | 4 +- .../dynamo-browse/models/queryexpr/expr.go | 6 +-- .../models/queryexpr/expr_test.go | 4 +- .../dynamo-browse/models/queryexpr/fncall.go | 4 +- internal/dynamo-browse/models/queryexpr/in.go | 6 +-- internal/dynamo-browse/models/queryexpr/ir.go | 2 +- internal/dynamo-browse/models/queryexpr/is.go | 2 +- .../models/queryexpr/placeholder.go | 2 +- .../dynamo-browse/models/queryexpr/subref.go | 4 +- .../dynamo-browse/models/queryexpr/types.go | 4 +- .../dynamo-browse/models/queryexpr/values.go | 2 +- .../models/serialisable/viewsnapshot.go | 2 +- internal/dynamo-browse/models/sorted.go | 2 +- internal/dynamo-browse/models/sorted_test.go | 2 +- .../providers/dynamo/provider.go | 6 +-- .../providers/dynamo/provider_test.go | 6 +-- .../providers/inputhistorystore/store.go | 4 +- .../providers/settingstore/settingstore.go | 2 +- .../workspacestore/resultsetsnapshot.go | 4 +- .../services/inputhistory/iter.go | 2 +- .../services/itemrenderer/service.go | 4 +- .../services/scriptmanager/iface.go | 2 +- .../scriptmanager/mocks/SessionService.go | 4 +- .../services/scriptmanager/modos_test.go | 4 +- .../services/scriptmanager/modsession_test.go | 6 +-- .../services/scriptmanager/modui_test.go | 4 +- .../services/scriptmanager/resultsetproxy.go | 4 +- .../scriptmanager/resultsetproxy_test.go | 6 +-- .../services/scriptmanager/service.go | 2 +- .../services/scriptmanager/service_test.go | 4 +- .../services/scriptmanager/tableproxy.go | 4 +- .../services/scriptmanager/typemapping.go | 4 +- .../dynamo-browse/services/tables/iface.go | 2 +- .../dynamo-browse/services/tables/service.go | 6 +-- .../services/tables/service_test.go | 6 +-- .../services/viewsnapshot/iface.go | 2 +- .../services/viewsnapshot/service.go | 2 +- .../services/viewsnapshot/service_test.go | 10 ++--- internal/dynamo-browse/ui/model.go | 32 ++++++++-------- .../ui/teamodels/colselector/colmodel.go | 10 ++--- .../ui/teamodels/colselector/model.go | 8 ++-- .../ui/teamodels/dialogprompt/dialogmodel.go | 2 +- .../ui/teamodels/dialogprompt/model.go | 2 +- .../ui/teamodels/dynamoitemedit/model.go | 4 +- .../ui/teamodels/dynamoitemview/events.go | 2 +- .../ui/teamodels/dynamoitemview/model.go | 10 ++--- .../ui/teamodels/dynamotableview/model.go | 16 ++++---- .../ui/teamodels/dynamotableview/tblmodel.go | 4 +- .../dynamo-browse/ui/teamodels/frame/frame.go | 2 +- .../ui/teamodels/itemdisplay/model.go | 4 +- .../ui/teamodels/layout/model.go | 2 +- .../dynamo-browse/ui/teamodels/layout/vbox.go | 2 +- .../ui/teamodels/layout/zstack.go | 2 +- .../dynamo-browse/ui/teamodels/modal/model.go | 4 +- .../ui/teamodels/statusandprompt/model.go | 8 ++-- .../ui/teamodels/statusandprompt/types.go | 2 +- .../ui/teamodels/styles/styles.go | 4 +- .../ui/teamodels/tableselect/list.go | 2 +- .../ui/teamodels/tableselect/model.go | 12 +++--- test/cmd/load-test-table/main.go | 6 +-- test/testdynamo/helpers.go | 2 +- test/testuictx/testuictx.go | 4 +- test/testworkspace/workspace.go | 2 +- 107 files changed, 257 insertions(+), 257 deletions(-) diff --git a/cmd/dynamo-browse/main.go b/cmd/dynamo-browse/main.go index ffcebc6..1e0b05d 100644 --- a/cmd/dynamo-browse/main.go +++ b/cmd/dynamo-browse/main.go @@ -7,25 +7,25 @@ import ( "github.com/aws/aws-sdk-go-v2/config" "github.com/aws/aws-sdk-go-v2/service/dynamodb" tea "github.com/charmbracelet/bubbletea" - "github.com/lmika/audax/internal/common/ui/commandctrl" - "github.com/lmika/audax/internal/common/ui/logging" - "github.com/lmika/audax/internal/common/ui/osstyle" - "github.com/lmika/audax/internal/common/workspaces" - "github.com/lmika/audax/internal/dynamo-browse/controllers" - "github.com/lmika/audax/internal/dynamo-browse/providers/dynamo" - "github.com/lmika/audax/internal/dynamo-browse/providers/inputhistorystore" - "github.com/lmika/audax/internal/dynamo-browse/providers/settingstore" - "github.com/lmika/audax/internal/dynamo-browse/providers/workspacestore" - "github.com/lmika/audax/internal/dynamo-browse/services/inputhistory" - "github.com/lmika/audax/internal/dynamo-browse/services/itemrenderer" - "github.com/lmika/audax/internal/dynamo-browse/services/jobs" - keybindings_service "github.com/lmika/audax/internal/dynamo-browse/services/keybindings" - "github.com/lmika/audax/internal/dynamo-browse/services/scriptmanager" - "github.com/lmika/audax/internal/dynamo-browse/services/tables" - "github.com/lmika/audax/internal/dynamo-browse/services/viewsnapshot" - "github.com/lmika/audax/internal/dynamo-browse/ui" - "github.com/lmika/audax/internal/dynamo-browse/ui/keybindings" - "github.com/lmika/audax/internal/dynamo-browse/ui/teamodels/styles" + "github.com/lmika/dynamo-browse/internal/common/ui/commandctrl" + "github.com/lmika/dynamo-browse/internal/common/ui/logging" + "github.com/lmika/dynamo-browse/internal/common/ui/osstyle" + "github.com/lmika/dynamo-browse/internal/common/workspaces" + "github.com/lmika/dynamo-browse/internal/dynamo-browse/controllers" + "github.com/lmika/dynamo-browse/internal/dynamo-browse/providers/dynamo" + "github.com/lmika/dynamo-browse/internal/dynamo-browse/providers/inputhistorystore" + "github.com/lmika/dynamo-browse/internal/dynamo-browse/providers/settingstore" + "github.com/lmika/dynamo-browse/internal/dynamo-browse/providers/workspacestore" + "github.com/lmika/dynamo-browse/internal/dynamo-browse/services/inputhistory" + "github.com/lmika/dynamo-browse/internal/dynamo-browse/services/itemrenderer" + "github.com/lmika/dynamo-browse/internal/dynamo-browse/services/jobs" + keybindings_service "github.com/lmika/dynamo-browse/internal/dynamo-browse/services/keybindings" + "github.com/lmika/dynamo-browse/internal/dynamo-browse/services/scriptmanager" + "github.com/lmika/dynamo-browse/internal/dynamo-browse/services/tables" + "github.com/lmika/dynamo-browse/internal/dynamo-browse/services/viewsnapshot" + "github.com/lmika/dynamo-browse/internal/dynamo-browse/ui" + "github.com/lmika/dynamo-browse/internal/dynamo-browse/ui/keybindings" + "github.com/lmika/dynamo-browse/internal/dynamo-browse/ui/teamodels/styles" bus "github.com/lmika/events" "github.com/lmika/gopkgs/cli" "log" diff --git a/go.mod b/go.mod index 4562fef..a165db4 100644 --- a/go.mod +++ b/go.mod @@ -1,4 +1,4 @@ -module github.com/lmika/audax +module github.com/lmika/dynamo-browse go 1.18 diff --git a/internal/common/ui/commandctrl/commandctrl.go b/internal/common/ui/commandctrl/commandctrl.go index 6c08f96..b3e831f 100644 --- a/internal/common/ui/commandctrl/commandctrl.go +++ b/internal/common/ui/commandctrl/commandctrl.go @@ -11,7 +11,7 @@ import ( "path/filepath" "strings" - "github.com/lmika/audax/internal/common/ui/events" + "github.com/lmika/dynamo-browse/internal/common/ui/events" "github.com/lmika/shellwords" ) diff --git a/internal/common/ui/commandctrl/commandctrl_test.go b/internal/common/ui/commandctrl/commandctrl_test.go index b300418..b21783e 100644 --- a/internal/common/ui/commandctrl/commandctrl_test.go +++ b/internal/common/ui/commandctrl/commandctrl_test.go @@ -2,11 +2,11 @@ package commandctrl_test import ( "context" - "github.com/lmika/audax/internal/common/ui/events" - "github.com/lmika/audax/internal/dynamo-browse/services" + "github.com/lmika/dynamo-browse/internal/common/ui/events" + "github.com/lmika/dynamo-browse/internal/dynamo-browse/services" "testing" - "github.com/lmika/audax/internal/common/ui/commandctrl" + "github.com/lmika/dynamo-browse/internal/common/ui/commandctrl" "github.com/stretchr/testify/assert" ) diff --git a/internal/common/ui/commandctrl/iface.go b/internal/common/ui/commandctrl/iface.go index 671bc74..1cb834a 100644 --- a/internal/common/ui/commandctrl/iface.go +++ b/internal/common/ui/commandctrl/iface.go @@ -2,7 +2,7 @@ package commandctrl import ( "context" - "github.com/lmika/audax/internal/dynamo-browse/services" + "github.com/lmika/dynamo-browse/internal/dynamo-browse/services" ) type IterProvider interface { diff --git a/internal/common/ui/dispatcher/context.go b/internal/common/ui/dispatcher/context.go index deb7559..a7abf8d 100644 --- a/internal/common/ui/dispatcher/context.go +++ b/internal/common/ui/dispatcher/context.go @@ -2,7 +2,7 @@ package dispatcher import ( tea "github.com/charmbracelet/bubbletea" - "github.com/lmika/audax/internal/common/ui/uimodels" + "github.com/lmika/dynamo-browse/internal/common/ui/uimodels" ) type DispatcherContext struct { diff --git a/internal/common/ui/dispatcher/dispatcher.go b/internal/common/ui/dispatcher/dispatcher.go index 45350ae..eca48a5 100644 --- a/internal/common/ui/dispatcher/dispatcher.go +++ b/internal/common/ui/dispatcher/dispatcher.go @@ -4,8 +4,8 @@ import ( "context" "sync" - "github.com/lmika/audax/internal/common/ui/events" - "github.com/lmika/audax/internal/common/ui/uimodels" + "github.com/lmika/dynamo-browse/internal/common/ui/events" + "github.com/lmika/dynamo-browse/internal/common/ui/uimodels" "github.com/pkg/errors" ) diff --git a/internal/common/ui/events/commands.go b/internal/common/ui/events/commands.go index 19e30bc..183c9c6 100644 --- a/internal/common/ui/events/commands.go +++ b/internal/common/ui/events/commands.go @@ -2,7 +2,7 @@ package events import ( tea "github.com/charmbracelet/bubbletea" - "github.com/lmika/audax/internal/dynamo-browse/services" + "github.com/lmika/dynamo-browse/internal/dynamo-browse/services" "log" ) diff --git a/internal/common/ui/events/errors.go b/internal/common/ui/events/errors.go index c34d73f..08eda79 100644 --- a/internal/common/ui/events/errors.go +++ b/internal/common/ui/events/errors.go @@ -2,7 +2,7 @@ package events import ( tea "github.com/charmbracelet/bubbletea" - "github.com/lmika/audax/internal/dynamo-browse/services" + "github.com/lmika/dynamo-browse/internal/dynamo-browse/services" ) // Error indicates that an error occurred diff --git a/internal/dynamo-browse/controllers/columns.go b/internal/dynamo-browse/controllers/columns.go index b58f97b..b05f999 100644 --- a/internal/dynamo-browse/controllers/columns.go +++ b/internal/dynamo-browse/controllers/columns.go @@ -2,10 +2,10 @@ package controllers import ( tea "github.com/charmbracelet/bubbletea" - "github.com/lmika/audax/internal/common/ui/events" - "github.com/lmika/audax/internal/dynamo-browse/models" - "github.com/lmika/audax/internal/dynamo-browse/models/columns" - "github.com/lmika/audax/internal/dynamo-browse/models/queryexpr" + "github.com/lmika/dynamo-browse/internal/common/ui/events" + "github.com/lmika/dynamo-browse/internal/dynamo-browse/models" + "github.com/lmika/dynamo-browse/internal/dynamo-browse/models/columns" + "github.com/lmika/dynamo-browse/internal/dynamo-browse/models/queryexpr" bus "github.com/lmika/events" ) diff --git a/internal/dynamo-browse/controllers/commands.go b/internal/dynamo-browse/controllers/commands.go index 16390e2..97585f2 100644 --- a/internal/dynamo-browse/controllers/commands.go +++ b/internal/dynamo-browse/controllers/commands.go @@ -2,7 +2,7 @@ package controllers import ( tea "github.com/charmbracelet/bubbletea" - "github.com/lmika/audax/internal/common/ui/events" + "github.com/lmika/dynamo-browse/internal/common/ui/events" ) type promptSequence struct { diff --git a/internal/dynamo-browse/controllers/events.go b/internal/dynamo-browse/controllers/events.go index 8805de4..b2ef4cc 100644 --- a/internal/dynamo-browse/controllers/events.go +++ b/internal/dynamo-browse/controllers/events.go @@ -3,7 +3,7 @@ package controllers import ( "fmt" tea "github.com/charmbracelet/bubbletea" - "github.com/lmika/audax/internal/dynamo-browse/models" + "github.com/lmika/dynamo-browse/internal/dynamo-browse/models" ) type SetTableItemView struct { diff --git a/internal/dynamo-browse/controllers/export.go b/internal/dynamo-browse/controllers/export.go index a755ae9..3f70c93 100644 --- a/internal/dynamo-browse/controllers/export.go +++ b/internal/dynamo-browse/controllers/export.go @@ -3,8 +3,8 @@ package controllers import ( "encoding/csv" tea "github.com/charmbracelet/bubbletea" - "github.com/lmika/audax/internal/common/ui/events" - "github.com/lmika/audax/internal/dynamo-browse/models/attrutils" + "github.com/lmika/dynamo-browse/internal/common/ui/events" + "github.com/lmika/dynamo-browse/internal/dynamo-browse/models/attrutils" "github.com/pkg/errors" "os" ) diff --git a/internal/dynamo-browse/controllers/iface.go b/internal/dynamo-browse/controllers/iface.go index 6f00ad5..101d190 100644 --- a/internal/dynamo-browse/controllers/iface.go +++ b/internal/dynamo-browse/controllers/iface.go @@ -4,7 +4,7 @@ import ( "context" "github.com/aws/aws-sdk-go-v2/service/dynamodb/types" tea "github.com/charmbracelet/bubbletea" - "github.com/lmika/audax/internal/dynamo-browse/models" + "github.com/lmika/dynamo-browse/internal/dynamo-browse/models" "io/fs" ) diff --git a/internal/dynamo-browse/controllers/jobbuilder.go b/internal/dynamo-browse/controllers/jobbuilder.go index 7455b58..cbe7004 100644 --- a/internal/dynamo-browse/controllers/jobbuilder.go +++ b/internal/dynamo-browse/controllers/jobbuilder.go @@ -3,8 +3,8 @@ package controllers import ( "context" tea "github.com/charmbracelet/bubbletea" - "github.com/lmika/audax/internal/common/ui/events" - "github.com/lmika/audax/internal/dynamo-browse/services/jobs" + "github.com/lmika/dynamo-browse/internal/common/ui/events" + "github.com/lmika/dynamo-browse/internal/dynamo-browse/services/jobs" ) func NewJob[T any](jc *JobsController, description string, job func(ctx context.Context) (T, error)) JobBuilder[T] { diff --git a/internal/dynamo-browse/controllers/jobs.go b/internal/dynamo-browse/controllers/jobs.go index 063d9e5..3dc8fea 100644 --- a/internal/dynamo-browse/controllers/jobs.go +++ b/internal/dynamo-browse/controllers/jobs.go @@ -2,8 +2,8 @@ package controllers import ( tea "github.com/charmbracelet/bubbletea" - "github.com/lmika/audax/internal/common/ui/events" - "github.com/lmika/audax/internal/dynamo-browse/services/jobs" + "github.com/lmika/dynamo-browse/internal/common/ui/events" + "github.com/lmika/dynamo-browse/internal/dynamo-browse/services/jobs" bus "github.com/lmika/events" "log" ) diff --git a/internal/dynamo-browse/controllers/keybinding.go b/internal/dynamo-browse/controllers/keybinding.go index 9081026..043248f 100644 --- a/internal/dynamo-browse/controllers/keybinding.go +++ b/internal/dynamo-browse/controllers/keybinding.go @@ -3,8 +3,8 @@ package controllers import ( "fmt" tea "github.com/charmbracelet/bubbletea" - "github.com/lmika/audax/internal/common/ui/events" - "github.com/lmika/audax/internal/dynamo-browse/services/keybindings" + "github.com/lmika/dynamo-browse/internal/common/ui/events" + "github.com/lmika/dynamo-browse/internal/dynamo-browse/services/keybindings" "github.com/pkg/errors" ) diff --git a/internal/dynamo-browse/controllers/scripts.go b/internal/dynamo-browse/controllers/scripts.go index 35e66c2..2bb24fc 100644 --- a/internal/dynamo-browse/controllers/scripts.go +++ b/internal/dynamo-browse/controllers/scripts.go @@ -4,11 +4,11 @@ import ( "context" "fmt" tea "github.com/charmbracelet/bubbletea" - "github.com/lmika/audax/internal/common/ui/commandctrl" - "github.com/lmika/audax/internal/common/ui/events" - "github.com/lmika/audax/internal/dynamo-browse/models" - "github.com/lmika/audax/internal/dynamo-browse/models/queryexpr" - "github.com/lmika/audax/internal/dynamo-browse/services/scriptmanager" + "github.com/lmika/dynamo-browse/internal/common/ui/commandctrl" + "github.com/lmika/dynamo-browse/internal/common/ui/events" + "github.com/lmika/dynamo-browse/internal/dynamo-browse/models" + "github.com/lmika/dynamo-browse/internal/dynamo-browse/models/queryexpr" + "github.com/lmika/dynamo-browse/internal/dynamo-browse/services/scriptmanager" bus "github.com/lmika/events" "github.com/pkg/errors" "log" diff --git a/internal/dynamo-browse/controllers/scripts_test.go b/internal/dynamo-browse/controllers/scripts_test.go index 06ee419..a99e820 100644 --- a/internal/dynamo-browse/controllers/scripts_test.go +++ b/internal/dynamo-browse/controllers/scripts_test.go @@ -2,8 +2,8 @@ package controllers_test import ( "github.com/aws/aws-sdk-go-v2/service/dynamodb/types" - "github.com/lmika/audax/internal/common/ui/events" - "github.com/lmika/audax/internal/dynamo-browse/controllers" + "github.com/lmika/dynamo-browse/internal/common/ui/events" + "github.com/lmika/dynamo-browse/internal/dynamo-browse/controllers" "github.com/stretchr/testify/assert" "testing" "time" diff --git a/internal/dynamo-browse/controllers/settings.go b/internal/dynamo-browse/controllers/settings.go index ec69dfc..a0bb5a1 100644 --- a/internal/dynamo-browse/controllers/settings.go +++ b/internal/dynamo-browse/controllers/settings.go @@ -3,7 +3,7 @@ package controllers import ( "fmt" tea "github.com/charmbracelet/bubbletea" - "github.com/lmika/audax/internal/common/ui/events" + "github.com/lmika/dynamo-browse/internal/common/ui/events" bus "github.com/lmika/events" "github.com/pkg/errors" "log" diff --git a/internal/dynamo-browse/controllers/settings_test.go b/internal/dynamo-browse/controllers/settings_test.go index e7c7315..f400603 100644 --- a/internal/dynamo-browse/controllers/settings_test.go +++ b/internal/dynamo-browse/controllers/settings_test.go @@ -1,8 +1,8 @@ package controllers_test import ( - "github.com/lmika/audax/internal/common/ui/events" - "github.com/lmika/audax/internal/dynamo-browse/controllers" + "github.com/lmika/dynamo-browse/internal/common/ui/events" + "github.com/lmika/dynamo-browse/internal/dynamo-browse/controllers" "github.com/stretchr/testify/assert" "testing" ) diff --git a/internal/dynamo-browse/controllers/state.go b/internal/dynamo-browse/controllers/state.go index 3aebe13..2a75518 100644 --- a/internal/dynamo-browse/controllers/state.go +++ b/internal/dynamo-browse/controllers/state.go @@ -3,7 +3,7 @@ package controllers import ( "sync" - "github.com/lmika/audax/internal/dynamo-browse/models" + "github.com/lmika/dynamo-browse/internal/dynamo-browse/models" ) type State struct { diff --git a/internal/dynamo-browse/controllers/tableread.go b/internal/dynamo-browse/controllers/tableread.go index 5170731..a39bc1c 100644 --- a/internal/dynamo-browse/controllers/tableread.go +++ b/internal/dynamo-browse/controllers/tableread.go @@ -6,14 +6,14 @@ import ( "fmt" "github.com/aws/aws-sdk-go-v2/service/dynamodb/types" tea "github.com/charmbracelet/bubbletea" - "github.com/lmika/audax/internal/common/ui/events" - "github.com/lmika/audax/internal/dynamo-browse/models" - "github.com/lmika/audax/internal/dynamo-browse/models/attrcodec" - "github.com/lmika/audax/internal/dynamo-browse/models/queryexpr" - "github.com/lmika/audax/internal/dynamo-browse/models/serialisable" - "github.com/lmika/audax/internal/dynamo-browse/services/inputhistory" - "github.com/lmika/audax/internal/dynamo-browse/services/itemrenderer" - "github.com/lmika/audax/internal/dynamo-browse/services/viewsnapshot" + "github.com/lmika/dynamo-browse/internal/common/ui/events" + "github.com/lmika/dynamo-browse/internal/dynamo-browse/models" + "github.com/lmika/dynamo-browse/internal/dynamo-browse/models/attrcodec" + "github.com/lmika/dynamo-browse/internal/dynamo-browse/models/queryexpr" + "github.com/lmika/dynamo-browse/internal/dynamo-browse/models/serialisable" + "github.com/lmika/dynamo-browse/internal/dynamo-browse/services/inputhistory" + "github.com/lmika/dynamo-browse/internal/dynamo-browse/services/itemrenderer" + "github.com/lmika/dynamo-browse/internal/dynamo-browse/services/viewsnapshot" bus "github.com/lmika/events" "github.com/pkg/errors" "golang.design/x/clipboard" diff --git a/internal/dynamo-browse/controllers/tableread_test.go b/internal/dynamo-browse/controllers/tableread_test.go index 6a86a85..c042983 100644 --- a/internal/dynamo-browse/controllers/tableread_test.go +++ b/internal/dynamo-browse/controllers/tableread_test.go @@ -4,9 +4,9 @@ import ( "fmt" "github.com/aws/aws-sdk-go-v2/service/dynamodb/types" tea "github.com/charmbracelet/bubbletea" - "github.com/lmika/audax/internal/common/ui/events" - "github.com/lmika/audax/internal/dynamo-browse/controllers" - "github.com/lmika/audax/test/testdynamo" + "github.com/lmika/dynamo-browse/internal/common/ui/events" + "github.com/lmika/dynamo-browse/internal/dynamo-browse/controllers" + "github.com/lmika/dynamo-browse/test/testdynamo" "github.com/stretchr/testify/assert" "os" "strings" diff --git a/internal/dynamo-browse/controllers/tablewrite.go b/internal/dynamo-browse/controllers/tablewrite.go index 2409c1f..a92319b 100644 --- a/internal/dynamo-browse/controllers/tablewrite.go +++ b/internal/dynamo-browse/controllers/tablewrite.go @@ -5,11 +5,11 @@ import ( "fmt" "github.com/aws/aws-sdk-go-v2/service/dynamodb/types" tea "github.com/charmbracelet/bubbletea" - "github.com/lmika/audax/internal/common/sliceutils" - "github.com/lmika/audax/internal/common/ui/events" - "github.com/lmika/audax/internal/dynamo-browse/models" - "github.com/lmika/audax/internal/dynamo-browse/models/queryexpr" - "github.com/lmika/audax/internal/dynamo-browse/services/tables" + "github.com/lmika/dynamo-browse/internal/common/sliceutils" + "github.com/lmika/dynamo-browse/internal/common/ui/events" + "github.com/lmika/dynamo-browse/internal/dynamo-browse/models" + "github.com/lmika/dynamo-browse/internal/dynamo-browse/models/queryexpr" + "github.com/lmika/dynamo-browse/internal/dynamo-browse/services/tables" "github.com/pkg/errors" "log" "strconv" diff --git a/internal/dynamo-browse/controllers/tablewrite_test.go b/internal/dynamo-browse/controllers/tablewrite_test.go index 2dd2269..a28f182 100644 --- a/internal/dynamo-browse/controllers/tablewrite_test.go +++ b/internal/dynamo-browse/controllers/tablewrite_test.go @@ -4,21 +4,21 @@ import ( "fmt" "github.com/aws/aws-sdk-go-v2/service/dynamodb/types" tea "github.com/charmbracelet/bubbletea" - "github.com/lmika/audax/internal/common/ui/commandctrl" - "github.com/lmika/audax/internal/dynamo-browse/controllers" - "github.com/lmika/audax/internal/dynamo-browse/models" - "github.com/lmika/audax/internal/dynamo-browse/providers/dynamo" - "github.com/lmika/audax/internal/dynamo-browse/providers/inputhistorystore" - "github.com/lmika/audax/internal/dynamo-browse/providers/settingstore" - "github.com/lmika/audax/internal/dynamo-browse/providers/workspacestore" - "github.com/lmika/audax/internal/dynamo-browse/services/inputhistory" - "github.com/lmika/audax/internal/dynamo-browse/services/itemrenderer" - "github.com/lmika/audax/internal/dynamo-browse/services/jobs" - "github.com/lmika/audax/internal/dynamo-browse/services/scriptmanager" - "github.com/lmika/audax/internal/dynamo-browse/services/tables" - "github.com/lmika/audax/internal/dynamo-browse/services/viewsnapshot" - "github.com/lmika/audax/test/testdynamo" - "github.com/lmika/audax/test/testworkspace" + "github.com/lmika/dynamo-browse/internal/common/ui/commandctrl" + "github.com/lmika/dynamo-browse/internal/dynamo-browse/controllers" + "github.com/lmika/dynamo-browse/internal/dynamo-browse/models" + "github.com/lmika/dynamo-browse/internal/dynamo-browse/providers/dynamo" + "github.com/lmika/dynamo-browse/internal/dynamo-browse/providers/inputhistorystore" + "github.com/lmika/dynamo-browse/internal/dynamo-browse/providers/settingstore" + "github.com/lmika/dynamo-browse/internal/dynamo-browse/providers/workspacestore" + "github.com/lmika/dynamo-browse/internal/dynamo-browse/services/inputhistory" + "github.com/lmika/dynamo-browse/internal/dynamo-browse/services/itemrenderer" + "github.com/lmika/dynamo-browse/internal/dynamo-browse/services/jobs" + "github.com/lmika/dynamo-browse/internal/dynamo-browse/services/scriptmanager" + "github.com/lmika/dynamo-browse/internal/dynamo-browse/services/tables" + "github.com/lmika/dynamo-browse/internal/dynamo-browse/services/viewsnapshot" + "github.com/lmika/dynamo-browse/test/testdynamo" + "github.com/lmika/dynamo-browse/test/testworkspace" bus "github.com/lmika/events" "github.com/stretchr/testify/assert" "io/fs" diff --git a/internal/dynamo-browse/controllers/utils.go b/internal/dynamo-browse/controllers/utils.go index 9a09708..c8930b5 100644 --- a/internal/dynamo-browse/controllers/utils.go +++ b/internal/dynamo-browse/controllers/utils.go @@ -1,6 +1,6 @@ package controllers -import "github.com/lmika/audax/internal/dynamo-browse/models" +import "github.com/lmika/dynamo-browse/internal/dynamo-browse/models" func applyToMarkedItems(rs *models.ResultSet, selectedIndex int, applyFn func(idx int, item models.Item) error) error { if markedItems := rs.MarkedItems(); len(markedItems) > 0 { diff --git a/internal/dynamo-browse/models/attrcodec/codec_test.go b/internal/dynamo-browse/models/attrcodec/codec_test.go index 866e42e..8ac1a0c 100644 --- a/internal/dynamo-browse/models/attrcodec/codec_test.go +++ b/internal/dynamo-browse/models/attrcodec/codec_test.go @@ -3,7 +3,7 @@ package attrcodec_test import ( "bytes" "github.com/aws/aws-sdk-go-v2/service/dynamodb/types" - "github.com/lmika/audax/internal/dynamo-browse/models/attrcodec" + "github.com/lmika/dynamo-browse/internal/dynamo-browse/models/attrcodec" "github.com/stretchr/testify/assert" "strings" "testing" diff --git a/internal/dynamo-browse/models/columns/columns.go b/internal/dynamo-browse/models/columns/columns.go index c24fe1d..bdf290d 100644 --- a/internal/dynamo-browse/models/columns/columns.go +++ b/internal/dynamo-browse/models/columns/columns.go @@ -2,8 +2,8 @@ package columns import ( "github.com/aws/aws-sdk-go-v2/service/dynamodb/types" - "github.com/lmika/audax/internal/dynamo-browse/models" - "github.com/lmika/audax/internal/dynamo-browse/models/queryexpr" + "github.com/lmika/dynamo-browse/internal/dynamo-browse/models" + "github.com/lmika/dynamo-browse/internal/dynamo-browse/models/queryexpr" ) type Columns struct { diff --git a/internal/dynamo-browse/models/items.go b/internal/dynamo-browse/models/items.go index 4ed4e1a..0df6bf8 100644 --- a/internal/dynamo-browse/models/items.go +++ b/internal/dynamo-browse/models/items.go @@ -2,7 +2,7 @@ package models import ( "github.com/aws/aws-sdk-go-v2/service/dynamodb/types" - "github.com/lmika/audax/internal/dynamo-browse/models/attrutils" + "github.com/lmika/dynamo-browse/internal/dynamo-browse/models/attrutils" ) type ItemIndex struct { diff --git a/internal/dynamo-browse/models/modexpr/astmods.go b/internal/dynamo-browse/models/modexpr/astmods.go index abc6841..67ca70c 100644 --- a/internal/dynamo-browse/models/modexpr/astmods.go +++ b/internal/dynamo-browse/models/modexpr/astmods.go @@ -1,6 +1,6 @@ package modexpr -import "github.com/lmika/audax/internal/dynamo-browse/models" +import "github.com/lmika/dynamo-browse/internal/dynamo-browse/models" func (a *astExpr) calcPatchMods(item models.Item) ([]patchMod, error) { patchMods := make([]patchMod, 0) diff --git a/internal/dynamo-browse/models/modexpr/expr.go b/internal/dynamo-browse/models/modexpr/expr.go index 6ee5cb4..856e4f3 100644 --- a/internal/dynamo-browse/models/modexpr/expr.go +++ b/internal/dynamo-browse/models/modexpr/expr.go @@ -1,6 +1,6 @@ package modexpr -import "github.com/lmika/audax/internal/dynamo-browse/models" +import "github.com/lmika/dynamo-browse/internal/dynamo-browse/models" type ModExpr struct { ast *astExpr diff --git a/internal/dynamo-browse/models/modexpr/expr_test.go b/internal/dynamo-browse/models/modexpr/expr_test.go index 87c0357..6a5436e 100644 --- a/internal/dynamo-browse/models/modexpr/expr_test.go +++ b/internal/dynamo-browse/models/modexpr/expr_test.go @@ -4,8 +4,8 @@ import ( "testing" "github.com/aws/aws-sdk-go-v2/service/dynamodb/types" - "github.com/lmika/audax/internal/dynamo-browse/models" - "github.com/lmika/audax/internal/dynamo-browse/models/modexpr" + "github.com/lmika/dynamo-browse/internal/dynamo-browse/models" + "github.com/lmika/dynamo-browse/internal/dynamo-browse/models/modexpr" "github.com/stretchr/testify/assert" ) diff --git a/internal/dynamo-browse/models/modexpr/mods.go b/internal/dynamo-browse/models/modexpr/mods.go index 0e9ac80..4669cd5 100644 --- a/internal/dynamo-browse/models/modexpr/mods.go +++ b/internal/dynamo-browse/models/modexpr/mods.go @@ -2,7 +2,7 @@ package modexpr import ( "github.com/aws/aws-sdk-go-v2/service/dynamodb/types" - "github.com/lmika/audax/internal/dynamo-browse/models" + "github.com/lmika/dynamo-browse/internal/dynamo-browse/models" ) type patchMod interface { diff --git a/internal/dynamo-browse/models/query.go b/internal/dynamo-browse/models/query.go index 65d1451..dbd2e26 100644 --- a/internal/dynamo-browse/models/query.go +++ b/internal/dynamo-browse/models/query.go @@ -3,7 +3,7 @@ package models import ( "github.com/aws/aws-sdk-go-v2/aws" "github.com/aws/aws-sdk-go-v2/feature/dynamodb/expression" - "github.com/lmika/audax/internal/dynamo-browse/models/itemrender" + "github.com/lmika/dynamo-browse/internal/dynamo-browse/models/itemrender" ) type QueryExecutionPlan struct { diff --git a/internal/dynamo-browse/models/queryexpr/ast.go b/internal/dynamo-browse/models/queryexpr/ast.go index 2c81161..3455947 100644 --- a/internal/dynamo-browse/models/queryexpr/ast.go +++ b/internal/dynamo-browse/models/queryexpr/ast.go @@ -4,8 +4,8 @@ import ( "github.com/alecthomas/participle/v2" "github.com/alecthomas/participle/v2/lexer" "github.com/aws/aws-sdk-go-v2/feature/dynamodb/expression" - "github.com/lmika/audax/internal/common/sliceutils" - "github.com/lmika/audax/internal/dynamo-browse/models" + "github.com/lmika/dynamo-browse/internal/common/sliceutils" + "github.com/lmika/dynamo-browse/internal/dynamo-browse/models" "github.com/pkg/errors" "strconv" ) diff --git a/internal/dynamo-browse/models/queryexpr/atom.go b/internal/dynamo-browse/models/queryexpr/atom.go index 5a24d2e..029f683 100644 --- a/internal/dynamo-browse/models/queryexpr/atom.go +++ b/internal/dynamo-browse/models/queryexpr/atom.go @@ -1,7 +1,7 @@ package queryexpr import ( - "github.com/lmika/audax/internal/dynamo-browse/models" + "github.com/lmika/dynamo-browse/internal/dynamo-browse/models" "github.com/pkg/errors" ) diff --git a/internal/dynamo-browse/models/queryexpr/between.go b/internal/dynamo-browse/models/queryexpr/between.go index cbb4c3d..3e8efb7 100644 --- a/internal/dynamo-browse/models/queryexpr/between.go +++ b/internal/dynamo-browse/models/queryexpr/between.go @@ -3,7 +3,7 @@ package queryexpr import ( "fmt" "github.com/aws/aws-sdk-go-v2/feature/dynamodb/expression" - "github.com/lmika/audax/internal/dynamo-browse/models" + "github.com/lmika/dynamo-browse/internal/dynamo-browse/models" ) func (a *astBetweenOp) evalToIR(ctx *evalContext, info *models.TableInfo) (irAtom, error) { diff --git a/internal/dynamo-browse/models/queryexpr/boolnot.go b/internal/dynamo-browse/models/queryexpr/boolnot.go index 79a11c7..8c5f7d2 100644 --- a/internal/dynamo-browse/models/queryexpr/boolnot.go +++ b/internal/dynamo-browse/models/queryexpr/boolnot.go @@ -2,7 +2,7 @@ package queryexpr import ( "github.com/aws/aws-sdk-go-v2/feature/dynamodb/expression" - "github.com/lmika/audax/internal/dynamo-browse/models" + "github.com/lmika/dynamo-browse/internal/dynamo-browse/models" "strings" ) diff --git a/internal/dynamo-browse/models/queryexpr/comp.go b/internal/dynamo-browse/models/queryexpr/comp.go index 3e19472..29079c3 100644 --- a/internal/dynamo-browse/models/queryexpr/comp.go +++ b/internal/dynamo-browse/models/queryexpr/comp.go @@ -2,8 +2,8 @@ package queryexpr import ( "github.com/aws/aws-sdk-go-v2/feature/dynamodb/expression" - "github.com/lmika/audax/internal/dynamo-browse/models" - "github.com/lmika/audax/internal/dynamo-browse/models/attrutils" + "github.com/lmika/dynamo-browse/internal/dynamo-browse/models" + "github.com/lmika/dynamo-browse/internal/dynamo-browse/models/attrutils" "github.com/pkg/errors" ) diff --git a/internal/dynamo-browse/models/queryexpr/conj.go b/internal/dynamo-browse/models/queryexpr/conj.go index d54bb4b..3890a1f 100644 --- a/internal/dynamo-browse/models/queryexpr/conj.go +++ b/internal/dynamo-browse/models/queryexpr/conj.go @@ -2,7 +2,7 @@ package queryexpr import ( "github.com/aws/aws-sdk-go-v2/feature/dynamodb/expression" - "github.com/lmika/audax/internal/dynamo-browse/models" + "github.com/lmika/dynamo-browse/internal/dynamo-browse/models" "math/big" "strings" ) diff --git a/internal/dynamo-browse/models/queryexpr/disj.go b/internal/dynamo-browse/models/queryexpr/disj.go index 8819587..1a8a5f1 100644 --- a/internal/dynamo-browse/models/queryexpr/disj.go +++ b/internal/dynamo-browse/models/queryexpr/disj.go @@ -2,7 +2,7 @@ package queryexpr import ( "github.com/aws/aws-sdk-go-v2/feature/dynamodb/expression" - "github.com/lmika/audax/internal/dynamo-browse/models" + "github.com/lmika/dynamo-browse/internal/dynamo-browse/models" "strings" ) diff --git a/internal/dynamo-browse/models/queryexpr/dot.go b/internal/dynamo-browse/models/queryexpr/dot.go index 97063c9..8981f5a 100644 --- a/internal/dynamo-browse/models/queryexpr/dot.go +++ b/internal/dynamo-browse/models/queryexpr/dot.go @@ -3,7 +3,7 @@ package queryexpr import ( "fmt" "github.com/aws/aws-sdk-go-v2/feature/dynamodb/expression" - "github.com/lmika/audax/internal/dynamo-browse/models" + "github.com/lmika/dynamo-browse/internal/dynamo-browse/models" "strings" ) diff --git a/internal/dynamo-browse/models/queryexpr/equality.go b/internal/dynamo-browse/models/queryexpr/equality.go index ffa26cd..8d317e5 100644 --- a/internal/dynamo-browse/models/queryexpr/equality.go +++ b/internal/dynamo-browse/models/queryexpr/equality.go @@ -2,8 +2,8 @@ package queryexpr import ( "github.com/aws/aws-sdk-go-v2/feature/dynamodb/expression" - "github.com/lmika/audax/internal/dynamo-browse/models" - "github.com/lmika/audax/internal/dynamo-browse/models/attrutils" + "github.com/lmika/dynamo-browse/internal/dynamo-browse/models" + "github.com/lmika/dynamo-browse/internal/dynamo-browse/models/attrutils" "github.com/pkg/errors" "strings" ) diff --git a/internal/dynamo-browse/models/queryexpr/errors.go b/internal/dynamo-browse/models/queryexpr/errors.go index 8c48bbf..172b2fa 100644 --- a/internal/dynamo-browse/models/queryexpr/errors.go +++ b/internal/dynamo-browse/models/queryexpr/errors.go @@ -3,8 +3,8 @@ package queryexpr import ( "fmt" "github.com/aws/aws-sdk-go-v2/service/dynamodb/types" - "github.com/lmika/audax/internal/dynamo-browse/models/attrutils" - "github.com/lmika/audax/internal/dynamo-browse/models/itemrender" + "github.com/lmika/dynamo-browse/internal/dynamo-browse/models/attrutils" + "github.com/lmika/dynamo-browse/internal/dynamo-browse/models/itemrender" "strings" ) diff --git a/internal/dynamo-browse/models/queryexpr/expr.go b/internal/dynamo-browse/models/queryexpr/expr.go index a68a275..2d254e7 100644 --- a/internal/dynamo-browse/models/queryexpr/expr.go +++ b/internal/dynamo-browse/models/queryexpr/expr.go @@ -4,9 +4,9 @@ import ( "bytes" "encoding/gob" "github.com/aws/aws-sdk-go-v2/service/dynamodb/types" - "github.com/lmika/audax/internal/dynamo-browse/models" - "github.com/lmika/audax/internal/dynamo-browse/models/attrcodec" - "github.com/lmika/audax/internal/dynamo-browse/models/attrutils" + "github.com/lmika/dynamo-browse/internal/dynamo-browse/models" + "github.com/lmika/dynamo-browse/internal/dynamo-browse/models/attrcodec" + "github.com/lmika/dynamo-browse/internal/dynamo-browse/models/attrutils" "github.com/pkg/errors" "golang.org/x/exp/maps" "golang.org/x/exp/slices" diff --git a/internal/dynamo-browse/models/queryexpr/expr_test.go b/internal/dynamo-browse/models/queryexpr/expr_test.go index eb3a52e..b8c7dce 100644 --- a/internal/dynamo-browse/models/queryexpr/expr_test.go +++ b/internal/dynamo-browse/models/queryexpr/expr_test.go @@ -5,11 +5,11 @@ import ( "fmt" "github.com/aws/aws-sdk-go-v2/aws" "github.com/aws/aws-sdk-go-v2/service/dynamodb/types" - "github.com/lmika/audax/internal/dynamo-browse/models/queryexpr" + "github.com/lmika/dynamo-browse/internal/dynamo-browse/models/queryexpr" "testing" "time" - "github.com/lmika/audax/internal/dynamo-browse/models" + "github.com/lmika/dynamo-browse/internal/dynamo-browse/models" "github.com/stretchr/testify/assert" ) diff --git a/internal/dynamo-browse/models/queryexpr/fncall.go b/internal/dynamo-browse/models/queryexpr/fncall.go index 1eff92d..69d6790 100644 --- a/internal/dynamo-browse/models/queryexpr/fncall.go +++ b/internal/dynamo-browse/models/queryexpr/fncall.go @@ -3,8 +3,8 @@ package queryexpr import ( "context" "github.com/aws/aws-sdk-go-v2/feature/dynamodb/expression" - "github.com/lmika/audax/internal/common/sliceutils" - "github.com/lmika/audax/internal/dynamo-browse/models" + "github.com/lmika/dynamo-browse/internal/common/sliceutils" + "github.com/lmika/dynamo-browse/internal/dynamo-browse/models" "github.com/pkg/errors" "strings" ) diff --git a/internal/dynamo-browse/models/queryexpr/in.go b/internal/dynamo-browse/models/queryexpr/in.go index 18d25dd..63c46cb 100644 --- a/internal/dynamo-browse/models/queryexpr/in.go +++ b/internal/dynamo-browse/models/queryexpr/in.go @@ -2,9 +2,9 @@ package queryexpr import ( "github.com/aws/aws-sdk-go-v2/feature/dynamodb/expression" - "github.com/lmika/audax/internal/common/sliceutils" - "github.com/lmika/audax/internal/dynamo-browse/models" - "github.com/lmika/audax/internal/dynamo-browse/models/attrutils" + "github.com/lmika/dynamo-browse/internal/common/sliceutils" + "github.com/lmika/dynamo-browse/internal/dynamo-browse/models" + "github.com/lmika/dynamo-browse/internal/dynamo-browse/models/attrutils" "github.com/pkg/errors" "strings" ) diff --git a/internal/dynamo-browse/models/queryexpr/ir.go b/internal/dynamo-browse/models/queryexpr/ir.go index a8dddbd..ad86d69 100644 --- a/internal/dynamo-browse/models/queryexpr/ir.go +++ b/internal/dynamo-browse/models/queryexpr/ir.go @@ -2,7 +2,7 @@ package queryexpr import ( "github.com/aws/aws-sdk-go-v2/feature/dynamodb/expression" - "github.com/lmika/audax/internal/dynamo-browse/models" + "github.com/lmika/dynamo-browse/internal/dynamo-browse/models" ) // TO DELETE = operandFieldName() string diff --git a/internal/dynamo-browse/models/queryexpr/is.go b/internal/dynamo-browse/models/queryexpr/is.go index 18e40d5..c7cd392 100644 --- a/internal/dynamo-browse/models/queryexpr/is.go +++ b/internal/dynamo-browse/models/queryexpr/is.go @@ -2,7 +2,7 @@ package queryexpr import ( "github.com/aws/aws-sdk-go-v2/feature/dynamodb/expression" - "github.com/lmika/audax/internal/dynamo-browse/models" + "github.com/lmika/dynamo-browse/internal/dynamo-browse/models" "reflect" "strings" ) diff --git a/internal/dynamo-browse/models/queryexpr/placeholder.go b/internal/dynamo-browse/models/queryexpr/placeholder.go index 1f6ffdb..eca6dc7 100644 --- a/internal/dynamo-browse/models/queryexpr/placeholder.go +++ b/internal/dynamo-browse/models/queryexpr/placeholder.go @@ -1,7 +1,7 @@ package queryexpr import ( - "github.com/lmika/audax/internal/dynamo-browse/models" + "github.com/lmika/dynamo-browse/internal/dynamo-browse/models" "github.com/pkg/errors" ) diff --git a/internal/dynamo-browse/models/queryexpr/subref.go b/internal/dynamo-browse/models/queryexpr/subref.go index 0e3c8f9..0498298 100644 --- a/internal/dynamo-browse/models/queryexpr/subref.go +++ b/internal/dynamo-browse/models/queryexpr/subref.go @@ -1,8 +1,8 @@ package queryexpr import ( - "github.com/lmika/audax/internal/common/sliceutils" - "github.com/lmika/audax/internal/dynamo-browse/models" + "github.com/lmika/dynamo-browse/internal/common/sliceutils" + "github.com/lmika/dynamo-browse/internal/dynamo-browse/models" "strings" ) diff --git a/internal/dynamo-browse/models/queryexpr/types.go b/internal/dynamo-browse/models/queryexpr/types.go index 1a4106a..32e39ae 100644 --- a/internal/dynamo-browse/models/queryexpr/types.go +++ b/internal/dynamo-browse/models/queryexpr/types.go @@ -3,8 +3,8 @@ package queryexpr import ( "github.com/aws/aws-sdk-go-v2/feature/dynamodb/expression" "github.com/aws/aws-sdk-go-v2/service/dynamodb/types" - "github.com/lmika/audax/internal/common/maputils" - "github.com/lmika/audax/internal/common/sliceutils" + "github.com/lmika/dynamo-browse/internal/common/maputils" + "github.com/lmika/dynamo-browse/internal/common/sliceutils" "github.com/pkg/errors" "math/big" "strconv" diff --git a/internal/dynamo-browse/models/queryexpr/values.go b/internal/dynamo-browse/models/queryexpr/values.go index 9e97ba2..b58a567 100644 --- a/internal/dynamo-browse/models/queryexpr/values.go +++ b/internal/dynamo-browse/models/queryexpr/values.go @@ -2,7 +2,7 @@ package queryexpr import ( "github.com/aws/aws-sdk-go-v2/feature/dynamodb/expression" - "github.com/lmika/audax/internal/dynamo-browse/models" + "github.com/lmika/dynamo-browse/internal/dynamo-browse/models" "strconv" "github.com/pkg/errors" diff --git a/internal/dynamo-browse/models/serialisable/viewsnapshot.go b/internal/dynamo-browse/models/serialisable/viewsnapshot.go index 610e695..541ed8b 100644 --- a/internal/dynamo-browse/models/serialisable/viewsnapshot.go +++ b/internal/dynamo-browse/models/serialisable/viewsnapshot.go @@ -2,7 +2,7 @@ package serialisable import ( "bytes" - "github.com/lmika/audax/internal/dynamo-browse/models/queryexpr" + "github.com/lmika/dynamo-browse/internal/dynamo-browse/models/queryexpr" "time" ) diff --git a/internal/dynamo-browse/models/sorted.go b/internal/dynamo-browse/models/sorted.go index 60e5d49..dd8e200 100644 --- a/internal/dynamo-browse/models/sorted.go +++ b/internal/dynamo-browse/models/sorted.go @@ -1,7 +1,7 @@ package models import ( - "github.com/lmika/audax/internal/dynamo-browse/models/attrutils" + "github.com/lmika/dynamo-browse/internal/dynamo-browse/models/attrutils" "sort" ) diff --git a/internal/dynamo-browse/models/sorted_test.go b/internal/dynamo-browse/models/sorted_test.go index b23b71c..5cb1fc6 100644 --- a/internal/dynamo-browse/models/sorted_test.go +++ b/internal/dynamo-browse/models/sorted_test.go @@ -4,7 +4,7 @@ import ( "testing" "github.com/aws/aws-sdk-go-v2/service/dynamodb/types" - "github.com/lmika/audax/internal/dynamo-browse/models" + "github.com/lmika/dynamo-browse/internal/dynamo-browse/models" "github.com/stretchr/testify/assert" ) diff --git a/internal/dynamo-browse/providers/dynamo/provider.go b/internal/dynamo-browse/providers/dynamo/provider.go index 66e8d1c..6953931 100644 --- a/internal/dynamo-browse/providers/dynamo/provider.go +++ b/internal/dynamo-browse/providers/dynamo/provider.go @@ -7,9 +7,9 @@ import ( "github.com/aws/aws-sdk-go-v2/feature/dynamodb/expression" "github.com/aws/aws-sdk-go-v2/service/dynamodb" "github.com/aws/aws-sdk-go-v2/service/dynamodb/types" - "github.com/lmika/audax/internal/common/sliceutils" - "github.com/lmika/audax/internal/dynamo-browse/models" - "github.com/lmika/audax/internal/dynamo-browse/services/jobs" + "github.com/lmika/dynamo-browse/internal/common/sliceutils" + "github.com/lmika/dynamo-browse/internal/dynamo-browse/models" + "github.com/lmika/dynamo-browse/internal/dynamo-browse/services/jobs" "github.com/pkg/errors" "time" ) diff --git a/internal/dynamo-browse/providers/dynamo/provider_test.go b/internal/dynamo-browse/providers/dynamo/provider_test.go index 625895d..ce86a04 100644 --- a/internal/dynamo-browse/providers/dynamo/provider_test.go +++ b/internal/dynamo-browse/providers/dynamo/provider_test.go @@ -3,12 +3,12 @@ package dynamo_test import ( "context" "fmt" - "github.com/lmika/audax/internal/dynamo-browse/models" + "github.com/lmika/dynamo-browse/internal/dynamo-browse/models" "testing" "github.com/aws/aws-sdk-go-v2/service/dynamodb/types" - "github.com/lmika/audax/internal/dynamo-browse/providers/dynamo" - "github.com/lmika/audax/test/testdynamo" + "github.com/lmika/dynamo-browse/internal/dynamo-browse/providers/dynamo" + "github.com/lmika/dynamo-browse/test/testdynamo" "github.com/stretchr/testify/assert" ) diff --git a/internal/dynamo-browse/providers/inputhistorystore/store.go b/internal/dynamo-browse/providers/inputhistorystore/store.go index 14878d0..57d6bc4 100644 --- a/internal/dynamo-browse/providers/inputhistorystore/store.go +++ b/internal/dynamo-browse/providers/inputhistorystore/store.go @@ -3,8 +3,8 @@ package inputhistorystore import ( "context" "github.com/asdine/storm" - "github.com/lmika/audax/internal/common/sliceutils" - "github.com/lmika/audax/internal/common/workspaces" + "github.com/lmika/dynamo-browse/internal/common/sliceutils" + "github.com/lmika/dynamo-browse/internal/common/workspaces" "github.com/pkg/errors" "sort" "time" diff --git a/internal/dynamo-browse/providers/settingstore/settingstore.go b/internal/dynamo-browse/providers/settingstore/settingstore.go index 30db68e..bcdba19 100644 --- a/internal/dynamo-browse/providers/settingstore/settingstore.go +++ b/internal/dynamo-browse/providers/settingstore/settingstore.go @@ -2,7 +2,7 @@ package settingstore import ( "github.com/asdine/storm" - "github.com/lmika/audax/internal/common/workspaces" + "github.com/lmika/dynamo-browse/internal/common/workspaces" "github.com/pkg/errors" "io/fs" "log" diff --git a/internal/dynamo-browse/providers/workspacestore/resultsetsnapshot.go b/internal/dynamo-browse/providers/workspacestore/resultsetsnapshot.go index 6639e17..65ea0ad 100644 --- a/internal/dynamo-browse/providers/workspacestore/resultsetsnapshot.go +++ b/internal/dynamo-browse/providers/workspacestore/resultsetsnapshot.go @@ -2,8 +2,8 @@ package workspacestore import ( "github.com/asdine/storm" - "github.com/lmika/audax/internal/common/workspaces" - "github.com/lmika/audax/internal/dynamo-browse/models/serialisable" + "github.com/lmika/dynamo-browse/internal/common/workspaces" + "github.com/lmika/dynamo-browse/internal/dynamo-browse/models/serialisable" "github.com/pkg/errors" "log" ) diff --git a/internal/dynamo-browse/services/inputhistory/iter.go b/internal/dynamo-browse/services/inputhistory/iter.go index 3cb64cc..ea9417c 100644 --- a/internal/dynamo-browse/services/inputhistory/iter.go +++ b/internal/dynamo-browse/services/inputhistory/iter.go @@ -2,7 +2,7 @@ package inputhistory import ( "context" - "github.com/lmika/audax/internal/dynamo-browse/services" + "github.com/lmika/dynamo-browse/internal/dynamo-browse/services" "log" "strings" ) diff --git a/internal/dynamo-browse/services/itemrenderer/service.go b/internal/dynamo-browse/services/itemrenderer/service.go index 374bbd1..4f543cd 100644 --- a/internal/dynamo-browse/services/itemrenderer/service.go +++ b/internal/dynamo-browse/services/itemrenderer/service.go @@ -2,8 +2,8 @@ package itemrenderer import ( "fmt" - "github.com/lmika/audax/internal/dynamo-browse/models" - "github.com/lmika/audax/internal/dynamo-browse/models/itemrender" + "github.com/lmika/dynamo-browse/internal/dynamo-browse/models" + "github.com/lmika/dynamo-browse/internal/dynamo-browse/models/itemrender" "io" "text/tabwriter" ) diff --git a/internal/dynamo-browse/services/scriptmanager/iface.go b/internal/dynamo-browse/services/scriptmanager/iface.go index 35dce04..39b4d9e 100644 --- a/internal/dynamo-browse/services/scriptmanager/iface.go +++ b/internal/dynamo-browse/services/scriptmanager/iface.go @@ -3,7 +3,7 @@ package scriptmanager import ( "context" "github.com/aws/aws-sdk-go-v2/service/dynamodb/types" - "github.com/lmika/audax/internal/dynamo-browse/models" + "github.com/lmika/dynamo-browse/internal/dynamo-browse/models" ) //go:generate mockery --with-expecter --name UIService diff --git a/internal/dynamo-browse/services/scriptmanager/mocks/SessionService.go b/internal/dynamo-browse/services/scriptmanager/mocks/SessionService.go index 7ef842b..bdfa6b1 100644 --- a/internal/dynamo-browse/services/scriptmanager/mocks/SessionService.go +++ b/internal/dynamo-browse/services/scriptmanager/mocks/SessionService.go @@ -5,10 +5,10 @@ package mocks import ( context "context" - models "github.com/lmika/audax/internal/dynamo-browse/models" + models "github.com/lmika/dynamo-browse/internal/dynamo-browse/models" mock "github.com/stretchr/testify/mock" - scriptmanager "github.com/lmika/audax/internal/dynamo-browse/services/scriptmanager" + scriptmanager "github.com/lmika/dynamo-browse/internal/dynamo-browse/services/scriptmanager" ) // SessionService is an autogenerated mock type for the SessionService type diff --git a/internal/dynamo-browse/services/scriptmanager/modos_test.go b/internal/dynamo-browse/services/scriptmanager/modos_test.go index d9823e9..63dfe32 100644 --- a/internal/dynamo-browse/services/scriptmanager/modos_test.go +++ b/internal/dynamo-browse/services/scriptmanager/modos_test.go @@ -2,8 +2,8 @@ package scriptmanager_test import ( "context" - "github.com/lmika/audax/internal/dynamo-browse/services/scriptmanager" - "github.com/lmika/audax/internal/dynamo-browse/services/scriptmanager/mocks" + "github.com/lmika/dynamo-browse/internal/dynamo-browse/services/scriptmanager" + "github.com/lmika/dynamo-browse/internal/dynamo-browse/services/scriptmanager/mocks" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/mock" "testing" diff --git a/internal/dynamo-browse/services/scriptmanager/modsession_test.go b/internal/dynamo-browse/services/scriptmanager/modsession_test.go index c28bade..947efe0 100644 --- a/internal/dynamo-browse/services/scriptmanager/modsession_test.go +++ b/internal/dynamo-browse/services/scriptmanager/modsession_test.go @@ -3,9 +3,9 @@ package scriptmanager_test import ( "context" "github.com/aws/aws-sdk-go-v2/service/dynamodb/types" - "github.com/lmika/audax/internal/dynamo-browse/models" - "github.com/lmika/audax/internal/dynamo-browse/services/scriptmanager" - "github.com/lmika/audax/internal/dynamo-browse/services/scriptmanager/mocks" + "github.com/lmika/dynamo-browse/internal/dynamo-browse/models" + "github.com/lmika/dynamo-browse/internal/dynamo-browse/services/scriptmanager" + "github.com/lmika/dynamo-browse/internal/dynamo-browse/services/scriptmanager/mocks" "github.com/pkg/errors" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/mock" diff --git a/internal/dynamo-browse/services/scriptmanager/modui_test.go b/internal/dynamo-browse/services/scriptmanager/modui_test.go index 7fa264e..3a8b96d 100644 --- a/internal/dynamo-browse/services/scriptmanager/modui_test.go +++ b/internal/dynamo-browse/services/scriptmanager/modui_test.go @@ -2,8 +2,8 @@ package scriptmanager_test import ( "context" - "github.com/lmika/audax/internal/dynamo-browse/services/scriptmanager" - "github.com/lmika/audax/internal/dynamo-browse/services/scriptmanager/mocks" + "github.com/lmika/dynamo-browse/internal/dynamo-browse/services/scriptmanager" + "github.com/lmika/dynamo-browse/internal/dynamo-browse/services/scriptmanager/mocks" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/mock" "testing" diff --git a/internal/dynamo-browse/services/scriptmanager/resultsetproxy.go b/internal/dynamo-browse/services/scriptmanager/resultsetproxy.go index ed169ff..d3ee331 100644 --- a/internal/dynamo-browse/services/scriptmanager/resultsetproxy.go +++ b/internal/dynamo-browse/services/scriptmanager/resultsetproxy.go @@ -4,8 +4,8 @@ import ( "context" "github.com/cloudcmds/tamarin/arg" "github.com/cloudcmds/tamarin/object" - "github.com/lmika/audax/internal/dynamo-browse/models" - "github.com/lmika/audax/internal/dynamo-browse/models/queryexpr" + "github.com/lmika/dynamo-browse/internal/dynamo-browse/models" + "github.com/lmika/dynamo-browse/internal/dynamo-browse/models/queryexpr" "github.com/pkg/errors" ) diff --git a/internal/dynamo-browse/services/scriptmanager/resultsetproxy_test.go b/internal/dynamo-browse/services/scriptmanager/resultsetproxy_test.go index 9b5b87d..c52ea3e 100644 --- a/internal/dynamo-browse/services/scriptmanager/resultsetproxy_test.go +++ b/internal/dynamo-browse/services/scriptmanager/resultsetproxy_test.go @@ -3,9 +3,9 @@ package scriptmanager_test import ( "context" "github.com/aws/aws-sdk-go-v2/service/dynamodb/types" - "github.com/lmika/audax/internal/dynamo-browse/models" - "github.com/lmika/audax/internal/dynamo-browse/services/scriptmanager" - "github.com/lmika/audax/internal/dynamo-browse/services/scriptmanager/mocks" + "github.com/lmika/dynamo-browse/internal/dynamo-browse/models" + "github.com/lmika/dynamo-browse/internal/dynamo-browse/services/scriptmanager" + "github.com/lmika/dynamo-browse/internal/dynamo-browse/services/scriptmanager/mocks" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/mock" "testing" diff --git a/internal/dynamo-browse/services/scriptmanager/service.go b/internal/dynamo-browse/services/scriptmanager/service.go index 30d4f7a..ce8ec78 100644 --- a/internal/dynamo-browse/services/scriptmanager/service.go +++ b/internal/dynamo-browse/services/scriptmanager/service.go @@ -5,7 +5,7 @@ import ( "github.com/cloudcmds/tamarin/exec" "github.com/cloudcmds/tamarin/object" "github.com/cloudcmds/tamarin/scope" - "github.com/lmika/audax/internal/dynamo-browse/services/keybindings" + "github.com/lmika/dynamo-browse/internal/dynamo-browse/services/keybindings" "github.com/pkg/errors" "io/fs" "log" diff --git a/internal/dynamo-browse/services/scriptmanager/service_test.go b/internal/dynamo-browse/services/scriptmanager/service_test.go index 1526434..745124d 100644 --- a/internal/dynamo-browse/services/scriptmanager/service_test.go +++ b/internal/dynamo-browse/services/scriptmanager/service_test.go @@ -2,8 +2,8 @@ package scriptmanager_test import ( "context" - "github.com/lmika/audax/internal/dynamo-browse/services/scriptmanager" - "github.com/lmika/audax/internal/dynamo-browse/services/scriptmanager/mocks" + "github.com/lmika/dynamo-browse/internal/dynamo-browse/services/scriptmanager" + "github.com/lmika/dynamo-browse/internal/dynamo-browse/services/scriptmanager/mocks" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/mock" "io/fs" diff --git a/internal/dynamo-browse/services/scriptmanager/tableproxy.go b/internal/dynamo-browse/services/scriptmanager/tableproxy.go index 1ef6204..c626085 100644 --- a/internal/dynamo-browse/services/scriptmanager/tableproxy.go +++ b/internal/dynamo-browse/services/scriptmanager/tableproxy.go @@ -2,8 +2,8 @@ package scriptmanager import ( "github.com/cloudcmds/tamarin/object" - "github.com/lmika/audax/internal/common/sliceutils" - "github.com/lmika/audax/internal/dynamo-browse/models" + "github.com/lmika/dynamo-browse/internal/common/sliceutils" + "github.com/lmika/dynamo-browse/internal/dynamo-browse/models" "reflect" ) diff --git a/internal/dynamo-browse/services/scriptmanager/typemapping.go b/internal/dynamo-browse/services/scriptmanager/typemapping.go index 6cf1583..26cec3c 100644 --- a/internal/dynamo-browse/services/scriptmanager/typemapping.go +++ b/internal/dynamo-browse/services/scriptmanager/typemapping.go @@ -4,8 +4,8 @@ import ( "fmt" "github.com/aws/aws-sdk-go-v2/service/dynamodb/types" "github.com/cloudcmds/tamarin/object" - "github.com/lmika/audax/internal/common/maputils" - "github.com/lmika/audax/internal/common/sliceutils" + "github.com/lmika/dynamo-browse/internal/common/maputils" + "github.com/lmika/dynamo-browse/internal/common/sliceutils" "github.com/pkg/errors" "regexp" "strconv" diff --git a/internal/dynamo-browse/services/tables/iface.go b/internal/dynamo-browse/services/tables/iface.go index 75aca24..8ebc335 100644 --- a/internal/dynamo-browse/services/tables/iface.go +++ b/internal/dynamo-browse/services/tables/iface.go @@ -4,7 +4,7 @@ import ( "context" "github.com/aws/aws-sdk-go-v2/feature/dynamodb/expression" "github.com/aws/aws-sdk-go-v2/service/dynamodb/types" - "github.com/lmika/audax/internal/dynamo-browse/models" + "github.com/lmika/dynamo-browse/internal/dynamo-browse/models" ) type TableProvider interface { diff --git a/internal/dynamo-browse/services/tables/service.go b/internal/dynamo-browse/services/tables/service.go index fb2bad3..c49075a 100644 --- a/internal/dynamo-browse/services/tables/service.go +++ b/internal/dynamo-browse/services/tables/service.go @@ -5,13 +5,13 @@ import ( "fmt" "github.com/aws/aws-sdk-go-v2/feature/dynamodb/expression" "github.com/aws/aws-sdk-go-v2/service/dynamodb/types" - "github.com/lmika/audax/internal/common/sliceutils" - "github.com/lmika/audax/internal/dynamo-browse/services/jobs" + "github.com/lmika/dynamo-browse/internal/common/sliceutils" + "github.com/lmika/dynamo-browse/internal/dynamo-browse/services/jobs" "log" "strings" "time" - "github.com/lmika/audax/internal/dynamo-browse/models" + "github.com/lmika/dynamo-browse/internal/dynamo-browse/models" "github.com/pkg/errors" ) diff --git a/internal/dynamo-browse/services/tables/service_test.go b/internal/dynamo-browse/services/tables/service_test.go index 3135b5d..3ed9b9f 100644 --- a/internal/dynamo-browse/services/tables/service_test.go +++ b/internal/dynamo-browse/services/tables/service_test.go @@ -4,9 +4,9 @@ import ( "context" "testing" - "github.com/lmika/audax/internal/dynamo-browse/providers/dynamo" - "github.com/lmika/audax/internal/dynamo-browse/services/tables" - "github.com/lmika/audax/test/testdynamo" + "github.com/lmika/dynamo-browse/internal/dynamo-browse/providers/dynamo" + "github.com/lmika/dynamo-browse/internal/dynamo-browse/services/tables" + "github.com/lmika/dynamo-browse/test/testdynamo" "github.com/stretchr/testify/assert" ) diff --git a/internal/dynamo-browse/services/viewsnapshot/iface.go b/internal/dynamo-browse/services/viewsnapshot/iface.go index 69ec99c..c6ac3a4 100644 --- a/internal/dynamo-browse/services/viewsnapshot/iface.go +++ b/internal/dynamo-browse/services/viewsnapshot/iface.go @@ -1,6 +1,6 @@ package viewsnapshot -import "github.com/lmika/audax/internal/dynamo-browse/models/serialisable" +import "github.com/lmika/dynamo-browse/internal/dynamo-browse/models/serialisable" type ViewSnapshotStore interface { Save(rs *serialisable.ViewSnapshot) error diff --git a/internal/dynamo-browse/services/viewsnapshot/service.go b/internal/dynamo-browse/services/viewsnapshot/service.go index 791ee90..410f43d 100644 --- a/internal/dynamo-browse/services/viewsnapshot/service.go +++ b/internal/dynamo-browse/services/viewsnapshot/service.go @@ -1,7 +1,7 @@ package viewsnapshot import ( - "github.com/lmika/audax/internal/dynamo-browse/models/serialisable" + "github.com/lmika/dynamo-browse/internal/dynamo-browse/models/serialisable" "github.com/pkg/errors" "time" ) diff --git a/internal/dynamo-browse/services/viewsnapshot/service_test.go b/internal/dynamo-browse/services/viewsnapshot/service_test.go index 845be56..5805cce 100644 --- a/internal/dynamo-browse/services/viewsnapshot/service_test.go +++ b/internal/dynamo-browse/services/viewsnapshot/service_test.go @@ -3,11 +3,11 @@ package viewsnapshot_test import ( "bytes" "github.com/aws/aws-sdk-go-v2/service/dynamodb/types" - "github.com/lmika/audax/internal/dynamo-browse/models/queryexpr" - "github.com/lmika/audax/internal/dynamo-browse/models/serialisable" - "github.com/lmika/audax/internal/dynamo-browse/providers/workspacestore" - "github.com/lmika/audax/internal/dynamo-browse/services/viewsnapshot" - "github.com/lmika/audax/test/testworkspace" + "github.com/lmika/dynamo-browse/internal/dynamo-browse/models/queryexpr" + "github.com/lmika/dynamo-browse/internal/dynamo-browse/models/serialisable" + "github.com/lmika/dynamo-browse/internal/dynamo-browse/providers/workspacestore" + "github.com/lmika/dynamo-browse/internal/dynamo-browse/services/viewsnapshot" + "github.com/lmika/dynamo-browse/test/testworkspace" "github.com/stretchr/testify/assert" "testing" ) diff --git a/internal/dynamo-browse/ui/model.go b/internal/dynamo-browse/ui/model.go index 43cf146..b3aa53a 100644 --- a/internal/dynamo-browse/ui/model.go +++ b/internal/dynamo-browse/ui/model.go @@ -3,22 +3,22 @@ package ui import ( "github.com/charmbracelet/bubbles/key" tea "github.com/charmbracelet/bubbletea" - "github.com/lmika/audax/internal/common/ui/commandctrl" - "github.com/lmika/audax/internal/common/ui/events" - "github.com/lmika/audax/internal/dynamo-browse/controllers" - "github.com/lmika/audax/internal/dynamo-browse/models" - "github.com/lmika/audax/internal/dynamo-browse/services/itemrenderer" - "github.com/lmika/audax/internal/dynamo-browse/ui/keybindings" - "github.com/lmika/audax/internal/dynamo-browse/ui/teamodels/colselector" - "github.com/lmika/audax/internal/dynamo-browse/ui/teamodels/dialogprompt" - "github.com/lmika/audax/internal/dynamo-browse/ui/teamodels/dynamoitemedit" - "github.com/lmika/audax/internal/dynamo-browse/ui/teamodels/dynamoitemview" - "github.com/lmika/audax/internal/dynamo-browse/ui/teamodels/dynamotableview" - "github.com/lmika/audax/internal/dynamo-browse/ui/teamodels/layout" - "github.com/lmika/audax/internal/dynamo-browse/ui/teamodels/statusandprompt" - "github.com/lmika/audax/internal/dynamo-browse/ui/teamodels/styles" - "github.com/lmika/audax/internal/dynamo-browse/ui/teamodels/tableselect" - "github.com/lmika/audax/internal/dynamo-browse/ui/teamodels/utils" + "github.com/lmika/dynamo-browse/internal/common/ui/commandctrl" + "github.com/lmika/dynamo-browse/internal/common/ui/events" + "github.com/lmika/dynamo-browse/internal/dynamo-browse/controllers" + "github.com/lmika/dynamo-browse/internal/dynamo-browse/models" + "github.com/lmika/dynamo-browse/internal/dynamo-browse/services/itemrenderer" + "github.com/lmika/dynamo-browse/internal/dynamo-browse/ui/keybindings" + "github.com/lmika/dynamo-browse/internal/dynamo-browse/ui/teamodels/colselector" + "github.com/lmika/dynamo-browse/internal/dynamo-browse/ui/teamodels/dialogprompt" + "github.com/lmika/dynamo-browse/internal/dynamo-browse/ui/teamodels/dynamoitemedit" + "github.com/lmika/dynamo-browse/internal/dynamo-browse/ui/teamodels/dynamoitemview" + "github.com/lmika/dynamo-browse/internal/dynamo-browse/ui/teamodels/dynamotableview" + "github.com/lmika/dynamo-browse/internal/dynamo-browse/ui/teamodels/layout" + "github.com/lmika/dynamo-browse/internal/dynamo-browse/ui/teamodels/statusandprompt" + "github.com/lmika/dynamo-browse/internal/dynamo-browse/ui/teamodels/styles" + "github.com/lmika/dynamo-browse/internal/dynamo-browse/ui/teamodels/tableselect" + "github.com/lmika/dynamo-browse/internal/dynamo-browse/ui/teamodels/utils" bus "github.com/lmika/events" "github.com/pkg/errors" "log" diff --git a/internal/dynamo-browse/ui/teamodels/colselector/colmodel.go b/internal/dynamo-browse/ui/teamodels/colselector/colmodel.go index 7a6f2ae..a261032 100644 --- a/internal/dynamo-browse/ui/teamodels/colselector/colmodel.go +++ b/internal/dynamo-browse/ui/teamodels/colselector/colmodel.go @@ -4,11 +4,11 @@ import ( "github.com/charmbracelet/bubbles/key" tea "github.com/charmbracelet/bubbletea" "github.com/charmbracelet/lipgloss" - "github.com/lmika/audax/internal/common/ui/events" - "github.com/lmika/audax/internal/dynamo-browse/controllers" - "github.com/lmika/audax/internal/dynamo-browse/models/columns" - "github.com/lmika/audax/internal/dynamo-browse/ui/keybindings" - "github.com/lmika/audax/internal/dynamo-browse/ui/teamodels/layout" + "github.com/lmika/dynamo-browse/internal/common/ui/events" + "github.com/lmika/dynamo-browse/internal/dynamo-browse/controllers" + "github.com/lmika/dynamo-browse/internal/dynamo-browse/models/columns" + "github.com/lmika/dynamo-browse/internal/dynamo-browse/ui/keybindings" + "github.com/lmika/dynamo-browse/internal/dynamo-browse/ui/teamodels/layout" table "github.com/lmika/go-bubble-table" "log" "strings" diff --git a/internal/dynamo-browse/ui/teamodels/colselector/model.go b/internal/dynamo-browse/ui/teamodels/colselector/model.go index 8d9d805..2b572d7 100644 --- a/internal/dynamo-browse/ui/teamodels/colselector/model.go +++ b/internal/dynamo-browse/ui/teamodels/colselector/model.go @@ -2,10 +2,10 @@ package colselector import ( tea "github.com/charmbracelet/bubbletea" - "github.com/lmika/audax/internal/dynamo-browse/controllers" - "github.com/lmika/audax/internal/dynamo-browse/ui/keybindings" - "github.com/lmika/audax/internal/dynamo-browse/ui/teamodels/layout" - "github.com/lmika/audax/internal/dynamo-browse/ui/teamodels/utils" + "github.com/lmika/dynamo-browse/internal/dynamo-browse/controllers" + "github.com/lmika/dynamo-browse/internal/dynamo-browse/ui/keybindings" + "github.com/lmika/dynamo-browse/internal/dynamo-browse/ui/teamodels/layout" + "github.com/lmika/dynamo-browse/internal/dynamo-browse/ui/teamodels/utils" ) const ( diff --git a/internal/dynamo-browse/ui/teamodels/dialogprompt/dialogmodel.go b/internal/dynamo-browse/ui/teamodels/dialogprompt/dialogmodel.go index 467eca6..da74c27 100644 --- a/internal/dynamo-browse/ui/teamodels/dialogprompt/dialogmodel.go +++ b/internal/dynamo-browse/ui/teamodels/dialogprompt/dialogmodel.go @@ -3,7 +3,7 @@ package dialogprompt import ( tea "github.com/charmbracelet/bubbletea" "github.com/charmbracelet/lipgloss" - "github.com/lmika/audax/internal/dynamo-browse/ui/teamodels/layout" + "github.com/lmika/dynamo-browse/internal/dynamo-browse/ui/teamodels/layout" ) var style = lipgloss.NewStyle(). diff --git a/internal/dynamo-browse/ui/teamodels/dialogprompt/model.go b/internal/dynamo-browse/ui/teamodels/dialogprompt/model.go index e5629b0..6711293 100644 --- a/internal/dynamo-browse/ui/teamodels/dialogprompt/model.go +++ b/internal/dynamo-browse/ui/teamodels/dialogprompt/model.go @@ -2,7 +2,7 @@ package dialogprompt import ( tea "github.com/charmbracelet/bubbletea" - "github.com/lmika/audax/internal/dynamo-browse/ui/teamodels/layout" + "github.com/lmika/dynamo-browse/internal/dynamo-browse/ui/teamodels/layout" ) type Model struct { diff --git a/internal/dynamo-browse/ui/teamodels/dynamoitemedit/model.go b/internal/dynamo-browse/ui/teamodels/dynamoitemedit/model.go index a827dd8..48d8362 100644 --- a/internal/dynamo-browse/ui/teamodels/dynamoitemedit/model.go +++ b/internal/dynamo-browse/ui/teamodels/dynamoitemedit/model.go @@ -4,8 +4,8 @@ import ( table "github.com/calyptia/go-bubble-table" "github.com/charmbracelet/bubbles/textinput" tea "github.com/charmbracelet/bubbletea" - "github.com/lmika/audax/internal/dynamo-browse/ui/teamodels/layout" - "github.com/lmika/audax/internal/dynamo-browse/ui/teamodels/utils" + "github.com/lmika/dynamo-browse/internal/dynamo-browse/ui/teamodels/layout" + "github.com/lmika/dynamo-browse/internal/dynamo-browse/ui/teamodels/utils" ) type Model struct { diff --git a/internal/dynamo-browse/ui/teamodels/dynamoitemview/events.go b/internal/dynamo-browse/ui/teamodels/dynamoitemview/events.go index ec393f0..030be9e 100644 --- a/internal/dynamo-browse/ui/teamodels/dynamoitemview/events.go +++ b/internal/dynamo-browse/ui/teamodels/dynamoitemview/events.go @@ -1,6 +1,6 @@ package dynamoitemview -import "github.com/lmika/audax/internal/dynamo-browse/models" +import "github.com/lmika/dynamo-browse/internal/dynamo-browse/models" type NewItemSelected struct { ResultSet *models.ResultSet diff --git a/internal/dynamo-browse/ui/teamodels/dynamoitemview/model.go b/internal/dynamo-browse/ui/teamodels/dynamoitemview/model.go index 1703a30..1d8adae 100644 --- a/internal/dynamo-browse/ui/teamodels/dynamoitemview/model.go +++ b/internal/dynamo-browse/ui/teamodels/dynamoitemview/model.go @@ -4,11 +4,11 @@ import ( "github.com/charmbracelet/bubbles/viewport" tea "github.com/charmbracelet/bubbletea" "github.com/charmbracelet/lipgloss" - "github.com/lmika/audax/internal/dynamo-browse/models" - "github.com/lmika/audax/internal/dynamo-browse/services/itemrenderer" - "github.com/lmika/audax/internal/dynamo-browse/ui/teamodels/frame" - "github.com/lmika/audax/internal/dynamo-browse/ui/teamodels/layout" - "github.com/lmika/audax/internal/dynamo-browse/ui/teamodels/styles" + "github.com/lmika/dynamo-browse/internal/dynamo-browse/models" + "github.com/lmika/dynamo-browse/internal/dynamo-browse/services/itemrenderer" + "github.com/lmika/dynamo-browse/internal/dynamo-browse/ui/teamodels/frame" + "github.com/lmika/dynamo-browse/internal/dynamo-browse/ui/teamodels/layout" + "github.com/lmika/dynamo-browse/internal/dynamo-browse/ui/teamodels/styles" "strings" ) diff --git a/internal/dynamo-browse/ui/teamodels/dynamotableview/model.go b/internal/dynamo-browse/ui/teamodels/dynamotableview/model.go index a7b3c57..cf61d72 100644 --- a/internal/dynamo-browse/ui/teamodels/dynamotableview/model.go +++ b/internal/dynamo-browse/ui/teamodels/dynamotableview/model.go @@ -4,14 +4,14 @@ import ( "github.com/charmbracelet/bubbles/key" tea "github.com/charmbracelet/bubbletea" "github.com/charmbracelet/lipgloss" - "github.com/lmika/audax/internal/dynamo-browse/controllers" - "github.com/lmika/audax/internal/dynamo-browse/models" - "github.com/lmika/audax/internal/dynamo-browse/models/columns" - "github.com/lmika/audax/internal/dynamo-browse/ui/keybindings" - "github.com/lmika/audax/internal/dynamo-browse/ui/teamodels/dynamoitemview" - "github.com/lmika/audax/internal/dynamo-browse/ui/teamodels/frame" - "github.com/lmika/audax/internal/dynamo-browse/ui/teamodels/layout" - "github.com/lmika/audax/internal/dynamo-browse/ui/teamodels/styles" + "github.com/lmika/dynamo-browse/internal/dynamo-browse/controllers" + "github.com/lmika/dynamo-browse/internal/dynamo-browse/models" + "github.com/lmika/dynamo-browse/internal/dynamo-browse/models/columns" + "github.com/lmika/dynamo-browse/internal/dynamo-browse/ui/keybindings" + "github.com/lmika/dynamo-browse/internal/dynamo-browse/ui/teamodels/dynamoitemview" + "github.com/lmika/dynamo-browse/internal/dynamo-browse/ui/teamodels/frame" + "github.com/lmika/dynamo-browse/internal/dynamo-browse/ui/teamodels/layout" + "github.com/lmika/dynamo-browse/internal/dynamo-browse/ui/teamodels/styles" bus "github.com/lmika/events" table "github.com/lmika/go-bubble-table" "strings" diff --git a/internal/dynamo-browse/ui/teamodels/dynamotableview/tblmodel.go b/internal/dynamo-browse/ui/teamodels/dynamotableview/tblmodel.go index feeb5fd..a38aea8 100644 --- a/internal/dynamo-browse/ui/teamodels/dynamotableview/tblmodel.go +++ b/internal/dynamo-browse/ui/teamodels/dynamotableview/tblmodel.go @@ -3,11 +3,11 @@ package dynamotableview import ( "fmt" "github.com/charmbracelet/lipgloss" - "github.com/lmika/audax/internal/dynamo-browse/models/itemrender" + "github.com/lmika/dynamo-browse/internal/dynamo-browse/models/itemrender" "io" "strings" - "github.com/lmika/audax/internal/dynamo-browse/models" + "github.com/lmika/dynamo-browse/internal/dynamo-browse/models" table "github.com/lmika/go-bubble-table" ) diff --git a/internal/dynamo-browse/ui/teamodels/frame/frame.go b/internal/dynamo-browse/ui/teamodels/frame/frame.go index 032ce1a..e2aea1c 100644 --- a/internal/dynamo-browse/ui/teamodels/frame/frame.go +++ b/internal/dynamo-browse/ui/teamodels/frame/frame.go @@ -4,7 +4,7 @@ import ( "strings" "github.com/charmbracelet/lipgloss" - "github.com/lmika/audax/internal/dynamo-browse/ui/teamodels/utils" + "github.com/lmika/dynamo-browse/internal/dynamo-browse/ui/teamodels/utils" ) var ( diff --git a/internal/dynamo-browse/ui/teamodels/itemdisplay/model.go b/internal/dynamo-browse/ui/teamodels/itemdisplay/model.go index 550f8ee..138b771 100644 --- a/internal/dynamo-browse/ui/teamodels/itemdisplay/model.go +++ b/internal/dynamo-browse/ui/teamodels/itemdisplay/model.go @@ -2,8 +2,8 @@ package itemdisplay import ( tea "github.com/charmbracelet/bubbletea" - "github.com/lmika/audax/internal/dynamo-browse/ui/teamodels/layout" - "github.com/lmika/audax/internal/dynamo-browse/ui/teamodels/utils" + "github.com/lmika/dynamo-browse/internal/dynamo-browse/ui/teamodels/layout" + "github.com/lmika/dynamo-browse/internal/dynamo-browse/ui/teamodels/utils" ) type Model struct { diff --git a/internal/dynamo-browse/ui/teamodels/layout/model.go b/internal/dynamo-browse/ui/teamodels/layout/model.go index 9ffe92c..13a5791 100644 --- a/internal/dynamo-browse/ui/teamodels/layout/model.go +++ b/internal/dynamo-browse/ui/teamodels/layout/model.go @@ -3,7 +3,7 @@ package layout import ( tea "github.com/charmbracelet/bubbletea" "github.com/charmbracelet/lipgloss" - "github.com/lmika/audax/internal/dynamo-browse/ui/teamodels/utils" + "github.com/lmika/dynamo-browse/internal/dynamo-browse/ui/teamodels/utils" "strconv" "strings" ) diff --git a/internal/dynamo-browse/ui/teamodels/layout/vbox.go b/internal/dynamo-browse/ui/teamodels/layout/vbox.go index 3f4be6e..e52a917 100644 --- a/internal/dynamo-browse/ui/teamodels/layout/vbox.go +++ b/internal/dynamo-browse/ui/teamodels/layout/vbox.go @@ -4,7 +4,7 @@ import ( "strings" tea "github.com/charmbracelet/bubbletea" - "github.com/lmika/audax/internal/dynamo-browse/ui/teamodels/utils" + "github.com/lmika/dynamo-browse/internal/dynamo-browse/ui/teamodels/utils" ) // VBox is a model which will display its children vertically. diff --git a/internal/dynamo-browse/ui/teamodels/layout/zstack.go b/internal/dynamo-browse/ui/teamodels/layout/zstack.go index 63aa687..2d271c8 100644 --- a/internal/dynamo-browse/ui/teamodels/layout/zstack.go +++ b/internal/dynamo-browse/ui/teamodels/layout/zstack.go @@ -2,7 +2,7 @@ package layout import ( tea "github.com/charmbracelet/bubbletea" - "github.com/lmika/audax/internal/dynamo-browse/ui/teamodels/utils" + "github.com/lmika/dynamo-browse/internal/dynamo-browse/ui/teamodels/utils" ) type ZStack struct { diff --git a/internal/dynamo-browse/ui/teamodels/modal/model.go b/internal/dynamo-browse/ui/teamodels/modal/model.go index a9e9767..6d8bec8 100644 --- a/internal/dynamo-browse/ui/teamodels/modal/model.go +++ b/internal/dynamo-browse/ui/teamodels/modal/model.go @@ -2,8 +2,8 @@ package modal import ( tea "github.com/charmbracelet/bubbletea" - "github.com/lmika/audax/internal/dynamo-browse/ui/teamodels/layout" - "github.com/lmika/audax/internal/dynamo-browse/ui/teamodels/utils" + "github.com/lmika/dynamo-browse/internal/dynamo-browse/ui/teamodels/layout" + "github.com/lmika/dynamo-browse/internal/dynamo-browse/ui/teamodels/utils" "log" ) diff --git a/internal/dynamo-browse/ui/teamodels/statusandprompt/model.go b/internal/dynamo-browse/ui/teamodels/statusandprompt/model.go index 62be7b0..7bdab7c 100644 --- a/internal/dynamo-browse/ui/teamodels/statusandprompt/model.go +++ b/internal/dynamo-browse/ui/teamodels/statusandprompt/model.go @@ -5,10 +5,10 @@ import ( "github.com/charmbracelet/bubbles/textinput" tea "github.com/charmbracelet/bubbletea" "github.com/charmbracelet/lipgloss" - "github.com/lmika/audax/internal/common/sliceutils" - "github.com/lmika/audax/internal/common/ui/events" - "github.com/lmika/audax/internal/dynamo-browse/ui/teamodels/layout" - "github.com/lmika/audax/internal/dynamo-browse/ui/teamodels/utils" + "github.com/lmika/dynamo-browse/internal/common/sliceutils" + "github.com/lmika/dynamo-browse/internal/common/ui/events" + "github.com/lmika/dynamo-browse/internal/dynamo-browse/ui/teamodels/layout" + "github.com/lmika/dynamo-browse/internal/dynamo-browse/ui/teamodels/utils" ) // StatusAndPrompt is a resizing model which displays a submodel and a status bar. When the start prompt diff --git a/internal/dynamo-browse/ui/teamodels/statusandprompt/types.go b/internal/dynamo-browse/ui/teamodels/statusandprompt/types.go index 10c766c..8f3ccc3 100644 --- a/internal/dynamo-browse/ui/teamodels/statusandprompt/types.go +++ b/internal/dynamo-browse/ui/teamodels/statusandprompt/types.go @@ -1,6 +1,6 @@ package statusandprompt -import "github.com/lmika/audax/internal/common/ui/events" +import "github.com/lmika/dynamo-browse/internal/common/ui/events" type pendingInputState struct { originalMsg events.PromptForInputMsg diff --git a/internal/dynamo-browse/ui/teamodels/styles/styles.go b/internal/dynamo-browse/ui/teamodels/styles/styles.go index 29c25de..9d4fc0e 100644 --- a/internal/dynamo-browse/ui/teamodels/styles/styles.go +++ b/internal/dynamo-browse/ui/teamodels/styles/styles.go @@ -2,8 +2,8 @@ package styles import ( "github.com/charmbracelet/lipgloss" - "github.com/lmika/audax/internal/dynamo-browse/ui/teamodels/frame" - "github.com/lmika/audax/internal/dynamo-browse/ui/teamodels/statusandprompt" + "github.com/lmika/dynamo-browse/internal/dynamo-browse/ui/teamodels/frame" + "github.com/lmika/dynamo-browse/internal/dynamo-browse/ui/teamodels/statusandprompt" ) type Styles struct { diff --git a/internal/dynamo-browse/ui/teamodels/tableselect/list.go b/internal/dynamo-browse/ui/teamodels/tableselect/list.go index 629a178..21fa76e 100644 --- a/internal/dynamo-browse/ui/teamodels/tableselect/list.go +++ b/internal/dynamo-browse/ui/teamodels/tableselect/list.go @@ -5,7 +5,7 @@ import ( "github.com/charmbracelet/bubbles/list" tea "github.com/charmbracelet/bubbletea" "github.com/charmbracelet/lipgloss" - "github.com/lmika/audax/internal/dynamo-browse/ui/teamodels/layout" + "github.com/lmika/dynamo-browse/internal/dynamo-browse/ui/teamodels/layout" ) var ( diff --git a/internal/dynamo-browse/ui/teamodels/tableselect/model.go b/internal/dynamo-browse/ui/teamodels/tableselect/model.go index 95c03a1..de18bab 100644 --- a/internal/dynamo-browse/ui/teamodels/tableselect/model.go +++ b/internal/dynamo-browse/ui/teamodels/tableselect/model.go @@ -5,12 +5,12 @@ import ( "github.com/charmbracelet/bubbles/list" tea "github.com/charmbracelet/bubbletea" "github.com/charmbracelet/lipgloss" - "github.com/lmika/audax/internal/common/ui/events" - "github.com/lmika/audax/internal/dynamo-browse/controllers" - "github.com/lmika/audax/internal/dynamo-browse/ui/teamodels/frame" - "github.com/lmika/audax/internal/dynamo-browse/ui/teamodels/layout" - "github.com/lmika/audax/internal/dynamo-browse/ui/teamodels/styles" - "github.com/lmika/audax/internal/dynamo-browse/ui/teamodels/utils" + "github.com/lmika/dynamo-browse/internal/common/ui/events" + "github.com/lmika/dynamo-browse/internal/dynamo-browse/controllers" + "github.com/lmika/dynamo-browse/internal/dynamo-browse/ui/teamodels/frame" + "github.com/lmika/dynamo-browse/internal/dynamo-browse/ui/teamodels/layout" + "github.com/lmika/dynamo-browse/internal/dynamo-browse/ui/teamodels/styles" + "github.com/lmika/dynamo-browse/internal/dynamo-browse/ui/teamodels/utils" ) var ( diff --git a/test/cmd/load-test-table/main.go b/test/cmd/load-test-table/main.go index 5665378..154d99e 100644 --- a/test/cmd/load-test-table/main.go +++ b/test/cmd/load-test-table/main.go @@ -9,9 +9,9 @@ import ( "github.com/aws/aws-sdk-go-v2/service/dynamodb" "github.com/aws/aws-sdk-go-v2/service/dynamodb/types" "github.com/brianvoe/gofakeit/v6" - "github.com/lmika/audax/internal/dynamo-browse/models" - "github.com/lmika/audax/internal/dynamo-browse/providers/dynamo" - "github.com/lmika/audax/internal/dynamo-browse/services/tables" + "github.com/lmika/dynamo-browse/internal/dynamo-browse/models" + "github.com/lmika/dynamo-browse/internal/dynamo-browse/providers/dynamo" + "github.com/lmika/dynamo-browse/internal/dynamo-browse/services/tables" "github.com/lmika/gopkgs/cli" "github.com/pkg/errors" "log" diff --git a/test/testdynamo/helpers.go b/test/testdynamo/helpers.go index 0a834ab..37daeea 100644 --- a/test/testdynamo/helpers.go +++ b/test/testdynamo/helpers.go @@ -4,7 +4,7 @@ import ( "testing" "github.com/aws/aws-sdk-go-v2/feature/dynamodb/attributevalue" - "github.com/lmika/audax/internal/dynamo-browse/models" + "github.com/lmika/dynamo-browse/internal/dynamo-browse/models" "github.com/stretchr/testify/assert" ) diff --git a/test/testuictx/testuictx.go b/test/testuictx/testuictx.go index 6668ca2..e75ff28 100644 --- a/test/testuictx/testuictx.go +++ b/test/testuictx/testuictx.go @@ -4,8 +4,8 @@ import ( "context" tea "github.com/charmbracelet/bubbletea" - "github.com/lmika/audax/internal/common/ui/dispatcher" - "github.com/lmika/audax/internal/common/ui/uimodels" + "github.com/lmika/dynamo-browse/internal/common/ui/dispatcher" + "github.com/lmika/dynamo-browse/internal/common/ui/uimodels" ) func New(ctx context.Context) (context.Context, *TestUIContext) { diff --git a/test/testworkspace/workspace.go b/test/testworkspace/workspace.go index d3f6f34..9715d0e 100644 --- a/test/testworkspace/workspace.go +++ b/test/testworkspace/workspace.go @@ -1,7 +1,7 @@ package testworkspace import ( - "github.com/lmika/audax/internal/common/workspaces" + "github.com/lmika/dynamo-browse/internal/common/workspaces" "github.com/stretchr/testify/assert" "os" "testing" From 20a9a8c758c8a76cc37af0f3eccb69050c7473cd Mon Sep 17 00:00:00 2001 From: Leon Mika Date: Mon, 3 Jul 2023 11:24:16 +1000 Subject: [PATCH 03/10] fix: Added a small timeout to the runNow() script scheduler This is to avoid a small race conditions in the tests, where a script has signalled that it's finished loading but the schedular has not started waiting for the next task. --- internal/dynamo-browse/services/scriptmanager/scrsched.go | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/internal/dynamo-browse/services/scriptmanager/scrsched.go b/internal/dynamo-browse/services/scriptmanager/scrsched.go index e04ebdf..3846676 100644 --- a/internal/dynamo-browse/services/scriptmanager/scrsched.go +++ b/internal/dynamo-browse/services/scriptmanager/scrsched.go @@ -3,6 +3,7 @@ package scriptmanager import ( "context" "github.com/pkg/errors" + "time" ) type scriptScheduler struct { @@ -41,7 +42,7 @@ func (ss *scriptScheduler) runNow(ctx context.Context, job func(ctx context.Cont select { case ss.jobChan <- scriptJob{ctx: ctx, job: job}: return nil - default: + case <-time.After(500 * time.Millisecond): return errors.New("a script is already running") } } From ed53173a1d3e587383bb9055c2144042c24cddac Mon Sep 17 00:00:00 2001 From: Leon Mika Date: Mon, 31 Jul 2023 20:59:05 +1000 Subject: [PATCH 04/10] Added the "export -all" switch (#54) Extended the "export" command with an "-all" flag. When included, all rows of the table matching the query will be exported to CSV. --- cmd/dynamo-browse/main.go | 2 +- internal/common/sliceutils/map.go | 8 ++ internal/dynamo-browse/controllers/export.go | 94 ++++++++++++------- .../dynamo-browse/controllers/export_test.go | 64 ++++++++++++- .../dynamo-browse/controllers/jobbuilder.go | 3 + .../dynamo-browse/controllers/tableread.go | 2 +- .../controllers/tableread_test.go | 6 +- .../controllers/tablewrite_test.go | 2 +- internal/dynamo-browse/models/models.go | 4 + internal/dynamo-browse/ui/model.go | 9 +- 10 files changed, 150 insertions(+), 44 deletions(-) diff --git a/cmd/dynamo-browse/main.go b/cmd/dynamo-browse/main.go index 1e0b05d..170a2dc 100644 --- a/cmd/dynamo-browse/main.go +++ b/cmd/dynamo-browse/main.go @@ -108,7 +108,7 @@ func main() { tableReadController := controllers.NewTableReadController(state, tableService, workspaceService, itemRendererService, jobsController, inputHistoryService, eventBus, *flagTable) tableWriteController := controllers.NewTableWriteController(state, tableService, jobsController, tableReadController, settingStore) columnsController := controllers.NewColumnsController(eventBus) - exportController := controllers.NewExportController(state, columnsController) + exportController := controllers.NewExportController(state, tableService, jobsController, columnsController) settingsController := controllers.NewSettingsController(settingStore, eventBus) keyBindings := keybindings.Default() scriptController := controllers.NewScriptController(scriptManagerService, tableReadController, settingsController, eventBus) diff --git a/internal/common/sliceutils/map.go b/internal/common/sliceutils/map.go index ae2a1be..0864a56 100644 --- a/internal/common/sliceutils/map.go +++ b/internal/common/sliceutils/map.go @@ -9,6 +9,14 @@ func All[T any](ts []T, predicate func(t T) bool) bool { return true } +func Generate[U any](from, to int, fn func(t int) U) []U { + us := make([]U, to-from+1) + for i := from; i <= to; i++ { + us[i-from] = fn(i) + } + return us +} + func Map[T, U any](ts []T, fn func(t T) U) []U { us := make([]U, len(ts)) for i, t := range ts { diff --git a/internal/dynamo-browse/controllers/export.go b/internal/dynamo-browse/controllers/export.go index 3f70c93..15a86c6 100644 --- a/internal/dynamo-browse/controllers/export.go +++ b/internal/dynamo-browse/controllers/export.go @@ -1,57 +1,85 @@ package controllers import ( + "context" "encoding/csv" + "fmt" tea "github.com/charmbracelet/bubbletea" "github.com/lmika/dynamo-browse/internal/common/ui/events" "github.com/lmika/dynamo-browse/internal/dynamo-browse/models/attrutils" + "github.com/lmika/dynamo-browse/internal/dynamo-browse/services/jobs" "github.com/pkg/errors" "os" ) type ExportController struct { - state *State - columns *ColumnsController + state *State + tableService TableReadService + jobController *JobsController + columns *ColumnsController } -func NewExportController(state *State, columns *ColumnsController) *ExportController { - return &ExportController{state, columns} +func NewExportController(state *State, tableService TableReadService, jobsController *JobsController, columns *ColumnsController) *ExportController { + return &ExportController{state, tableService, jobsController, columns} } -func (c *ExportController) ExportCSV(filename string) tea.Msg { +func (c *ExportController) ExportCSV(filename string, opts ExportOptions) tea.Msg { resultSet := c.state.ResultSet() if resultSet == nil { return events.Error(errors.New("no result set")) } - f, err := os.Create(filename) - if err != nil { - return events.Error(errors.Wrapf(err, "cannot export to '%v'", filename)) - } - defer f.Close() - - cw := csv.NewWriter(f) - defer cw.Flush() - - columns := c.columns.Columns().VisibleColumns() - - colNames := make([]string, len(columns)) - for i, c := range columns { - colNames[i] = c.Name - } - if err := cw.Write(colNames); err != nil { - return events.Error(errors.Wrapf(err, "cannot export to '%v'", filename)) - } - - row := make([]string, len(columns)) - for _, item := range resultSet.Items() { - for i, col := range columns { - row[i], _ = attrutils.AttributeToString(col.Evaluator.EvaluateForItem(item)) + return NewJob(c.jobController, fmt.Sprintf("Exporting to %v…", filename), func(ctx context.Context) (int, error) { + f, err := os.Create(filename) + if err != nil { + return 0, errors.Wrapf(err, "cannot export to '%v'", filename) } - if err := cw.Write(row); err != nil { - return events.Error(errors.Wrapf(err, "cannot export to '%v'", filename)) - } - } + defer f.Close() - return nil + cw := csv.NewWriter(f) + defer cw.Flush() + + columns := c.columns.Columns().VisibleColumns() + + colNames := make([]string, len(columns)) + for i, c := range columns { + colNames[i] = c.Name + } + if err := cw.Write(colNames); err != nil { + return 0, errors.Wrapf(err, "cannot export to '%v'", filename) + } + + totalRows := 0 + row := make([]string, len(columns)) + for { + for _, item := range resultSet.Items() { + for i, col := range columns { + row[i], _ = attrutils.AttributeToString(col.Evaluator.EvaluateForItem(item)) + } + if err := cw.Write(row); err != nil { + return 0, errors.Wrapf(err, "cannot export to '%v'", filename) + } + } + totalRows += len(resultSet.Items()) + + if !opts.AllResults || !resultSet.HasNextPage() { + break + } + + jobs.PostUpdate(ctx, fmt.Sprintf("exported %d items", totalRows)) + resultSet, err = c.tableService.NextPage(ctx, resultSet) + if err != nil { + return 0, errors.Wrapf(err, "cannot get next page while exporting to '%v'", filename) + } + } + + return totalRows, nil + }).OnDone(func(rows int) tea.Msg { + return events.StatusMsg(applyToN("Exported ", rows, "item", "items", " to "+filename)) + }).Submit() +} + +type ExportOptions struct { + // AllResults returns all results from the table + AllResults bool } diff --git a/internal/dynamo-browse/controllers/export_test.go b/internal/dynamo-browse/controllers/export_test.go index f70a387..ce62fc3 100644 --- a/internal/dynamo-browse/controllers/export_test.go +++ b/internal/dynamo-browse/controllers/export_test.go @@ -1,6 +1,9 @@ package controllers_test import ( + "fmt" + "github.com/lmika/dynamo-browse/internal/common/sliceutils" + "github.com/lmika/dynamo-browse/internal/dynamo-browse/controllers" "github.com/stretchr/testify/assert" "os" "strings" @@ -14,7 +17,7 @@ func TestExportController_ExportCSV(t *testing.T) { tempFile := tempFile(t) invokeCommand(t, srv.readController.Init()) - invokeCommand(t, srv.exportController.ExportCSV(tempFile)) + invokeCommand(t, srv.exportController.ExportCSV(tempFile, controllers.ExportOptions{})) bts, err := os.ReadFile(tempFile) assert.NoError(t, err) @@ -27,13 +30,66 @@ func TestExportController_ExportCSV(t *testing.T) { }, "")) }) + t.Run("should export all pages of the results", func(t *testing.T) { + pageLimits := []int{5, 10, 50} + + for _, pageLimit := range pageLimits { + t.Run(fmt.Sprintf("page size %d", pageLimit), func(t *testing.T) { + t.Run("all results", func(t *testing.T) { + srv := newService(t, serviceConfig{tableName: "count-to-30", defaultLimit: 5}) + + tempFile := tempFile(t) + + expected := append([]string{ + "pk,sk,num\n", + }, sliceutils.Generate(1, 30, func(i int) string { + return fmt.Sprintf("NUM,NUM#%02d,%d\n", i, i) + })...) + + invokeCommand(t, srv.readController.Init()) + invokeCommand(t, srv.exportController.ExportCSV(tempFile, controllers.ExportOptions{ + AllResults: true, + })) + + bts, err := os.ReadFile(tempFile) + assert.NoError(t, err) + + assert.Equal(t, strings.Join(expected, ""), string(bts)) + }) + + t.Run("with query", func(t *testing.T) { + srv := newService(t, serviceConfig{tableName: "count-to-30", defaultLimit: 5}) + + tempFile := tempFile(t) + + expected := append([]string{ + "pk,sk,num\n", + }, sliceutils.Generate(1, 15, func(i int) string { + return fmt.Sprintf("NUM,NUM#%02d,%d\n", i, i) + })...) + + invokeCommand(t, srv.readController.Init()) + invokeCommandWithPrompt(t, srv.readController.PromptForQuery(), "num<=15") + invokeCommand(t, srv.exportController.ExportCSV(tempFile, controllers.ExportOptions{ + AllResults: true, + })) + + bts, err := os.ReadFile(tempFile) + assert.NoError(t, err) + + assert.Equal(t, strings.Join(expected, ""), string(bts)) + }) + }) + } + }) + t.Run("should return error if result set is not set", func(t *testing.T) { srv := newService(t, serviceConfig{tableName: "non-existant-table"}) tempFile := tempFile(t) invokeCommandExpectingError(t, srv.readController.Init()) - invokeCommandExpectingError(t, srv.exportController.ExportCSV(tempFile)) + invokeCommandExpectingError(t, srv.exportController.ExportCSV(tempFile, controllers.ExportOptions{})) }) t.Run("should honour new columns in CSV file", func(t *testing.T) { @@ -48,7 +104,7 @@ func TestExportController_ExportCSV(t *testing.T) { invokeCommandWithPrompt(t, srv.columnsController.AddColumn(1), "address.street") invokeCommand(t, srv.columnsController.ShiftColumnLeft(1)) - invokeCommand(t, srv.exportController.ExportCSV(tempFile)) + invokeCommand(t, srv.exportController.ExportCSV(tempFile, controllers.ExportOptions{})) bts, err := os.ReadFile(tempFile) assert.NoError(t, err) @@ -71,7 +127,7 @@ func TestExportController_ExportCSV(t *testing.T) { invokeCommand(t, srv.columnsController.ToggleVisible(1)) invokeCommand(t, srv.columnsController.ToggleVisible(2)) - invokeCommand(t, srv.exportController.ExportCSV(tempFile)) + invokeCommand(t, srv.exportController.ExportCSV(tempFile, controllers.ExportOptions{})) bts, err := os.ReadFile(tempFile) assert.NoError(t, err) diff --git a/internal/dynamo-browse/controllers/jobbuilder.go b/internal/dynamo-browse/controllers/jobbuilder.go index cbe7004..2128c44 100644 --- a/internal/dynamo-browse/controllers/jobbuilder.go +++ b/internal/dynamo-browse/controllers/jobbuilder.go @@ -51,6 +51,9 @@ func (jb JobBuilder[T]) executeJob(ctx context.Context) tea.Msg { if jb.onEither != nil { return jb.onEither(res, err) } else if err == nil { + if jb.onDone == nil { + return nil + } return jb.onDone(res) } else { if jb.onErr != nil { diff --git a/internal/dynamo-browse/controllers/tableread.go b/internal/dynamo-browse/controllers/tableread.go index a39bc1c..5297d1a 100644 --- a/internal/dynamo-browse/controllers/tableread.go +++ b/internal/dynamo-browse/controllers/tableread.go @@ -377,7 +377,7 @@ func (c *TableReadController) NextPage() tea.Msg { resultSet := c.state.ResultSet() if resultSet == nil { return events.StatusMsg("Result-set is nil") - } else if resultSet.LastEvaluatedKey == nil { + } else if !resultSet.HasNextPage() { return events.StatusMsg("No more results") } currentFilter := c.state.filter diff --git a/internal/dynamo-browse/controllers/tableread_test.go b/internal/dynamo-browse/controllers/tableread_test.go index c042983..18bf075 100644 --- a/internal/dynamo-browse/controllers/tableread_test.go +++ b/internal/dynamo-browse/controllers/tableread_test.go @@ -89,7 +89,7 @@ func TestTableReadController_Query(t *testing.T) { invokeCommand(t, srv.readController.Init()) invokeCommandWithPrompts(t, srv.readController.PromptForQuery(), `pk ^= "abc"`) - invokeCommand(t, srv.exportController.ExportCSV(tempFile)) + invokeCommand(t, srv.exportController.ExportCSV(tempFile, controllers.ExportOptions{})) bts, err := os.ReadFile(tempFile) assert.NoError(t, err) @@ -107,7 +107,7 @@ func TestTableReadController_Query(t *testing.T) { invokeCommand(t, srv.readController.Init()) invokeCommandWithPrompts(t, srv.readController.PromptForQuery(), `alpha = "This is some value"`) - invokeCommand(t, srv.exportController.ExportCSV(tempFile)) + invokeCommand(t, srv.exportController.ExportCSV(tempFile, controllers.ExportOptions{})) bts, err := os.ReadFile(tempFile) assert.NoError(t, err) @@ -124,7 +124,7 @@ func TestTableReadController_Query(t *testing.T) { tempFile := tempFile(t) invokeCommandExpectingError(t, srv.readController.Init()) - invokeCommandExpectingError(t, srv.exportController.ExportCSV(tempFile)) + invokeCommandExpectingError(t, srv.exportController.ExportCSV(tempFile, controllers.ExportOptions{})) }) } diff --git a/internal/dynamo-browse/controllers/tablewrite_test.go b/internal/dynamo-browse/controllers/tablewrite_test.go index a28f182..a4f25ec 100644 --- a/internal/dynamo-browse/controllers/tablewrite_test.go +++ b/internal/dynamo-browse/controllers/tablewrite_test.go @@ -621,7 +621,7 @@ func newService(t *testing.T, cfg serviceConfig) *services { writeController := controllers.NewTableWriteController(state, service, jobsController, readController, settingStore) settingsController := controllers.NewSettingsController(settingStore, eventBus) columnsController := controllers.NewColumnsController(eventBus) - exportController := controllers.NewExportController(state, columnsController) + exportController := controllers.NewExportController(state, service, jobsController, columnsController) scriptController := controllers.NewScriptController(scriptService, readController, settingsController, eventBus) commandController := commandctrl.NewCommandController(inputHistoryService) diff --git a/internal/dynamo-browse/models/models.go b/internal/dynamo-browse/models/models.go index 1e9bf16..f94aa72 100644 --- a/internal/dynamo-browse/models/models.go +++ b/internal/dynamo-browse/models/models.go @@ -135,3 +135,7 @@ func (rs *ResultSet) RefreshColumns() { rs.columns = columns } + +func (rs *ResultSet) HasNextPage() bool { + return rs.LastEvaluatedKey != nil +} diff --git a/internal/dynamo-browse/ui/model.go b/internal/dynamo-browse/ui/model.go index b3aa53a..35aa9cd 100644 --- a/internal/dynamo-browse/ui/model.go +++ b/internal/dynamo-browse/ui/model.go @@ -102,7 +102,14 @@ func NewModel( if len(args) == 0 { return events.Error(errors.New("expected filename")) } - return exportController.ExportCSV(args[0]) + + opts := controllers.ExportOptions{} + if len(args) == 2 && args[0] == "-all" { + opts.AllResults = true + args = args[1:] + } + + return exportController.ExportCSV(args[0], opts) }, "mark": func(ctx commandctrl.ExecContext, args []string) tea.Msg { var markOp = controllers.MarkOpMark From 7ca0cf69820f246cb0f3104049c41bf3f1153d74 Mon Sep 17 00:00:00 2001 From: Leon Mika Date: Fri, 6 Oct 2023 15:27:06 +1100 Subject: [PATCH 05/10] Converted scripting language Tamarin to Risor (#55) - Converted Tamarin script language to Risor - Added a "find" and "merge" method to the result set script type. - Added the ability to copy the table of results to the pasteboard by pressing C - Added the -q flag, which will run a query and display the results as a CSV file on the command line - Upgraded Go to 1.21 in Github actions - Fix issue with missing limits - Added the '-where' switch to the mark - Added the 'marked' function to the query expression. - Added a sampled time and count on the right-side of the mode line - Added the 'M' key binding to toggle the marked items - Started working on tab completion for 'sa' and 'da' commands - Added count and sample time to the right-side of the mode line - Added Ctrl+V to the prompt to paste the text of the pasteboard with all whitespace characters trimmed - Fixed failing unit tests --- .github/workflows/ci.yaml | 2 +- .github/workflows/release.yaml | 2 +- cmd/dynamo-browse/main.go | 47 +++++- go.mod | 80 +++++++--- go.sum | 149 ++++++++++++++++++ internal/common/ui/commandctrl/commandctrl.go | 29 +++- internal/common/ui/commandctrl/types.go | 4 + internal/common/ui/events/commands.go | 5 + internal/common/ui/events/errors.go | 9 +- internal/dynamo-browse/controllers/columns.go | 11 ++ internal/dynamo-browse/controllers/events.go | 20 +++ internal/dynamo-browse/controllers/export.go | 75 ++++++++- .../dynamo-browse/controllers/scripts_test.go | 50 +++--- internal/dynamo-browse/controllers/state.go | 3 +- .../dynamo-browse/controllers/tableread.go | 82 ++++++---- .../dynamo-browse/controllers/tablewrite.go | 38 +++++ .../controllers/tablewrite_test.go | 15 +- .../dynamo-browse/models/attrutils/truthy.go | 32 ++++ internal/dynamo-browse/models/items.go | 10 +- internal/dynamo-browse/models/models.go | 2 + .../models/queryexpr/builtins.go | 37 +++++ .../dynamo-browse/models/queryexpr/context.go | 13 ++ .../dynamo-browse/models/queryexpr/dot.go | 2 +- .../models/queryexpr/equality.go | 4 +- .../dynamo-browse/models/queryexpr/errors.go | 4 + .../dynamo-browse/models/queryexpr/expr.go | 53 ++++--- .../models/queryexpr/expr_test.go | 32 +++- .../dynamo-browse/models/queryexpr/fncall.go | 4 +- .../models/queryexpr/helpers_test.go | 1 + internal/dynamo-browse/models/queryexpr/is.go | 2 +- .../dynamo-browse/models/queryexpr/types.go | 14 ++ .../pasteboardprovider/nilprovider.go | 11 ++ .../providers/pasteboardprovider/providers.go | 55 +++++++ .../providers/settingstore/settingstore.go | 2 +- .../services/pasteboardprovider.go | 6 + .../services/scriptmanager/builtins.go | 19 ++- .../services/scriptmanager/modext.go | 33 ++-- .../services/scriptmanager/modos.go | 27 ++-- .../services/scriptmanager/modos_test.go | 17 +- .../services/scriptmanager/modsession.go | 35 ++-- .../services/scriptmanager/modsession_test.go | 19 +-- .../services/scriptmanager/modui.go | 24 ++- .../services/scriptmanager/opts.go | 6 +- .../services/scriptmanager/resultsetproxy.go | 114 +++++++++++++- .../scriptmanager/resultsetproxy_test.go | 128 ++++++++++++++- .../services/scriptmanager/service.go | 77 +++++---- .../services/scriptmanager/tableproxy.go | 28 +++- .../services/scriptmanager/typemapping.go | 2 +- .../dynamo-browse/services/tables/service.go | 2 + .../dynamo-browse/ui/keybindings/defaults.go | 1 + .../ui/keybindings/keybindings.go | 2 + internal/dynamo-browse/ui/model.go | 18 ++- .../ui/teamodels/statusandprompt/model.go | 47 +++++- .../ui/teamodels/statusandprompt/types.go | 4 + 54 files changed, 1227 insertions(+), 281 deletions(-) create mode 100644 internal/dynamo-browse/models/attrutils/truthy.go create mode 100644 internal/dynamo-browse/providers/pasteboardprovider/nilprovider.go create mode 100644 internal/dynamo-browse/providers/pasteboardprovider/providers.go create mode 100644 internal/dynamo-browse/services/pasteboardprovider.go diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index 42d0051..de99ca1 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -24,7 +24,7 @@ jobs: - name: Setup Go uses: actions/setup-go@v3 with: - go-version: 1.18 + go-version: 1.21 - name: Configure run: | git config --global url."https://${{ secrets.GO_MODULES_TOKEN }}:x-oauth-basic@github.com/lmika".insteadOf "https://github.com/lmika" diff --git a/.github/workflows/release.yaml b/.github/workflows/release.yaml index 3f86c5a..b880db3 100644 --- a/.github/workflows/release.yaml +++ b/.github/workflows/release.yaml @@ -20,7 +20,7 @@ jobs: - name: Setup Go uses: actions/setup-go@v3 with: - go-version: 1.18 + go-version: 1.21 - name: Configure run: | git config --global url."https://${{ secrets.GO_MODULES_TOKEN }}:x-oauth-basic@github.com/lmika".insteadOf "https://github.com/lmika" diff --git a/cmd/dynamo-browse/main.go b/cmd/dynamo-browse/main.go index 170a2dc..c059b3a 100644 --- a/cmd/dynamo-browse/main.go +++ b/cmd/dynamo-browse/main.go @@ -12,8 +12,10 @@ import ( "github.com/lmika/dynamo-browse/internal/common/ui/osstyle" "github.com/lmika/dynamo-browse/internal/common/workspaces" "github.com/lmika/dynamo-browse/internal/dynamo-browse/controllers" + "github.com/lmika/dynamo-browse/internal/dynamo-browse/models/queryexpr" "github.com/lmika/dynamo-browse/internal/dynamo-browse/providers/dynamo" "github.com/lmika/dynamo-browse/internal/dynamo-browse/providers/inputhistorystore" + "github.com/lmika/dynamo-browse/internal/dynamo-browse/providers/pasteboardprovider" "github.com/lmika/dynamo-browse/internal/dynamo-browse/providers/settingstore" "github.com/lmika/dynamo-browse/internal/dynamo-browse/providers/workspacestore" "github.com/lmika/dynamo-browse/internal/dynamo-browse/services/inputhistory" @@ -40,6 +42,7 @@ func main() { var flagRO = flag.Bool("ro", false, "enable readonly mode") var flagDefaultLimit = flag.Int("default-limit", 0, "default limit for queries and scans") var flagWorkspace = flag.String("w", "", "workspace file") + var flagQuery = flag.String("q", "", "run query") flag.Parse() ctx := context.Background() @@ -84,6 +87,7 @@ func main() { resultSetSnapshotStore := workspacestore.NewResultSetSnapshotStore(ws) settingStore := settingstore.New(ws) inputHistoryStore := inputhistorystore.NewInputHistoryStore(ws) + pasteboardProvider := pasteboardprovider.New() if *flagRO { if err := settingStore.SetReadOnly(*flagRO); err != nil { @@ -105,19 +109,57 @@ func main() { state := controllers.NewState() jobsController := controllers.NewJobsController(jobsService, eventBus, false) - tableReadController := controllers.NewTableReadController(state, tableService, workspaceService, itemRendererService, jobsController, inputHistoryService, eventBus, *flagTable) + tableReadController := controllers.NewTableReadController( + state, + tableService, + workspaceService, + itemRendererService, + jobsController, + inputHistoryService, + eventBus, + pasteboardProvider, + *flagTable, + ) tableWriteController := controllers.NewTableWriteController(state, tableService, jobsController, tableReadController, settingStore) columnsController := controllers.NewColumnsController(eventBus) - exportController := controllers.NewExportController(state, tableService, jobsController, columnsController) + exportController := controllers.NewExportController(state, tableService, jobsController, columnsController, pasteboardProvider) settingsController := controllers.NewSettingsController(settingStore, eventBus) keyBindings := keybindings.Default() scriptController := controllers.NewScriptController(scriptManagerService, tableReadController, settingsController, eventBus) + if *flagQuery != "" { + if *flagTable == "" { + cli.Fatalf("-t will need to be set for -q") + } + + ctx := context.Background() + + query, err := queryexpr.Parse(*flagQuery) + if err != nil { + cli.Fatalf("query: %v", err) + } + + ti, err := tableService.Describe(ctx, *flagTable) + if err != nil { + cli.Fatalf("cannot describe table: %v", err) + } + + rs, err := tableService.ScanOrQuery(ctx, ti, query, nil) + if err != nil { + cli.Fatalf("cannot execute query: %v", err) + } + if err := exportController.ExportToWriter(os.Stdout, rs); err != nil { + cli.Fatalf("cannot export results of query: %v", err) + } + return + } + keyBindingService := keybindings_service.NewService(keyBindings) keyBindingController := controllers.NewKeyBindingController(keyBindingService, scriptController) commandController := commandctrl.NewCommandController(inputHistoryService) commandController.AddCommandLookupExtension(scriptController) + commandController.SetCommandCompletionProvider(columnsController) model := ui.NewModel( tableReadController, @@ -131,6 +173,7 @@ func main() { scriptController, eventBus, keyBindingController, + pasteboardProvider, keyBindings, ) diff --git a/go.mod b/go.mod index a165db4..728a385 100644 --- a/go.mod +++ b/go.mod @@ -1,17 +1,19 @@ module github.com/lmika/dynamo-browse -go 1.18 +go 1.21 + +toolchain go1.21.1 require ( github.com/alecthomas/participle/v2 v2.0.0-beta.5 github.com/asdine/storm v2.1.2+incompatible - github.com/aws/aws-sdk-go-v2 v1.17.4 - github.com/aws/aws-sdk-go-v2/config v1.13.1 - github.com/aws/aws-sdk-go-v2/credentials v1.8.0 + github.com/aws/aws-sdk-go-v2 v1.18.1 + github.com/aws/aws-sdk-go-v2/config v1.18.27 + github.com/aws/aws-sdk-go-v2/credentials v1.13.26 github.com/aws/aws-sdk-go-v2/feature/dynamodb/attributevalue v1.10.12 github.com/aws/aws-sdk-go-v2/feature/dynamodb/expression v1.4.39 - github.com/aws/aws-sdk-go-v2/service/dynamodb v1.18.3 - github.com/aws/aws-sdk-go-v2/service/sqs v1.16.0 + github.com/aws/aws-sdk-go-v2/service/dynamodb v1.19.11 + github.com/aws/aws-sdk-go-v2/service/sqs v1.23.2 github.com/aws/aws-sdk-go-v2/service/ssm v1.24.0 github.com/brianvoe/gofakeit/v6 v6.15.0 github.com/calyptia/go-bubble-table v0.2.1 @@ -27,7 +29,7 @@ require ( github.com/muesli/ansi v0.0.0-20211031195517-c9f0611b6c70 github.com/muesli/reflow v0.3.0 github.com/pkg/errors v0.9.1 - github.com/stretchr/testify v1.8.1 + github.com/stretchr/testify v1.8.4 golang.design/x/clipboard v0.6.2 golang.org/x/exp v0.0.0-20230108222341-4b8118a2686a ) @@ -35,38 +37,68 @@ require ( require ( github.com/DataDog/zstd v1.5.2 // indirect github.com/Sereal/Sereal v0.0.0-20220220040404-e0d1e550e879 // indirect + github.com/anthonynsimon/bild v0.13.0 // indirect github.com/atotto/clipboard v0.1.4 // indirect - github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.10.0 // indirect - github.com/aws/aws-sdk-go-v2/internal/configsources v1.1.28 // indirect - github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.4.22 // indirect - github.com/aws/aws-sdk-go-v2/internal/ini v1.3.5 // indirect + github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.4.10 // indirect + github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.13.4 // indirect + github.com/aws/aws-sdk-go-v2/internal/configsources v1.1.34 // indirect + github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.4.28 // indirect + github.com/aws/aws-sdk-go-v2/internal/ini v1.3.35 // indirect + github.com/aws/aws-sdk-go-v2/internal/v4a v1.0.26 // indirect + github.com/aws/aws-sdk-go-v2/service/cloudformation v1.30.0 // indirect + github.com/aws/aws-sdk-go-v2/service/cloudfront v1.26.8 // indirect + github.com/aws/aws-sdk-go-v2/service/cloudtrail v1.27.1 // indirect + github.com/aws/aws-sdk-go-v2/service/cloudwatch v1.26.2 // indirect github.com/aws/aws-sdk-go-v2/service/dynamodbstreams v1.14.3 // indirect + github.com/aws/aws-sdk-go-v2/service/ebs v1.16.14 // indirect + github.com/aws/aws-sdk-go-v2/service/ec2 v1.102.0 // indirect + github.com/aws/aws-sdk-go-v2/service/ecr v1.18.13 // indirect + github.com/aws/aws-sdk-go-v2/service/ecs v1.27.4 // indirect + github.com/aws/aws-sdk-go-v2/service/eks v1.27.14 // indirect + github.com/aws/aws-sdk-go-v2/service/elasticache v1.27.2 // indirect + github.com/aws/aws-sdk-go-v2/service/elasticsearchservice v1.19.2 // indirect + github.com/aws/aws-sdk-go-v2/service/glue v1.52.0 // indirect + github.com/aws/aws-sdk-go-v2/service/iam v1.21.0 // indirect github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.9.11 // indirect - github.com/aws/aws-sdk-go-v2/service/internal/endpoint-discovery v1.7.22 // indirect - github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.7.0 // indirect - github.com/aws/aws-sdk-go-v2/service/sso v1.9.0 // indirect - github.com/aws/aws-sdk-go-v2/service/sts v1.14.0 // indirect + github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.1.29 // indirect + github.com/aws/aws-sdk-go-v2/service/internal/endpoint-discovery v1.7.28 // indirect + github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.9.28 // indirect + github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.14.3 // indirect + github.com/aws/aws-sdk-go-v2/service/kinesis v1.17.14 // indirect + github.com/aws/aws-sdk-go-v2/service/kms v1.22.2 // indirect + github.com/aws/aws-sdk-go-v2/service/lambda v1.37.0 // indirect + github.com/aws/aws-sdk-go-v2/service/rds v1.46.0 // indirect + github.com/aws/aws-sdk-go-v2/service/redshift v1.28.0 // indirect + github.com/aws/aws-sdk-go-v2/service/route53 v1.28.3 // indirect + github.com/aws/aws-sdk-go-v2/service/s3 v1.36.0 // indirect + github.com/aws/aws-sdk-go-v2/service/sfn v1.18.0 // indirect + github.com/aws/aws-sdk-go-v2/service/sns v1.20.13 // indirect + github.com/aws/aws-sdk-go-v2/service/sso v1.12.12 // indirect + github.com/aws/aws-sdk-go-v2/service/ssooidc v1.14.12 // indirect + github.com/aws/aws-sdk-go-v2/service/sts v1.19.2 // indirect + github.com/aws/aws-sdk-go-v2/service/wafv2 v1.35.1 // indirect github.com/aws/smithy-go v1.13.5 // indirect github.com/aymanbagabas/go-osc52 v1.0.3 // indirect github.com/containerd/console v1.0.3 // indirect github.com/davecgh/go-spew v1.1.1 // indirect - github.com/gofrs/uuid v4.3.1+incompatible // indirect + github.com/gofrs/uuid v4.4.0+incompatible // indirect github.com/golang/protobuf v1.5.2 // indirect github.com/golang/snappy v0.0.4 // indirect github.com/hashicorp/errwrap v1.0.0 // indirect github.com/hashicorp/go-multierror v1.1.1 // indirect github.com/jackc/pgpassfile v1.0.0 // indirect - github.com/jackc/pgservicefile v0.0.0-20200714003250-2b9c44734f2b // indirect - github.com/jackc/pgx/v5 v5.0.4 // indirect + github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a // indirect + github.com/jackc/pgx/v5 v5.4.1 // indirect github.com/jmespath/go-jmespath v0.4.0 // indirect github.com/juju/ansiterm v0.0.0-20210929141451-8b71cc96ebdc // indirect github.com/lucasb-eyer/go-colorful v1.2.0 // indirect github.com/lunixbochs/vtclean v1.0.0 // indirect - github.com/mattn/go-isatty v0.0.16 // indirect + github.com/mattn/go-isatty v0.0.17 // indirect github.com/mattn/go-localereader v0.0.1 // indirect github.com/muesli/cancelreader v0.2.2 // indirect github.com/muesli/termenv v0.13.0 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect + github.com/risor-io/risor v1.1.1 // indirect github.com/rivo/uniseg v0.4.2 // indirect github.com/sahilm/fuzzy v0.1.0 // indirect github.com/stretchr/objx v0.5.0 // indirect @@ -76,13 +108,13 @@ require ( github.com/vmihailenco/msgpack v4.0.4+incompatible // indirect github.com/wI2L/jsondiff v0.3.0 // indirect go.etcd.io/bbolt v1.3.6 // indirect - golang.org/x/crypto v0.0.0-20220829220503-c86fa9a7ed90 // indirect + golang.org/x/crypto v0.9.0 // indirect golang.org/x/exp/shiny v0.0.0-20230213192124-5e25df0256eb // indirect - golang.org/x/image v0.0.0-20211028202545-6944b10bf410 // indirect + golang.org/x/image v0.5.0 // indirect golang.org/x/mobile v0.0.0-20210716004757-34ab1303b554 // indirect - golang.org/x/sys v0.1.0 // indirect - golang.org/x/term v0.0.0-20220919170432-7a66f970e087 // indirect - golang.org/x/text v0.3.8 // indirect + golang.org/x/sys v0.8.0 // indirect + golang.org/x/term v0.8.0 // indirect + golang.org/x/text v0.9.0 // indirect google.golang.org/appengine v1.6.7 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect ) diff --git a/go.sum b/go.sum index ce47c1b..b97ea59 100644 --- a/go.sum +++ b/go.sum @@ -1,3 +1,4 @@ +github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo= github.com/DataDog/zstd v1.5.2 h1:vUG4lAyuPCXO0TLbXvPv7EB7cNK1QV/luu55UHLrrn8= github.com/DataDog/zstd v1.5.2/go.mod h1:g4AWEaM3yOg3HYfnJ3YIawPnVdXJh9QME85blwSAmyw= @@ -7,6 +8,9 @@ github.com/alecthomas/assert/v2 v2.0.3 h1:WKqJODfOiQG0nEJKFKzDIG3E29CN2/4zR9XGJz github.com/alecthomas/participle/v2 v2.0.0-beta.5 h1:y6dsSYVb1G5eK6mgmy+BgI3Mw35a3WghArZ/Hbebrjo= github.com/alecthomas/participle/v2 v2.0.0-beta.5/go.mod h1:RC764t6n4L8D8ITAJv0qdokritYSNR3wV5cVwmIEaMM= github.com/alecthomas/repr v0.1.0 h1:ENn2e1+J3k09gyj2shc0dHr/yjaWSHRlrJ4DPMevDqE= +github.com/anthonynsimon/bild v0.13.0 h1:mN3tMaNds1wBWi1BrJq0ipDBhpkooYfu7ZFSMhXt1C8= +github.com/anthonynsimon/bild v0.13.0/go.mod h1:tpzzp0aYkAsMi1zmfhimaDyX1xjn2OUc1AJZK/TF0AE= +github.com/armon/consul-api v0.0.0-20180202201655-eb2c6b5be1b6/go.mod h1:grANhF5doyWs3UAsr3K4I6qtAmlQcZDesFNEHPZAzj8= github.com/asdine/storm v2.1.2+incompatible h1:dczuIkyqwY2LrtXPz8ixMrU/OFgZp71kbKTHGrXYt/Q= github.com/asdine/storm v2.1.2+incompatible/go.mod h1:RarYDc9hq1UPLImuiXK3BIWPJLdIygvV3PsInK0FbVQ= github.com/atotto/clipboard v0.1.4 h1:EH0zSVneZPSuFR11BlR9YppQTVDbh5+16AmcJi4g1z4= @@ -15,44 +19,126 @@ github.com/aws/aws-sdk-go-v2 v1.13.0/go.mod h1:L6+ZpqHaLbAaxsqV0L4cvxZY7QupWJB4f github.com/aws/aws-sdk-go-v2 v1.16.1/go.mod h1:ytwTPBG6fXTZLxxeeCCWj2/EMYp/xDUgX+OET6TLNNU= github.com/aws/aws-sdk-go-v2 v1.17.4 h1:wyC6p9Yfq6V2y98wfDsj6OnNQa4w2BLGCLIxzNhwOGY= github.com/aws/aws-sdk-go-v2 v1.17.4/go.mod h1:uzbQtefpm44goOPmdKyAlXSNcwlRgF3ePWVW6EtJvvw= +github.com/aws/aws-sdk-go-v2 v1.18.1 h1:+tefE750oAb7ZQGzla6bLkOwfcQCEtC5y2RqoqCeqKo= +github.com/aws/aws-sdk-go-v2 v1.18.1/go.mod h1:uzbQtefpm44goOPmdKyAlXSNcwlRgF3ePWVW6EtJvvw= +github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.4.10 h1:dK82zF6kkPeCo8J1e+tGx4JdvDIQzj7ygIoLg8WMuGs= +github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.4.10/go.mod h1:VeTZetY5KRJLuD/7fkQXMU6Mw7H5m/KP2J5Iy9osMno= github.com/aws/aws-sdk-go-v2/config v1.13.1 h1:yLv8bfNoT4r+UvUKQKqRtdnvuWGMK5a82l4ru9Jvnuo= github.com/aws/aws-sdk-go-v2/config v1.13.1/go.mod h1:Ba5Z4yL/UGbjQUzsiaN378YobhFo0MLfueXGiOsYtEs= +github.com/aws/aws-sdk-go-v2/config v1.18.27 h1:Az9uLwmssTE6OGTpsFqOnaGpLnKDqNYOJzWuC6UAYzA= +github.com/aws/aws-sdk-go-v2/config v1.18.27/go.mod h1:0My+YgmkGxeqjXZb5BYme5pc4drjTnM+x1GJ3zv42Nw= github.com/aws/aws-sdk-go-v2/credentials v1.8.0 h1:8Ow0WcyDesGNL0No11jcgb1JAtE+WtubqXjgxau+S0o= github.com/aws/aws-sdk-go-v2/credentials v1.8.0/go.mod h1:gnMo58Vwx3Mu7hj1wpcG8DI0s57c9o42UQ6wgTQT5to= +github.com/aws/aws-sdk-go-v2/credentials v1.13.26 h1:qmU+yhKmOCyujmuPY7tf5MxR/RKyZrOPO3V4DobiTUk= +github.com/aws/aws-sdk-go-v2/credentials v1.13.26/go.mod h1:GoXt2YC8jHUBbA4jr+W3JiemnIbkXOfxSXcisUsZ3os= github.com/aws/aws-sdk-go-v2/feature/dynamodb/attributevalue v1.10.12 h1:ama2cD4WaH6+8Gq/M/g+ZumPmmqCyanr+6Sm+iJVxfA= github.com/aws/aws-sdk-go-v2/feature/dynamodb/attributevalue v1.10.12/go.mod h1:tPnUO5mS3JThpwfq4Q8iPd745s7yh6fGPqDUEBw+Wv4= github.com/aws/aws-sdk-go-v2/feature/dynamodb/expression v1.4.39 h1:PhgfvgqwMFQKwOcxLV7V3lNDVnR3ZUWzoB6T9oCFpR4= github.com/aws/aws-sdk-go-v2/feature/dynamodb/expression v1.4.39/go.mod h1:/GkvC7uHpK50ilKkKx9I2gZiI/ieZbKjS2aah1rT9uE= github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.10.0 h1:NITDuUZO34mqtOwFWZiXo7yAHj7kf+XPE+EiKuCBNUI= github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.10.0/go.mod h1:I6/fHT/fH460v09eg2gVrd8B/IqskhNdpcLH0WNO3QI= +github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.13.4 h1:LxK/bitrAr4lnh9LnIS6i7zWbCOdMsfzKFBI6LUCS0I= +github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.13.4/go.mod h1:E1hLXN/BL2e6YizK1zFlYd8vsfi2GTjbjBazinMmeaM= github.com/aws/aws-sdk-go-v2/internal/configsources v1.1.4/go.mod h1:XHgQ7Hz2WY2GAn//UXHofLfPXWh+s62MbMOijrg12Lw= github.com/aws/aws-sdk-go-v2/internal/configsources v1.1.8/go.mod h1:LnTQMTqbKsbtt+UI5+wPsB7jedW+2ZgozoPG8k6cMxg= github.com/aws/aws-sdk-go-v2/internal/configsources v1.1.28 h1:r+XwaCLpIvCKjBIYy/HVZujQS9tsz5ohHG3ZIe0wKoE= github.com/aws/aws-sdk-go-v2/internal/configsources v1.1.28/go.mod h1:3lwChorpIM/BhImY/hy+Z6jekmN92cXGPI1QJasVPYY= +github.com/aws/aws-sdk-go-v2/internal/configsources v1.1.34 h1:A5UqQEmPaCFpedKouS4v+dHCTUo2sKqhoKO9U5kxyWo= +github.com/aws/aws-sdk-go-v2/internal/configsources v1.1.34/go.mod h1:wZpTEecJe0Btj3IYnDx/VlUzor9wm3fJHyvLpQF0VwY= github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.2.0/go.mod h1:BsCSJHx5DnDXIrOcqB8KN1/B+hXLG/bi4Y6Vjcx/x9E= github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.4.2/go.mod h1:1x4ZP3Z8odssdhuLI+/1Tqw6Pt/VAaP4Tr8EUxHvPXE= github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.4.22 h1:7AwGYXDdqRQYsluvKFmWoqpcOQJ4bH634SkYf3FNj/A= github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.4.22/go.mod h1:EqK7gVrIGAHyZItrD1D8B0ilgwMD1GiWAmbU4u/JHNk= +github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.4.28 h1:srIVS45eQuewqz6fKKu6ZGXaq6FuFg5NzgQBAM6g8Y4= +github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.4.28/go.mod h1:7VRpKQQedkfIEXb4k52I7swUnZP0wohVajJMRn3vsUw= github.com/aws/aws-sdk-go-v2/internal/ini v1.3.5 h1:ixotxbfTCFpqbuwFv/RcZwyzhkxPSYDYEMcj4niB5Uk= github.com/aws/aws-sdk-go-v2/internal/ini v1.3.5/go.mod h1:R3sWUqPcfXSiF/LSFJhjyJmpg9uV6yP2yv3YZZjldVI= +github.com/aws/aws-sdk-go-v2/internal/ini v1.3.35 h1:LWA+3kDM8ly001vJ1X1waCuLJdtTl48gwkPKWy9sosI= +github.com/aws/aws-sdk-go-v2/internal/ini v1.3.35/go.mod h1:0Eg1YjxE0Bhn56lx+SHJwCzhW+2JGtizsrx+lCqrfm0= +github.com/aws/aws-sdk-go-v2/internal/v4a v1.0.26 h1:wscW+pnn3J1OYnanMnza5ZVYXLX4cKk5rAvUAl4Qu+c= +github.com/aws/aws-sdk-go-v2/internal/v4a v1.0.26/go.mod h1:MtYiox5gvyB+OyP0Mr0Sm/yzbEAIPL9eijj/ouHAPw0= +github.com/aws/aws-sdk-go-v2/service/cloudformation v1.30.0 h1:XbDkc4FLeg1RfnqeblfbJvaEabqq9ByZl4zqyPFkfSc= +github.com/aws/aws-sdk-go-v2/service/cloudformation v1.30.0/go.mod h1:SwQFcCs9Rog8hSHm+81KBkAK+UKLXErA/1ChaEI8mLE= +github.com/aws/aws-sdk-go-v2/service/cloudfront v1.26.8 h1:loRDtQ0vT0+JCB0hQBCfv95tttEzJ1rqSaTDy5cpy0A= +github.com/aws/aws-sdk-go-v2/service/cloudfront v1.26.8/go.mod h1:YTd4wGn2beCF9wkSTpEcupk79zDFYJk2Ca76B8YyvJg= +github.com/aws/aws-sdk-go-v2/service/cloudtrail v1.27.1 h1:Qw1G/M7eanpm6s/URkG1UuRLKEnRnpUvkUb7NMVvWb8= +github.com/aws/aws-sdk-go-v2/service/cloudtrail v1.27.1/go.mod h1:oKRYqorIUkfAVmX03+lpv3tW5WelDpaliqzTwmCj/k8= +github.com/aws/aws-sdk-go-v2/service/cloudwatch v1.26.2 h1:PWGu2JhCb/XJlJ7SSFJq76pxk4xWsN76nZxh7TzMHx0= +github.com/aws/aws-sdk-go-v2/service/cloudwatch v1.26.2/go.mod h1:2KOZkkzMDZCo/aLzPhys06mHNkiU74u85aMJA3PLRvg= github.com/aws/aws-sdk-go-v2/service/dynamodb v1.18.3 h1:MxOpCZ+o9+AIeQHi2ocW7H4D7p0LhEkmetETVvDnkvg= github.com/aws/aws-sdk-go-v2/service/dynamodb v1.18.3/go.mod h1:nkpC9xkh+3vdxmhqN8Ac10pgV14DsJDLzUsV2CcS+44= +github.com/aws/aws-sdk-go-v2/service/dynamodb v1.19.11 h1:tLTGNAsazbfjfjW1k/i43kyCcyTTTTFaD93H7JbSbbs= +github.com/aws/aws-sdk-go-v2/service/dynamodb v1.19.11/go.mod h1:W1oiFegjVosgjIwb2Vv45jiCQT1ee8x85u8EyZRYLes= github.com/aws/aws-sdk-go-v2/service/dynamodbstreams v1.14.3 h1:B+bkmCnNJi194pu9aTtYUe8f4EPXafC+xfU+zciVxdg= github.com/aws/aws-sdk-go-v2/service/dynamodbstreams v1.14.3/go.mod h1:bRphLmXQD9Ux4jLcFEwyrWdmuPTj2Lh8VGl9wILuJII= +github.com/aws/aws-sdk-go-v2/service/ebs v1.16.14 h1:DosI4CvEUo6/V21pDspzYkOa2X3Zwy5XS/cbPFiqDv0= +github.com/aws/aws-sdk-go-v2/service/ebs v1.16.14/go.mod h1:yVTqVHjnrbAj6FvhTQfjNgwQbjPbDUUvA1x4IpXFmrE= +github.com/aws/aws-sdk-go-v2/service/ec2 v1.102.0 h1:P4dyjm49F2kKws0FpouBC6fjVImACXKt752+CWa01lM= +github.com/aws/aws-sdk-go-v2/service/ec2 v1.102.0/go.mod h1:tIctCeX9IbzsUTKHt53SVEcgyfxV2ElxJeEB+QUbc4M= +github.com/aws/aws-sdk-go-v2/service/ecr v1.18.13 h1:hF7MUVNjubetjggZDtn3AmqCJzD7EUi//tSdxMYPm7U= +github.com/aws/aws-sdk-go-v2/service/ecr v1.18.13/go.mod h1:XwEFO35g0uN/SftK0asWxh8Rk6DOx37R83TmWe2tzEE= +github.com/aws/aws-sdk-go-v2/service/ecs v1.27.4 h1:F1N0Eh5EGRRY9QpF+tMTkx8Wb59DkQWE91Xza/9dk1c= +github.com/aws/aws-sdk-go-v2/service/ecs v1.27.4/go.mod h1:0irnFofeEZwT7uTjSkNVcSQJbWRqZ9BRoxhKjt1BObM= +github.com/aws/aws-sdk-go-v2/service/eks v1.27.14 h1:47HQVuJXgwvuoc4AT3rVdm77H0qGFbFnsuE4PRT+xX0= +github.com/aws/aws-sdk-go-v2/service/eks v1.27.14/go.mod h1:QxuWcm9rlLkW3aEV8tiDzqZewnNSNUZfnqJvo1Nv9A0= +github.com/aws/aws-sdk-go-v2/service/elasticache v1.27.2 h1:IC9XLGcT3yEkziTlX7PX54km7cHJnltlV7Ppwq2+7ik= +github.com/aws/aws-sdk-go-v2/service/elasticache v1.27.2/go.mod h1:+oJhn/SIud310/2LLSVmlNZmExmlYPaGCLmUsnq5JZc= +github.com/aws/aws-sdk-go-v2/service/elasticsearchservice v1.19.2 h1:Zam6yofBgdtP13laNoeA+DA9wlKJNooU8p3CWw6xLaI= +github.com/aws/aws-sdk-go-v2/service/elasticsearchservice v1.19.2/go.mod h1:dehjpZ00q0RJcBUOUEysaj7zHK2rHSS4ePp89MsFiaI= +github.com/aws/aws-sdk-go-v2/service/glue v1.52.0 h1:ukSf8ZdoZ6AygsUWIjj177wLOXljxBspBaNMgvx6fRA= +github.com/aws/aws-sdk-go-v2/service/glue v1.52.0/go.mod h1:wMCE0B6l8eHb57l2DMYCGxt0rHIfcu3RvIY7SAfc+Fs= +github.com/aws/aws-sdk-go-v2/service/iam v1.21.0 h1:8hEpu60CWlrp7iEBUFRZhgPoX6+gadaGL1sD4LoRYS0= +github.com/aws/aws-sdk-go-v2/service/iam v1.21.0/go.mod h1:aQZ8BI+reeaY7RI/QQp7TKCSUHOesTdrzzylp3CW85c= github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.9.11 h1:y2+VQzC6Zh2ojtV2LoC0MNwHWc6qXv/j2vrQtlftkdA= github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.9.11/go.mod h1:iV4q2hsqtNECrfmlXyord9u4zyuFEJX9eLgLpSPzWA8= +github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.1.29 h1:zZSLP3v3riMOP14H7b4XP0uyfREDQOYv2cqIrvTXDNQ= +github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.1.29/go.mod h1:z7EjRjVwZ6pWcWdI2H64dKttvzaP99jRIj5hphW0M5U= github.com/aws/aws-sdk-go-v2/service/internal/endpoint-discovery v1.7.22 h1:6zEryIiJOSk5/OcVHzkPDwzNBQ2atYCTShyA7TqkuxA= github.com/aws/aws-sdk-go-v2/service/internal/endpoint-discovery v1.7.22/go.mod h1:moeOz5SKfY0p6pNIChdPIQdfaUfWI67+OVe0/r6+aGY= +github.com/aws/aws-sdk-go-v2/service/internal/endpoint-discovery v1.7.28 h1:/D994rtMQd1jQ2OY+7tvUlMlrv1L1c7Xtma/FhkbVtY= +github.com/aws/aws-sdk-go-v2/service/internal/endpoint-discovery v1.7.28/go.mod h1:3bJI2pLY3ilrqO5EclusI1GbjFJh1iXYrhOItf2sjKw= github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.7.0 h1:4QAOB3KrvI1ApJK14sliGr3Ie2pjyvNypn/lfzDHfUw= github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.7.0/go.mod h1:K/qPe6AP2TGYv4l6n7c88zh9jWBDf6nHhvg1fx/EWfU= +github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.9.28 h1:bkRyG4a929RCnpVSTvLM2j/T4ls015ZhhYApbmYs15s= +github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.9.28/go.mod h1:jj7znCIg05jXlaGBlFMGP8+7UN3VtCkRBG2spnmRQkU= +github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.14.3 h1:dBL3StFxHtpBzJJ/mNEsjXVgfO+7jR0dAIEwLqMapEA= +github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.14.3/go.mod h1:f1QyiAsvIv4B49DmCqrhlXqyaR+0IxMmyX+1P+AnzOM= +github.com/aws/aws-sdk-go-v2/service/kinesis v1.17.14 h1:oSw0SQN9cKeYvCUYfPul7bH11b8E9I9BnoVUme3iSaU= +github.com/aws/aws-sdk-go-v2/service/kinesis v1.17.14/go.mod h1:omXkSCk1T1difhE8wVaecXNeerY6jmpFFu49ngjEDQk= +github.com/aws/aws-sdk-go-v2/service/kms v1.22.2 h1:jwmtdM1/l1DRNy5jQrrYpsQm8zwetkgeqhAqefDr1yI= +github.com/aws/aws-sdk-go-v2/service/kms v1.22.2/go.mod h1:aNfh11Smy55o65PB3MyKbkM8BFyFUcZmj1k+4g8eNfg= +github.com/aws/aws-sdk-go-v2/service/lambda v1.37.0 h1:xzyM5ZR9kZW0/Bkw5EiihOy6B+BYclp5K+yb6OHjc7s= +github.com/aws/aws-sdk-go-v2/service/lambda v1.37.0/go.mod h1:Q8zQi5nZpjUF/H55dKEpKfEvFWJkgZzjjqvDb2AR5b4= +github.com/aws/aws-sdk-go-v2/service/rds v1.46.0 h1:uv2LAciZRd5lEXzJo2u92tdZh/JxcVL7YLC51D4NLG4= +github.com/aws/aws-sdk-go-v2/service/rds v1.46.0/go.mod h1:goBDR4OPrsnKpYyU0GHGcEnlTmL8O+eKGsWeyOAFJ5M= +github.com/aws/aws-sdk-go-v2/service/redshift v1.28.0 h1:tmhg03t7nNVSFqhxb8YpHqq8H1wwwrfEQv/rL7NkTAE= +github.com/aws/aws-sdk-go-v2/service/redshift v1.28.0/go.mod h1:x9am33DT5lVKUb0DH1UVbX+iFfpIqAKx6DAqB5Qu6jU= +github.com/aws/aws-sdk-go-v2/service/route53 v1.28.3 h1:nJbE4+tHd8xpM1RB1ZF0/xTJnFd/ATz42ZC35lwXx0w= +github.com/aws/aws-sdk-go-v2/service/route53 v1.28.3/go.mod h1:Cd4MnFoV+6fELBrgWXJ4Y09FrSkn/VjNPkOr1Yr1muU= +github.com/aws/aws-sdk-go-v2/service/s3 v1.36.0 h1:lEmQ1XSD9qLk+NZXbgvLJI/IiTz7OIR2TYUTFH25EI4= +github.com/aws/aws-sdk-go-v2/service/s3 v1.36.0/go.mod h1:aVbf0sko/TsLWHx30c/uVu7c62+0EAJ3vbxaJga0xCw= +github.com/aws/aws-sdk-go-v2/service/sfn v1.18.0 h1:1AIwJvCywFO4nGtHj7ZtKb9mhLpB5hToyjtE5OO6o/I= +github.com/aws/aws-sdk-go-v2/service/sfn v1.18.0/go.mod h1:41VgIwo6R/QE8DnFZ4RrP+f2w9xTzB77h3NRu/BzXyE= +github.com/aws/aws-sdk-go-v2/service/sns v1.20.13 h1:+ADGcDhddHTKyu6Qp3oZKootryteS7D3ODo2ZPDBgjQ= +github.com/aws/aws-sdk-go-v2/service/sns v1.20.13/go.mod h1:rWrvp9i8y/lX94lS7Kn/0iu9RY6vXzeKRqS/knVX8/c= github.com/aws/aws-sdk-go-v2/service/sqs v1.16.0 h1:dzWS4r8E9bA0TesHM40FSAtedwpTVCuTsLI8EziSqyk= github.com/aws/aws-sdk-go-v2/service/sqs v1.16.0/go.mod h1:IBTQMG8mtyj37OWg7vIXcg714Ntcb/LlYou/rZpvV1k= +github.com/aws/aws-sdk-go-v2/service/sqs v1.23.2 h1:Y2vfLiY3HmaMisuwx6fS2kMRYbajRXXB+9vesGVPseY= +github.com/aws/aws-sdk-go-v2/service/sqs v1.23.2/go.mod h1:TaV67b6JMD1988x/uMDop/JnMFK6v5d4Ru+sDmFg+ww= github.com/aws/aws-sdk-go-v2/service/ssm v1.24.0 h1:p22U2yL/AeRToERGcZv1R26Yci5VQnWIrpzcZdG54cg= github.com/aws/aws-sdk-go-v2/service/ssm v1.24.0/go.mod h1:chcyLYBEVRac/7rWJsD6cUHUR2osROwavvNqCplfwog= github.com/aws/aws-sdk-go-v2/service/sso v1.9.0 h1:1qLJeQGBmNQW3mBNzK2CFmrQNmoXWrscPqsrAaU1aTA= github.com/aws/aws-sdk-go-v2/service/sso v1.9.0/go.mod h1:vCV4glupK3tR7pw7ks7Y4jYRL86VvxS+g5qk04YeWrU= +github.com/aws/aws-sdk-go-v2/service/sso v1.12.12 h1:nneMBM2p79PGWBQovYO/6Xnc2ryRMw3InnDJq1FHkSY= +github.com/aws/aws-sdk-go-v2/service/sso v1.12.12/go.mod h1:HuCOxYsF21eKrerARYO6HapNeh9GBNq7fius2AcwodY= +github.com/aws/aws-sdk-go-v2/service/ssooidc v1.14.12 h1:2qTR7IFk7/0IN/adSFhYu9Xthr0zVFTgBrmPldILn80= +github.com/aws/aws-sdk-go-v2/service/ssooidc v1.14.12/go.mod h1:E4VrHCPzmVB/KFXtqBGKb3c8zpbNBgKe3fisDNLAW5w= github.com/aws/aws-sdk-go-v2/service/sts v1.14.0 h1:ksiDXhvNYg0D2/UFkLejsaz3LqpW5yjNQ8Nx9Sn2c0E= github.com/aws/aws-sdk-go-v2/service/sts v1.14.0/go.mod h1:u0xMJKDvvfocRjiozsoZglVNXRG19043xzp3r2ivLIk= +github.com/aws/aws-sdk-go-v2/service/sts v1.19.2 h1:XFJ2Z6sNUUcAz9poj+245DMkrHE4h2j5I9/xD50RHfE= +github.com/aws/aws-sdk-go-v2/service/sts v1.19.2/go.mod h1:dp0yLPsLBOi++WTxzCjA/oZqi6NPIhoR+uF7GeMU9eg= +github.com/aws/aws-sdk-go-v2/service/wafv2 v1.35.1 h1:FtzLuTf9HPECIcKdBMtA16ZwZWOIj/r57Z3QuWuYfqc= +github.com/aws/aws-sdk-go-v2/service/wafv2 v1.35.1/go.mod h1:RBpb9oTsEgAUfyaTAT2hFC83DxtLxj+SQpcbhaXiHnU= github.com/aws/smithy-go v1.10.0/go.mod h1:SObp3lf9smib00L/v3U2eAKG8FyQ7iLrJnQiAmR5n+E= github.com/aws/smithy-go v1.11.2/go.mod h1:3xHYmszWVx2c0kIwQeEVf9uSm4fYZt67FBJnwub1bgM= github.com/aws/smithy-go v1.13.5 h1:hgz0X/DX0dGqTYpGALqXJoRKRj5oQ7150i5FdTePzO8= @@ -76,11 +162,18 @@ github.com/cloudcmds/tamarin v1.0.0 h1:PhrJ74FCUJo24/nIPXnQe9E3WVEIYo4aG58pICOMD github.com/cloudcmds/tamarin v1.0.0/go.mod h1:U1aHBoAFtJbI9jzgaj8TUo9C6vfzUKzn1OhWKIdigVM= github.com/containerd/console v1.0.3 h1:lIr7SlA5PxZyMV30bDW0MGbiOPXwc63yRuCP0ARubLw= github.com/containerd/console v1.0.3/go.mod h1:7LqA/THxQ86k76b8c/EMSiaJ3h1eZkMkXar0TQ1gf3U= +github.com/coreos/etcd v3.3.10+incompatible/go.mod h1:uF7uidLiAD3TWHmW31ZFd/JWoc32PjwdhPthX9715RE= +github.com/coreos/go-etcd v2.0.0+incompatible/go.mod h1:Jez6KQU2B/sWsbdaef3ED8NzMklzPG4d5KIOhIy30Tk= +github.com/coreos/go-semver v0.2.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk= +github.com/cpuguy83/go-md2man v1.0.10/go.mod h1:SmD6nW6nTyfqj6ABTjUi3V3JVMnlJmwcJI5acqYI6dE= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= github.com/gofrs/uuid v4.3.1+incompatible h1:0/KbAdpx3UXAx1kEOWHJeOkpbgRFGHVgv+CFIY7dBJI= github.com/gofrs/uuid v4.3.1+incompatible/go.mod h1:b2aQJv3Z4Fp6yNu3cdSllBxTCLRxnplIgP/c0N/04lM= +github.com/gofrs/uuid v4.4.0+incompatible h1:3qXRTX8/NbyulANqlc0lchS1gqAVxRgsuW1YrTJupqA= +github.com/gofrs/uuid v4.4.0+incompatible/go.mod h1:b2aQJv3Z4Fp6yNu3cdSllBxTCLRxnplIgP/c0N/04lM= github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk= github.com/golang/protobuf v1.5.2 h1:ROPKBNFfQgOUMifHyP+KYbvpjbdoFNs+aK7DXlji0Tw= @@ -97,13 +190,19 @@ github.com/hashicorp/errwrap v1.0.0 h1:hLrqtEDnRye3+sgx6z4qVLNuviH3MR5aQ0ykNJa/U github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= github.com/hashicorp/go-multierror v1.1.1 h1:H5DkEtf6CXdFp0N0Em5UCwQpXMWke8IA0+lD48awMYo= github.com/hashicorp/go-multierror v1.1.1/go.mod h1:iw975J/qwKPdAO1clOe2L8331t/9/fmwbPZ6JB6eMoM= +github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ= github.com/hexops/gotextdiff v1.0.3 h1:gitA9+qJrrTCsiCl7+kh75nPqQt1cx4ZkudSTLoUqJM= +github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8= github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsIM= github.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg= github.com/jackc/pgservicefile v0.0.0-20200714003250-2b9c44734f2b h1:C8S2+VttkHFdOOCXJe+YGfa4vHYwlt4Zx+IVXQ97jYg= github.com/jackc/pgservicefile v0.0.0-20200714003250-2b9c44734f2b/go.mod h1:vsD4gTJCa9TptPL8sPkXrLZ+hDuNrZCnj29CQpr4X1E= +github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a h1:bbPeKD0xmW/Y25WS6cokEszi5g+S0QxI/d45PkRi7Nk= +github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a/go.mod h1:5TJZWKEWniPve33vlWYSoGYefn3gLQRzjfDlhSJ9ZKM= github.com/jackc/pgx/v5 v5.0.4 h1:r5O6y84qHX/z/HZV40JBdx2obsHz7/uRj5b+CcYEdeY= github.com/jackc/pgx/v5 v5.0.4/go.mod h1:U0ynklHtgg43fue9Ly30w3OCSTDPlXjig9ghrNGaguQ= +github.com/jackc/pgx/v5 v5.4.1 h1:oKfB/FhuVtit1bBM3zNRRsZ925ZkMN3HXL+LgLUM9lE= +github.com/jackc/pgx/v5 v5.4.1/go.mod h1:q6iHT8uDNXWiFNOlRqJzBTaSH3+2xCXkokxHZC5qWFY= github.com/jmespath/go-jmespath v0.4.0 h1:BEgLn5cpjn8UN1mAw4NjwDrS35OdebyEtFe+9YPoQUg= github.com/jmespath/go-jmespath v0.4.0/go.mod h1:T8mJZnbsbmF+m6zOOFylbeCJqk5+pHWvzYPziyZiYoo= github.com/jmespath/go-jmespath/internal/testify v1.5.1 h1:shLQSRRSCCPj3f2gpwzGwWFoC7ycTf1rcQZHOlsJ6N8= @@ -112,6 +211,7 @@ github.com/juju/ansiterm v0.0.0-20210929141451-8b71cc96ebdc h1:ZQrgZFsLzkw7o3CoD github.com/juju/ansiterm v0.0.0-20210929141451-8b71cc96ebdc/go.mod h1:PyXUpnI3olx3bsPcHt98FGPX/KCFZ1Fi+hw1XLI6384= github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= github.com/kr/pretty v0.3.0 h1:WgNl7dwNpEZ6jJ9k1snq4pZsg7DOEN8hP9Xw0Tsjwk0= +github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= @@ -129,11 +229,14 @@ github.com/lucasb-eyer/go-colorful v1.2.0 h1:1nnpGOrhyZZuNyfu1QjKiUICQ74+3FNCN69 github.com/lucasb-eyer/go-colorful v1.2.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0= github.com/lunixbochs/vtclean v1.0.0 h1:xu2sLAri4lGiovBDQKxl5mrXyESr3gUr5m5SM5+LVb8= github.com/lunixbochs/vtclean v1.0.0/go.mod h1:pHhQNgMf3btfWnGBVipUOjRYhoOsdGqdm/+2c2E2WMI= +github.com/magiconair/properties v1.8.0/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ= github.com/mattn/go-colorable v0.1.10/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc= github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU= github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94= github.com/mattn/go-isatty v0.0.16 h1:bq3VjFmv/sOjHtdEhmkEV4x1AJtvUvOJ2PFAZ5+peKQ= github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= +github.com/mattn/go-isatty v0.0.17 h1:BTarxUcIeDqL27Mc+vyvdWYSL28zpIhv3RoTdsLMPng= +github.com/mattn/go-isatty v0.0.17/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= github.com/mattn/go-localereader v0.0.1 h1:ygSAOl7ZXTx4RdPYinUpg6W99U8jWvWi9Ye2JC/oIi4= github.com/mattn/go-localereader v0.0.1/go.mod h1:8fBrzywKY7BI3czFoHkuzRoWE9C+EiG4R1k4Cjx5p88= github.com/mattn/go-runewidth v0.0.10/go.mod h1:RAqKPSqVFrSLVXbA8x7dzmKdmGzieGRCM46jaSJTDAk= @@ -141,6 +244,8 @@ github.com/mattn/go-runewidth v0.0.12/go.mod h1:RAqKPSqVFrSLVXbA8x7dzmKdmGzieGRC github.com/mattn/go-runewidth v0.0.13/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= github.com/mattn/go-runewidth v0.0.14 h1:+xnbZSEeDbOIg5/mE6JF0w6n9duR1l3/WmbinWVwUuU= github.com/mattn/go-runewidth v0.0.14/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= +github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0= +github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y= github.com/muesli/ansi v0.0.0-20211018074035-2e021307bc4b/go.mod h1:fQuZ0gauxyBcmsdE3ZT4NasjaRdxmbCS0jRHsrWu3Ho= github.com/muesli/ansi v0.0.0-20211031195517-c9f0611b6c70 h1:kMlmsLSbjkikxQJ1IPwaM+7LJ9ltFu/fi8CRzvSnQmA= github.com/muesli/ansi v0.0.0-20211031195517-c9f0611b6c70/go.mod h1:fQuZ0gauxyBcmsdE3ZT4NasjaRdxmbCS0jRHsrWu3Ho= @@ -154,27 +259,45 @@ github.com/muesli/termenv v0.11.1-0.20220204035834-5ac8409525e0/go.mod h1:Bd5NYQ github.com/muesli/termenv v0.11.1-0.20220212125758-44cd13922739/go.mod h1:Bd5NYQ7pd+SrtBSrSNoBBmXlcY8+Xj4BMJgh8qcZrvs= github.com/muesli/termenv v0.13.0 h1:wK20DRpJdDX8b7Ek2QfhvqhRQFZ237RGRO0RQ/Iqdy0= github.com/muesli/termenv v0.13.0/go.mod h1:sP1+uffeLaEYpyOTb8pLCUctGcGLnoFjSn4YJK5e2bc= +github.com/pelletier/go-toml v1.2.0/go.mod h1:5z9KED0ma1S8pY6P1sdut58dfprrGBbd/94hg7ilaic= github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/risor-io/risor v0.8.0 h1:G0fpHMGztvocKVu8egkKNbvLy4Rsjkuk+0zReu2JSn8= +github.com/risor-io/risor v0.8.0/go.mod h1:lvatEIYxs6HL+X/Bm0R+Mq4Z9a5Y036mniw6DwUnqs0= +github.com/risor-io/risor v1.1.1 h1:J8rIZX/0HXhg/t2+QygksvP65XCWhg5QxRZrwZabhxE= +github.com/risor-io/risor v1.1.1/go.mod h1:0UMw7ZMbUKSPFgQyuHCFe7UuBUewBKX4K3By4ba1CBA= github.com/rivo/uniseg v0.1.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= github.com/rivo/uniseg v0.4.2 h1:YwD0ulJSJytLpiaWua0sBDusfsCZohxjxzVTYjwxfV8= github.com/rivo/uniseg v0.4.2/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88= github.com/rogpeppe/go-internal v1.9.0 h1:73kH8U+JUqXU8lRuOHeVHaa/SZPifC7BkcraZVejAe8= +github.com/russross/blackfriday v1.5.2/go.mod h1:JO/DiYxRf+HjHt06OyowR9PTA263kcR/rfWxYHBV53g= github.com/sahilm/fuzzy v0.1.0 h1:FzWGaw2Opqyu+794ZQ9SYifWv2EIXpwP4q8dY1kDAwI= github.com/sahilm/fuzzy v0.1.0/go.mod h1:VFvziUEIMCrT6A6tw2RFIXPXXmzXbOsSHF0DOI8ZK9Y= +github.com/spf13/afero v1.1.2/go.mod h1:j4pytiNVoe2o6bmDsKpLACNPDBIoEAkihy7loJ1B0CQ= +github.com/spf13/cast v1.3.0/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE= +github.com/spf13/cobra v0.0.5/go.mod h1:3K3wKZymM7VvHMDS9+Akkh4K60UwM26emMESw8tLCHU= +github.com/spf13/jwalterweatherman v1.0.0/go.mod h1:cQK4TGJAtQXfYWX+Ddv3mKDzgVb68N+wFjFa4jdeBTo= +github.com/spf13/pflag v1.0.3/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4= +github.com/spf13/viper v1.3.2/go.mod h1:ZiWeW+zYFKm7srdB9IoDzzZXaJaI5eL9QjNiN/DMA2s= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= github.com/stretchr/objx v0.5.0 h1:1zr/of2m5FGMsad5YfcqgdqdWrIhu+EBEJRhR1U7z/c= github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= +github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA= +github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= github.com/stretchr/testify v1.8.1 h1:w7B6lhMri9wdJUVmEZPGGhZzrYTPvgJArz7wNPgYKsk= github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= +github.com/stretchr/testify v1.8.3 h1:RP3t2pwF7cMEbC1dqtB6poj3niw/9gnV4Cjg5oW5gtY= +github.com/stretchr/testify v1.8.3/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= +github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk= +github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= github.com/tidwall/gjson v1.14.3 h1:9jvXn7olKEHU1S9vwoMGliaT8jq1vJ7IH/n9zD9Dnlw= github.com/tidwall/gjson v1.14.3/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk= github.com/tidwall/match v1.1.1 h1:+Ho715JplO36QYgwN9PGYNhgZvoUSc9X2c80KVTi+GA= @@ -182,42 +305,58 @@ github.com/tidwall/match v1.1.1/go.mod h1:eRSPERbgtNPcGhD8UCthc6PmLEQXEWd3PRB5JT github.com/tidwall/pretty v1.2.0/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU= github.com/tidwall/pretty v1.2.1 h1:qjsOFOWWQl+N3RsoF5/ssm1pHmJJwhjlSbZ51I6wMl4= github.com/tidwall/pretty v1.2.1/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU= +github.com/ugorji/go/codec v0.0.0-20181204163529-d75b2dcb6bc8/go.mod h1:VFNgLljTbGfSG7qAOspJ7OScBnGdDN/yBr0sguwnwf0= github.com/vmihailenco/msgpack v4.0.4+incompatible h1:dSLoQfGFAo3F6OoNhwUmLwVgaUXK79GlxNBwueZn0xI= github.com/vmihailenco/msgpack v4.0.4+incompatible/go.mod h1:fy3FlTQTDXWkZ7Bh6AcGMlsjHatGryHQYUTf1ShIgkk= github.com/wI2L/jsondiff v0.3.0 h1:iTzQ9u/d86GE9RsBzVHX88f2EA1vQUboHwLhSQFc1s4= github.com/wI2L/jsondiff v0.3.0/go.mod h1:y1IMzNNjlSsk3IUoJdRJO7VRBtzMvRgyo4Vu0LdHpTc= +github.com/xordataexchange/crypt v0.0.3-0.20170626215501-b2862e3d0a77/go.mod h1:aYKd//L2LvnjZzWKhF00oedf4jCCReLcmhLdhm1A27Q= github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k= +github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= go.etcd.io/bbolt v1.3.6 h1:/ecaJf0sk1l4l6V4awd65v2C3ILy7MSj+s/x1ADCIMU= go.etcd.io/bbolt v1.3.6/go.mod h1:qXsaaIqmgQH0T+OPdb99Bf+PKfBBQVAdyD6TY9G8XM4= golang.design/x/clipboard v0.6.2 h1:a3Np4qfKnLWwfFJQhUWU3IDeRfmVuqWl+QPtP4CSYGw= golang.design/x/clipboard v0.6.2/go.mod h1:kqBSweBP0/im4SZGGjLrppH0D400Hnfo5WbFKSNK8N4= +golang.org/x/crypto v0.0.0-20181203042331-505ab145d0a9/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= golang.org/x/crypto v0.0.0-20220829220503-c86fa9a7ed90 h1:Y/gsMcFOcR+6S6f3YeMKl5g+dZMEWqcz5Czj/GWYbkM= golang.org/x/crypto v0.0.0-20220829220503-c86fa9a7ed90/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= +golang.org/x/crypto v0.9.0 h1:LF6fAI+IutBocDJ2OT0Q1g8plpYljMZ4+lty+dsqw3g= +golang.org/x/crypto v0.9.0/go.mod h1:yrmDGqONDYtNj3tH8X9dzUun2m2lzPa9ngI6/RUPGR0= golang.org/x/exp v0.0.0-20190731235908-ec7cb31e5a56/go.mod h1:JhuoJpWY28nO4Vef9tZUw9qufEGTyX1+7lmHxV5q5G4= golang.org/x/exp v0.0.0-20230108222341-4b8118a2686a h1:tlXy25amD5A7gOfbXdqCGN5k8ESEed/Ee1E5RcrYnqU= golang.org/x/exp v0.0.0-20230108222341-4b8118a2686a/go.mod h1:CxIveKay+FTh1D0yPZemJVgC/95VzuuOLq5Qi4xnoYc= golang.org/x/exp/shiny v0.0.0-20230213192124-5e25df0256eb h1:gdeQX7xJSkTNF+Sw7++XNIOo4pGL0CjQv3N2Vm1Erxk= golang.org/x/exp/shiny v0.0.0-20230213192124-5e25df0256eb/go.mod h1:UH99kUObWAZkDnWqppdQe5ZhPYESUw8I0zVV1uWBR+0= golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js= +golang.org/x/image v0.0.0-20190703141733-d6a02ce849c9/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= golang.org/x/image v0.0.0-20211028202545-6944b10bf410 h1:hTftEOvwiOq2+O8k2D5/Q7COC7k5Qcrgc2TFURJYnvQ= golang.org/x/image v0.0.0-20211028202545-6944b10bf410/go.mod h1:023OzeP/+EPmXeapQh35lcL3II3LrY8Ic+EFFKVhULM= +golang.org/x/image v0.5.0 h1:5JMiNunQeQw++mMOz48/ISeNu3Iweh/JaZU8ZLqHRrI= +golang.org/x/image v0.5.0/go.mod h1:FVC7BI/5Ym8R25iw5OLsgshdUBbT1h5jZTpA+mvAdZ4= golang.org/x/mobile v0.0.0-20190312151609-d3739f865fa6/go.mod h1:z+o9i4GpDbdi3rU15maQ/Ox0txvL9dWGYEHz965HBQE= golang.org/x/mobile v0.0.0-20210716004757-34ab1303b554 h1:3In5TnfvnuXTF/uflgpYxSCEGP2NdYT37KsPh3VjZYU= golang.org/x/mobile v0.0.0-20210716004757-34ab1303b554/go.mod h1:jFTmtFYCV0MFtXBU+J5V/+5AUeVS0ON/0WkE/KSrl6E= golang.org/x/mod v0.1.0/go.mod h1:0QHyrYULN0/3qlju5TqG8bIK38QM8yzMo5ekMj3DlcY= golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks= golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM= golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2 h1:CIJ76btIcR3eFI5EgSo6k1qKw9KJexJuRLI9G7Hp5wE= +golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= +golang.org/x/net v0.10.0 h1:X2//UzNDwYmtCLn7To6G58Wr6f5ahEAQgKNzv9Y951M= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sys v0.0.0-20181205085412-a5c9d58dba9a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= @@ -231,13 +370,19 @@ golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBc golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220204135822-1c1b9b1eba6a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220209214540-3681064d5158/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.1.0 h1:kunALQeHf1/185U1i0GOB/fy1IPRDDpuoOOqRReG57U= golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.8.0 h1:EBmGv8NaZBZTWvrbjNoL6HVt+IVy3QDQpJs7VRIw3tU= +golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= golang.org/x/term v0.0.0-20220919170432-7a66f970e087 h1:tPwmk4vmvVCMdr98VgL4JH+qZxPL8fqlUOHnyOM8N3w= golang.org/x/term v0.0.0-20220919170432-7a66f970e087/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= +golang.org/x/term v0.8.0 h1:n5xxQn2i3PC0yLAbjTpNT85q/Kgzcr2gIoX9OrJUols= +golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= @@ -245,10 +390,14 @@ golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= golang.org/x/text v0.3.8 h1:nAL+RVCQ9uMn3vJZbV+MRnydTJFPf8qqY42YiA6MrqY= golang.org/x/text v0.3.8/go.mod h1:E6s5w1FMmriuDzIBO73fBruAKo1PCIq6d2Q6DHfQ8WQ= +golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= +golang.org/x/text v0.9.0 h1:2sjJmO8cDvYveuX97RDLsxlyUxLl+GHoLxBiRdHllBE= +golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20190312151545-0bb0c0a6e846/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.1.2/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= +golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= diff --git a/internal/common/ui/commandctrl/commandctrl.go b/internal/common/ui/commandctrl/commandctrl.go index b3e831f..c0d857f 100644 --- a/internal/common/ui/commandctrl/commandctrl.go +++ b/internal/common/ui/commandctrl/commandctrl.go @@ -18,9 +18,10 @@ import ( const commandsCategory = "commands" type CommandController struct { - historyProvider IterProvider - commandList *CommandList - lookupExtensions []CommandLookupExtension + historyProvider IterProvider + commandList *CommandList + lookupExtensions []CommandLookupExtension + completionProvider CommandCompletionProvider } func NewCommandController(historyProvider IterProvider) *CommandController { @@ -40,6 +41,10 @@ func (c *CommandController) AddCommandLookupExtension(ext CommandLookupExtension c.lookupExtensions = append(c.lookupExtensions, ext) } +func (c *CommandController) SetCommandCompletionProvider(provider CommandCompletionProvider) { + c.completionProvider = provider +} + func (c *CommandController) Prompt() tea.Msg { return events.PromptForInputMsg{ Prompt: ":", @@ -47,6 +52,24 @@ func (c *CommandController) Prompt() tea.Msg { OnDone: func(value string) tea.Msg { return c.Execute(value) }, + // TEMP + OnTabComplete: func(value string) (string, bool) { + if c.completionProvider == nil { + return "", false + } + + if strings.HasPrefix(value, "sa ") || strings.HasPrefix(value, "da ") { + tokens := shellwords.Split(strings.TrimSpace(value)) + lastToken := tokens[len(tokens)-1] + + options := c.completionProvider.AttributesWithPrefix(lastToken) + if len(options) == 1 { + return value[:len(value)-len(lastToken)] + options[0], true + } + } + return "", false + }, + // END TEMP } } diff --git a/internal/common/ui/commandctrl/types.go b/internal/common/ui/commandctrl/types.go index c8a9058..7861e09 100644 --- a/internal/common/ui/commandctrl/types.go +++ b/internal/common/ui/commandctrl/types.go @@ -19,3 +19,7 @@ type CommandList struct { type CommandLookupExtension interface { LookupCommand(name string) Command } + +type CommandCompletionProvider interface { + AttributesWithPrefix(prefix string) []string +} diff --git a/internal/common/ui/events/commands.go b/internal/common/ui/events/commands.go index 183c9c6..68fdd5d 100644 --- a/internal/common/ui/events/commands.go +++ b/internal/common/ui/events/commands.go @@ -54,3 +54,8 @@ type MessageWithMode interface { MessageWithStatus ModeMessage() string } + +type MessageWithRightMode interface { + MessageWithStatus + RightModeMessage() string +} diff --git a/internal/common/ui/events/errors.go b/internal/common/ui/events/errors.go index 08eda79..127c3b3 100644 --- a/internal/common/ui/events/errors.go +++ b/internal/common/ui/events/errors.go @@ -21,8 +21,9 @@ type ModeMessage string // PromptForInput indicates that the context is requesting a line of input type PromptForInputMsg struct { - Prompt string - History services.HistoryProvider - OnDone func(value string) tea.Msg - OnCancel func() tea.Msg + Prompt string + History services.HistoryProvider + OnDone func(value string) tea.Msg + OnCancel func() tea.Msg + OnTabComplete func(value string) (string, bool) } diff --git a/internal/dynamo-browse/controllers/columns.go b/internal/dynamo-browse/controllers/columns.go index b05f999..8080477 100644 --- a/internal/dynamo-browse/controllers/columns.go +++ b/internal/dynamo-browse/controllers/columns.go @@ -7,6 +7,7 @@ import ( "github.com/lmika/dynamo-browse/internal/dynamo-browse/models/columns" "github.com/lmika/dynamo-browse/internal/dynamo-browse/models/queryexpr" bus "github.com/lmika/events" + "strings" ) type ColumnsController struct { @@ -115,3 +116,13 @@ func (cc *ColumnsController) DeleteColumn(afterIndex int) tea.Msg { return ColumnsUpdated{} } + +func (c *ColumnsController) AttributesWithPrefix(prefix string) []string { + options := make([]string, 0) + for _, col := range c.resultSet.Columns() { + if strings.HasPrefix(col, prefix) { + options = append(options, col) + } + } + return options +} diff --git a/internal/dynamo-browse/controllers/events.go b/internal/dynamo-browse/controllers/events.go index b2ef4cc..3d0821f 100644 --- a/internal/dynamo-browse/controllers/events.go +++ b/internal/dynamo-browse/controllers/events.go @@ -4,6 +4,8 @@ import ( "fmt" tea "github.com/charmbracelet/bubbletea" "github.com/lmika/dynamo-browse/internal/dynamo-browse/models" + "strings" + "time" ) type SetTableItemView struct { @@ -42,6 +44,24 @@ func (rs NewResultSet) ModeMessage() string { return modeLine } +func (rs NewResultSet) RightModeMessage() string { + var sb strings.Builder + + itemCountStr := applyToN("", len(rs.ResultSet.Items()), "item", "items", "") + if rs.currentFilter != "" { + sb.WriteString(fmt.Sprintf("%d of %v", rs.filteredCount, itemCountStr)) + } else { + sb.WriteString(itemCountStr) + } + + if !rs.ResultSet.Created.IsZero() { + sb.WriteString(" • ") + sb.WriteString(rs.ResultSet.Created.Format(time.Kitchen)) + } + + return sb.String() +} + func (rs NewResultSet) StatusMessage() string { if rs.statusMessage != "" { return rs.statusMessage diff --git a/internal/dynamo-browse/controllers/export.go b/internal/dynamo-browse/controllers/export.go index 15a86c6..7f4ce8e 100644 --- a/internal/dynamo-browse/controllers/export.go +++ b/internal/dynamo-browse/controllers/export.go @@ -1,26 +1,39 @@ package controllers import ( + "bytes" "context" "encoding/csv" "fmt" + "io" + "os" + tea "github.com/charmbracelet/bubbletea" "github.com/lmika/dynamo-browse/internal/common/ui/events" + "github.com/lmika/dynamo-browse/internal/dynamo-browse/models" "github.com/lmika/dynamo-browse/internal/dynamo-browse/models/attrutils" + "github.com/lmika/dynamo-browse/internal/dynamo-browse/models/columns" + "github.com/lmika/dynamo-browse/internal/dynamo-browse/services" "github.com/lmika/dynamo-browse/internal/dynamo-browse/services/jobs" "github.com/pkg/errors" - "os" ) type ExportController struct { - state *State - tableService TableReadService - jobController *JobsController - columns *ColumnsController + state *State + tableService TableReadService + jobController *JobsController + columns *ColumnsController + pasteboardProvider services.PasteboardProvider } -func NewExportController(state *State, tableService TableReadService, jobsController *JobsController, columns *ColumnsController) *ExportController { - return &ExportController{state, tableService, jobsController, columns} +func NewExportController( + state *State, + tableService TableReadService, + jobsController *JobsController, + columns *ColumnsController, + pasteboardProvider services.PasteboardProvider, +) *ExportController { + return &ExportController{state, tableService, jobsController, columns, pasteboardProvider} } func (c *ExportController) ExportCSV(filename string, opts ExportOptions) tea.Msg { @@ -79,6 +92,54 @@ func (c *ExportController) ExportCSV(filename string, opts ExportOptions) tea.Ms }).Submit() } +func (c *ExportController) ExportCSVToClipboard() tea.Msg { + var bts bytes.Buffer + + resultSet := c.state.ResultSet() + if resultSet == nil { + return errors.New("no result set") + } + + if err := c.exportCSV(&bts, c.columns.Columns().VisibleColumns(), resultSet); err != nil { + return events.Error(err) + } + + if err := c.pasteboardProvider.WriteText(bts.Bytes()); err != nil { + return events.Error(err) + } + return nil +} + +// TODO: this really needs to be a service! +func (c *ExportController) ExportToWriter(w io.Writer, resultSet *models.ResultSet) error { + return c.exportCSV(w, columns.NewColumnsFromResultSet(resultSet).Columns, resultSet) +} + +func (c *ExportController) exportCSV(w io.Writer, cols []columns.Column, resultSet *models.ResultSet) error { + cw := csv.NewWriter(w) + defer cw.Flush() + + colNames := make([]string, len(cols)) + for i, c := range cols { + colNames[i] = c.Name + } + if err := cw.Write(colNames); err != nil { + return errors.Wrap(err, "cannot export to clipboard") + } + + row := make([]string, len(cols)) + for _, item := range resultSet.Items() { + for i, col := range cols { + row[i], _ = attrutils.AttributeToString(col.Evaluator.EvaluateForItem(item)) + } + if err := cw.Write(row); err != nil { + return errors.Wrap(err, "cannot export to clipboard") + } + } + + return nil +} + type ExportOptions struct { // AllResults returns all results from the table AllResults bool diff --git a/internal/dynamo-browse/controllers/scripts_test.go b/internal/dynamo-browse/controllers/scripts_test.go index a99e820..ffcb8a8 100644 --- a/internal/dynamo-browse/controllers/scripts_test.go +++ b/internal/dynamo-browse/controllers/scripts_test.go @@ -1,12 +1,13 @@ package controllers_test import ( + "testing" + "time" + "github.com/aws/aws-sdk-go-v2/service/dynamodb/types" "github.com/lmika/dynamo-browse/internal/common/ui/events" "github.com/lmika/dynamo-browse/internal/dynamo-browse/controllers" "github.com/stretchr/testify/assert" - "testing" - "time" ) func TestScriptController_RunScript(t *testing.T) { @@ -53,7 +54,7 @@ func TestScriptController_RunScript(t *testing.T) { srv := newService(t, serviceConfig{ tableName: "alpha-table", scriptFS: testScriptFile(t, "test.tm", ` - rs := session.query('pk="abc"').unwrap() + rs := session.query('pk="abc"') ui.print(rs.length) `), }) @@ -72,7 +73,7 @@ func TestScriptController_RunScript(t *testing.T) { srv := newService(t, serviceConfig{ tableName: "alpha-table", scriptFS: testScriptFile(t, "test.tm", ` - rs := session.query('pk!="abc"', { table: "count-to-30" }).unwrap() + rs := session.query('pk!="abc"', { table: "count-to-30" }) ui.print(rs.length) `), }) @@ -93,7 +94,7 @@ func TestScriptController_RunScript(t *testing.T) { srv := newService(t, serviceConfig{ tableName: "alpha-table", scriptFS: testScriptFile(t, "test.tm", ` - rs := session.query('pk="abc"').unwrap() + rs := session.query('pk="abc"') session.set_result_set(rs) `), }) @@ -112,7 +113,7 @@ func TestScriptController_RunScript(t *testing.T) { srv := newService(t, serviceConfig{ tableName: "alpha-table", scriptFS: testScriptFile(t, "test.tm", ` - rs := session.query('pk="abc"').unwrap() + rs := session.query('pk="abc"') rs[0].set_attr("pk", "131") session.set_result_set(rs) `), @@ -135,22 +136,35 @@ func TestScriptController_RunScript(t *testing.T) { func TestScriptController_LookupCommand(t *testing.T) { t.Run("should schedule the script on a separate go-routine", func(t *testing.T) { - srv := newService(t, serviceConfig{ - tableName: "alpha-table", - scriptFS: testScriptFile(t, "test.tm", ` - ext.command("mycommand", func(name) { - ui.print("Hello, ", name) + scenarios := []struct { + descr string + command string + expectedOutput string + }{ + {descr: "command with arg", command: "mycommand \"test name\"", expectedOutput: "Hello, test name"}, + {descr: "command no arg", command: "mycommand", expectedOutput: "Hello, nil value"}, + } + + for _, scenario := range scenarios { + t.Run(scenario.descr, func(t *testing.T) { + srv := newService(t, serviceConfig{ + tableName: "alpha-table", + scriptFS: testScriptFile(t, "test.tm", ` + ext.command("mycommand", func(name = "nil value") { + ui.print(sprintf("Hello, %v", name)) + }) + `), }) - `), - }) - invokeCommand(t, srv.scriptController.LoadScript("test.tm")) - invokeCommand(t, srv.commandController.Execute(`mycommand "test name"`)) + invokeCommand(t, srv.scriptController.LoadScript("test.tm")) + invokeCommand(t, srv.commandController.Execute(scenario.command)) - srv.msgSender.waitForAtLeastOneMessages(t, 5*time.Second) + srv.msgSender.waitForAtLeastOneMessages(t, 5*time.Second) - assert.Len(t, srv.msgSender.msgs, 1) - assert.Equal(t, events.StatusMsg("Hello, test name"), srv.msgSender.msgs[0]) + assert.Len(t, srv.msgSender.msgs, 1) + assert.Equal(t, events.StatusMsg(scenario.expectedOutput), srv.msgSender.msgs[0]) + }) + } }) t.Run("should only allow one script to run at a time", func(t *testing.T) { diff --git a/internal/dynamo-browse/controllers/state.go b/internal/dynamo-browse/controllers/state.go index 2a75518..6a886d2 100644 --- a/internal/dynamo-browse/controllers/state.go +++ b/internal/dynamo-browse/controllers/state.go @@ -1,9 +1,8 @@ package controllers import ( - "sync" - "github.com/lmika/dynamo-browse/internal/dynamo-browse/models" + "sync" ) type State struct { diff --git a/internal/dynamo-browse/controllers/tableread.go b/internal/dynamo-browse/controllers/tableread.go index 5297d1a..f055e7d 100644 --- a/internal/dynamo-browse/controllers/tableread.go +++ b/internal/dynamo-browse/controllers/tableread.go @@ -4,22 +4,24 @@ import ( "bytes" "context" "fmt" + "log" + "strings" + "sync" + "github.com/aws/aws-sdk-go-v2/service/dynamodb/types" tea "github.com/charmbracelet/bubbletea" "github.com/lmika/dynamo-browse/internal/common/ui/events" "github.com/lmika/dynamo-browse/internal/dynamo-browse/models" "github.com/lmika/dynamo-browse/internal/dynamo-browse/models/attrcodec" + "github.com/lmika/dynamo-browse/internal/dynamo-browse/models/attrutils" "github.com/lmika/dynamo-browse/internal/dynamo-browse/models/queryexpr" "github.com/lmika/dynamo-browse/internal/dynamo-browse/models/serialisable" + "github.com/lmika/dynamo-browse/internal/dynamo-browse/services" "github.com/lmika/dynamo-browse/internal/dynamo-browse/services/inputhistory" "github.com/lmika/dynamo-browse/internal/dynamo-browse/services/itemrenderer" "github.com/lmika/dynamo-browse/internal/dynamo-browse/services/viewsnapshot" bus "github.com/lmika/events" "github.com/pkg/errors" - "golang.design/x/clipboard" - "log" - "strings" - "sync" ) type resultSetUpdateOp int @@ -57,11 +59,11 @@ type TableReadController struct { eventBus *bus.Bus tableName string loadFromLastView bool + pasteboardProvider services.PasteboardProvider // state - mutex *sync.Mutex - state *State - clipboardInit bool + mutex *sync.Mutex + state *State } func NewTableReadController( @@ -72,6 +74,7 @@ func NewTableReadController( jobController *JobsController, inputHistoryService *inputhistory.Service, eventBus *bus.Bus, + pasteboardProvider services.PasteboardProvider, tableName string, ) *TableReadController { return &TableReadController{ @@ -83,6 +86,7 @@ func NewTableReadController( inputHistoryService: inputHistoryService, eventBus: eventBus, tableName: tableName, + pasteboardProvider: pasteboardProvider, mutex: new(sync.Mutex), } } @@ -276,13 +280,35 @@ func (c *TableReadController) setResultSetAndFilter(resultSet *models.ResultSet, return c.state.buildNewResultSetMessage("") } -func (c *TableReadController) Mark(op MarkOp) tea.Msg { - c.state.withResultSet(func(resultSet *models.ResultSet) { - for i := range resultSet.Items() { +func (c *TableReadController) Mark(op MarkOp, where string) tea.Msg { + var ( + whereExpr *queryexpr.QueryExpr + err error + ) + + if where != "" { + whereExpr, err = queryexpr.Parse(where) + if err != nil { + return events.Error(err) + } + } + + if err := c.state.withResultSetReturningError(func(resultSet *models.ResultSet) error { + for i, item := range resultSet.Items() { if resultSet.Hidden(i) { continue } + if whereExpr != nil { + res, err := whereExpr.EvalItem(item) + if err != nil { + return errors.Wrapf(err, "item %d", i) + } + if !attrutils.Truthy(res) { + continue + } + } + switch op { case MarkOpMark: resultSet.SetMark(i, true) @@ -292,7 +318,10 @@ func (c *TableReadController) Mark(op MarkOp) tea.Msg { resultSet.SetMark(i, !resultSet.Marked(i)) } } - }) + return nil + }); err != nil { + return events.Error(err) + } return ResultSetUpdated{} } @@ -446,12 +475,8 @@ func (c *TableReadController) updateViewToSnapshot(viewSnapshot *serialisable.Vi } func (c *TableReadController) CopyItemToClipboard(idx int) tea.Msg { - if err := c.initClipboard(); err != nil { - return events.Error(err) - } - itemCount := 0 - c.state.withResultSet(func(resultSet *models.ResultSet) { + if err := c.state.withResultSetReturningError(func(resultSet *models.ResultSet) error { sb := new(strings.Builder) _ = applyToMarkedItems(resultSet, idx, func(idx int, item models.Item) error { if sb.Len() > 0 { @@ -461,23 +486,14 @@ func (c *TableReadController) CopyItemToClipboard(idx int) tea.Msg { itemCount += 1 return nil }) - clipboard.Write(clipboard.FmtText, []byte(sb.String())) - }) + + if err := c.pasteboardProvider.WriteText([]byte(sb.String())); err != nil { + return err + } + return nil + }); err != nil { + return events.Error(err) + } return events.StatusMsg(applyToN("", itemCount, "item", "items", " copied to clipboard")) } - -func (c *TableReadController) initClipboard() error { - c.mutex.Lock() - defer c.mutex.Unlock() - - if c.clipboardInit { - return nil - } - - if err := clipboard.Init(); err != nil { - return errors.Wrap(err, "unable to enable clipboard") - } - c.clipboardInit = true - return nil -} diff --git a/internal/dynamo-browse/controllers/tablewrite.go b/internal/dynamo-browse/controllers/tablewrite.go index a92319b..033a0cb 100644 --- a/internal/dynamo-browse/controllers/tablewrite.go +++ b/internal/dynamo-browse/controllers/tablewrite.go @@ -458,6 +458,44 @@ func (twc *TableWriteController) assertReadWrite() error { return nil } +func (twc *TableWriteController) CloneItem(idx int) tea.Msg { + if err := twc.assertReadWrite(); err != nil { + return events.Error(err) + } + + // Work out which keys we need to prompt for + rs := twc.state.ResultSet() + + keyPrompts := &promptSequence{ + prompts: []string{rs.TableInfo.Keys.PartitionKey + ": "}, + } + if rs.TableInfo.Keys.SortKey != "" { + keyPrompts.prompts = append(keyPrompts.prompts, rs.TableInfo.Keys.SortKey+": ") + } + keyPrompts.onAllDone = func(values []string) tea.Msg { + twc.state.withResultSet(func(set *models.ResultSet) { + applyToMarkedItems(set, idx, func(idx int, item models.Item) error { + // TODO: should be a deep clone + clonedItem := item.Clone() + + clonedItem[rs.TableInfo.Keys.PartitionKey] = &types.AttributeValueMemberS{Value: values[0]} + if len(values) == 2 { + clonedItem[rs.TableInfo.Keys.SortKey] = &types.AttributeValueMemberS{Value: values[1]} + } + + set.AddNewItem(clonedItem, models.ItemAttribute{ + New: true, + Dirty: true, + }) + return nil + }) + }) + return twc.state.buildNewResultSetMessage("New item cloned") + } + + return keyPrompts.next() +} + func applyToN(prefix string, n int, singular, plural, suffix string) string { if n == 1 { return fmt.Sprintf("%v%v %v%v", prefix, n, singular, suffix) diff --git a/internal/dynamo-browse/controllers/tablewrite_test.go b/internal/dynamo-browse/controllers/tablewrite_test.go index a4f25ec..493b5c7 100644 --- a/internal/dynamo-browse/controllers/tablewrite_test.go +++ b/internal/dynamo-browse/controllers/tablewrite_test.go @@ -9,6 +9,7 @@ import ( "github.com/lmika/dynamo-browse/internal/dynamo-browse/models" "github.com/lmika/dynamo-browse/internal/dynamo-browse/providers/dynamo" "github.com/lmika/dynamo-browse/internal/dynamo-browse/providers/inputhistorystore" + "github.com/lmika/dynamo-browse/internal/dynamo-browse/providers/pasteboardprovider" "github.com/lmika/dynamo-browse/internal/dynamo-browse/providers/settingstore" "github.com/lmika/dynamo-browse/internal/dynamo-browse/providers/workspacestore" "github.com/lmika/dynamo-browse/internal/dynamo-browse/services/inputhistory" @@ -617,11 +618,21 @@ func newService(t *testing.T, cfg serviceConfig) *services { state := controllers.NewState() jobsController := controllers.NewJobsController(jobs.NewService(eventBus), eventBus, true) - readController := controllers.NewTableReadController(state, service, workspaceService, itemRendererService, jobsController, inputHistoryService, eventBus, cfg.tableName) + readController := controllers.NewTableReadController( + state, + service, + workspaceService, + itemRendererService, + jobsController, + inputHistoryService, + eventBus, + pasteboardprovider.NilProvider{}, + cfg.tableName, + ) writeController := controllers.NewTableWriteController(state, service, jobsController, readController, settingStore) settingsController := controllers.NewSettingsController(settingStore, eventBus) columnsController := controllers.NewColumnsController(eventBus) - exportController := controllers.NewExportController(state, service, jobsController, columnsController) + exportController := controllers.NewExportController(state, service, jobsController, columnsController, pasteboardprovider.NilProvider{}) scriptController := controllers.NewScriptController(scriptService, readController, settingsController, eventBus) commandController := commandctrl.NewCommandController(inputHistoryService) diff --git a/internal/dynamo-browse/models/attrutils/truthy.go b/internal/dynamo-browse/models/attrutils/truthy.go new file mode 100644 index 0000000..91bce48 --- /dev/null +++ b/internal/dynamo-browse/models/attrutils/truthy.go @@ -0,0 +1,32 @@ +package attrutils + +import ( + "github.com/aws/aws-sdk-go-v2/service/dynamodb/types" +) + +func Truthy(x types.AttributeValue) bool { + switch xVal := x.(type) { + case *types.AttributeValueMemberS: + return len(xVal.Value) > 0 + case *types.AttributeValueMemberN: + return len(xVal.Value) > 0 && xVal.Value != "0" + case *types.AttributeValueMemberBOOL: + return xVal.Value + case *types.AttributeValueMemberB: + return len(xVal.Value) > 0 + case *types.AttributeValueMemberNULL: + return !xVal.Value + case *types.AttributeValueMemberL: + return len(xVal.Value) > 0 + case *types.AttributeValueMemberM: + return len(xVal.Value) > 0 + case *types.AttributeValueMemberBS: + return len(xVal.Value) > 0 + case *types.AttributeValueMemberNS: + return len(xVal.Value) > 0 + case *types.AttributeValueMemberSS: + return len(xVal.Value) > 0 + } + + return false +} diff --git a/internal/dynamo-browse/models/items.go b/internal/dynamo-browse/models/items.go index 0df6bf8..1319bee 100644 --- a/internal/dynamo-browse/models/items.go +++ b/internal/dynamo-browse/models/items.go @@ -16,7 +16,7 @@ type Item map[string]types.AttributeValue func (i Item) Clone() Item { newItem := Item{} - // TODO: should be a deep clone? + // TODO: should be a deep clone? YES!! for k, v := range i { newItem[k] = v } @@ -33,6 +33,14 @@ func (i Item) KeyValue(info *TableInfo) map[string]types.AttributeValue { return itemKey } +func (i Item) PKSK(info *TableInfo) (pk types.AttributeValue, sk types.AttributeValue) { + pk = i[info.Keys.PartitionKey] + if info.Keys.SortKey != "" { + sk = i[info.Keys.SortKey] + } + return pk, sk +} + func (i Item) AttributeValueAsString(key string) (string, bool) { return attrutils.AttributeToString(i[key]) } diff --git a/internal/dynamo-browse/models/models.go b/internal/dynamo-browse/models/models.go index f94aa72..0aed4f1 100644 --- a/internal/dynamo-browse/models/models.go +++ b/internal/dynamo-browse/models/models.go @@ -3,12 +3,14 @@ package models import ( "github.com/aws/aws-sdk-go-v2/service/dynamodb/types" "sort" + "time" ) type ResultSet struct { // Query information TableInfo *TableInfo Query Queryable + Created time.Time ExclusiveStartKey map[string]types.AttributeValue // Result information diff --git a/internal/dynamo-browse/models/queryexpr/builtins.go b/internal/dynamo-browse/models/queryexpr/builtins.go index a3e9322..d1a072a 100644 --- a/internal/dynamo-browse/models/queryexpr/builtins.go +++ b/internal/dynamo-browse/models/queryexpr/builtins.go @@ -2,6 +2,7 @@ package queryexpr import ( "context" + "github.com/pkg/errors" ) @@ -50,6 +51,42 @@ var nativeFuncs = map[string]nativeFunc{ return listExprValue(xs), nil }, + "marked": func(ctx context.Context, args []exprValue) (exprValue, error) { + if len(args) != 1 { + return nil, InvalidArgumentNumberError{Name: "marked", Expected: 1, Actual: len(args)} + } + + fieldName, ok := args[0].(stringableExprValue) + if !ok { + return nil, InvalidArgumentTypeError{Name: "marked", ArgIndex: 0, Expected: "S"} + } + + rs := currentResultSetFromContext(ctx) + if rs == nil { + return listExprValue{}, nil + } + + var items = []exprValue{} + for i, itm := range rs.Items() { + if !rs.Marked(i) { + continue + } + + attr, hasAttr := itm[fieldName.asString()] + if !hasAttr { + continue + } + + exprAttrValue, err := newExprValueFromAttributeValue(attr) + if err != nil { + return nil, errors.Wrapf(err, "marked(): item %d, attr %v", i, fieldName.asString()) + } + + items = append(items, exprAttrValue) + } + return listExprValue(items), nil + }, + "_x_now": func(ctx context.Context, args []exprValue) (exprValue, error) { now := timeSourceFromContext(ctx).now().Unix() return int64ExprValue(now), nil diff --git a/internal/dynamo-browse/models/queryexpr/context.go b/internal/dynamo-browse/models/queryexpr/context.go index 18c56ef..08bf1e1 100644 --- a/internal/dynamo-browse/models/queryexpr/context.go +++ b/internal/dynamo-browse/models/queryexpr/context.go @@ -3,6 +3,8 @@ package queryexpr import ( "context" "time" + + "github.com/lmika/dynamo-browse/internal/dynamo-browse/models" ) type timeSource interface { @@ -25,3 +27,14 @@ func timeSourceFromContext(ctx context.Context) timeSource { } return defaultTimeSource{} } + +type currentResultSetContextKeyType struct{} + +var currentResultSetContextKey = currentResultSetContextKeyType{} + +func currentResultSetFromContext(ctx context.Context) *models.ResultSet { + if crs, ok := ctx.Value(currentResultSetContextKey).(*models.ResultSet); ok { + return crs + } + return nil +} diff --git a/internal/dynamo-browse/models/queryexpr/dot.go b/internal/dynamo-browse/models/queryexpr/dot.go index 8981f5a..cdb9412 100644 --- a/internal/dynamo-browse/models/queryexpr/dot.go +++ b/internal/dynamo-browse/models/queryexpr/dot.go @@ -18,7 +18,7 @@ func (dt *astRef) unqualifiedName() (string, bool) { func (dt *astRef) evalItem(ctx *evalContext, item models.Item) (exprValue, error) { res, hasV := item[dt.Name] if !hasV { - return nil, nil + return undefinedExprValue{}, nil } return newExprValueFromAttributeValue(res) diff --git a/internal/dynamo-browse/models/queryexpr/equality.go b/internal/dynamo-browse/models/queryexpr/equality.go index 8d317e5..f67803b 100644 --- a/internal/dynamo-browse/models/queryexpr/equality.go +++ b/internal/dynamo-browse/models/queryexpr/equality.go @@ -73,8 +73,6 @@ func (a *astEqualityOp) evalItem(ctx *evalContext, item models.Item) (exprValue, return nil, err } - // TODO: use expr values here - switch a.Op { case "=": cmp, isComparable := attrutils.CompareScalarAttributes(left.asAttributeValue(), right.asAttributeValue()) @@ -96,7 +94,7 @@ func (a *astEqualityOp) evalItem(ctx *evalContext, item models.Item) (exprValue, leftAsStr, canBeString := left.(stringableExprValue) if !canBeString { - return nil, ValueNotConvertableToString{Val: leftAsStr.asAttributeValue()} + return nil, ValueNotConvertableToString{Val: left.asAttributeValue()} } return boolExprValue(strings.HasPrefix(leftAsStr.asString(), strValue.asString())), nil } diff --git a/internal/dynamo-browse/models/queryexpr/errors.go b/internal/dynamo-browse/models/queryexpr/errors.go index 172b2fa..852aaef 100644 --- a/internal/dynamo-browse/models/queryexpr/errors.go +++ b/internal/dynamo-browse/models/queryexpr/errors.go @@ -69,6 +69,10 @@ type ValueNotConvertableToString struct { func (n ValueNotConvertableToString) Error() string { render := itemrender.ToRenderer(n.Val) + if render == nil { + return "nil value is not convertable to string" + } + return fmt.Sprintf("values '%v', type %v, is not convertable to string", render.StringValue(), render.TypeName()) } diff --git a/internal/dynamo-browse/models/queryexpr/expr.go b/internal/dynamo-browse/models/queryexpr/expr.go index 2d254e7..5a263cc 100644 --- a/internal/dynamo-browse/models/queryexpr/expr.go +++ b/internal/dynamo-browse/models/queryexpr/expr.go @@ -3,6 +3,9 @@ package queryexpr import ( "bytes" "encoding/gob" + "hash/fnv" + "io" + "github.com/aws/aws-sdk-go-v2/service/dynamodb/types" "github.com/lmika/dynamo-browse/internal/dynamo-browse/models" "github.com/lmika/dynamo-browse/internal/dynamo-browse/models/attrcodec" @@ -10,15 +13,14 @@ import ( "github.com/pkg/errors" "golang.org/x/exp/maps" "golang.org/x/exp/slices" - "hash/fnv" - "io" ) type QueryExpr struct { - ast *astExpr - index string - names map[string]string - values map[string]types.AttributeValue + ast *astExpr + index string + names map[string]string + values map[string]types.AttributeValue + currentResultSet *models.ResultSet // tests fields only timeSource timeSource @@ -141,10 +143,11 @@ func (md *QueryExpr) HashCode() uint64 { func (md *QueryExpr) WithNameParams(value map[string]string) *QueryExpr { return &QueryExpr{ - ast: md.ast, - index: md.index, - names: value, - values: md.values, + ast: md.ast, + index: md.index, + names: value, + values: md.values, + currentResultSet: md.currentResultSet, } } @@ -166,19 +169,31 @@ func (md *QueryExpr) ValueParamOrNil(name string) types.AttributeValue { func (md *QueryExpr) WithValueParams(value map[string]types.AttributeValue) *QueryExpr { return &QueryExpr{ - ast: md.ast, - index: md.index, - names: md.names, - values: value, + ast: md.ast, + index: md.index, + names: md.names, + values: value, + currentResultSet: md.currentResultSet, } } func (md *QueryExpr) WithIndex(index string) *QueryExpr { return &QueryExpr{ - ast: md.ast, - index: index, - names: md.names, - values: md.values, + ast: md.ast, + index: index, + names: md.names, + values: md.values, + currentResultSet: md.currentResultSet, + } +} + +func (md *QueryExpr) WithCurrentResultSet(currentResultSet *models.ResultSet) *QueryExpr { + return &QueryExpr{ + ast: md.ast, + index: md.index, + names: md.names, + values: md.values, + currentResultSet: currentResultSet, } } @@ -217,6 +232,7 @@ func (md *QueryExpr) evalContext() *evalContext { return &evalContext{ namePlaceholders: md.names, valuePlaceholders: md.values, + ctxResultSet: md.currentResultSet, } } @@ -268,6 +284,7 @@ type evalContext struct { valuePlaceholders map[string]types.AttributeValue valueLookup func(string) (types.AttributeValue, bool) timeSource timeSource + ctxResultSet *models.ResultSet } func (ec *evalContext) lookupName(name string) (string, bool) { diff --git a/internal/dynamo-browse/models/queryexpr/expr_test.go b/internal/dynamo-browse/models/queryexpr/expr_test.go index b8c7dce..76e7a02 100644 --- a/internal/dynamo-browse/models/queryexpr/expr_test.go +++ b/internal/dynamo-browse/models/queryexpr/expr_test.go @@ -3,11 +3,12 @@ package queryexpr_test import ( "bytes" "fmt" + "testing" + "time" + "github.com/aws/aws-sdk-go-v2/aws" "github.com/aws/aws-sdk-go-v2/service/dynamodb/types" "github.com/lmika/dynamo-browse/internal/dynamo-browse/models/queryexpr" - "testing" - "time" "github.com/lmika/dynamo-browse/internal/dynamo-browse/models" "github.com/stretchr/testify/assert" @@ -603,19 +604,40 @@ func TestQueryExpr_EvalItem(t *testing.T) { t.Run("functions", func(t *testing.T) { timeNow := time.Now() + contextResultSet := models.ResultSet{} + contextResultSet.SetItems([]models.Item{ + {"pk": &types.AttributeValueMemberS{Value: "1"}, "num": &types.AttributeValueMemberN{Value: "1"}}, + {"pk": &types.AttributeValueMemberS{Value: "2"}, "num": &types.AttributeValueMemberN{Value: "2"}}, + {"pk": &types.AttributeValueMemberS{Value: "3"}, "num": &types.AttributeValueMemberN{Value: "3"}}, + {"pk": &types.AttributeValueMemberS{Value: "4"}, "num": &types.AttributeValueMemberN{Value: "4"}}, + }) + contextResultSet.SetMark(0, true) + contextResultSet.SetMark(1, true) + scenarios := []struct { expr string expected types.AttributeValue }{ // _x_now() -- unreleased version of now {expr: `_x_now()`, expected: &types.AttributeValueMemberN{Value: fmt.Sprint(timeNow.Unix())}}, + + // Marked + {expr: `marked("num")`, expected: &types.AttributeValueMemberL{Value: []types.AttributeValue{ + &types.AttributeValueMemberN{Value: "1"}, + &types.AttributeValueMemberN{Value: "2"}, + }}}, + {expr: `one in marked("num")`, expected: &types.AttributeValueMemberBOOL{Value: true}}, + {expr: `three in marked("num")`, expected: &types.AttributeValueMemberBOOL{Value: false}}, } for _, scenario := range scenarios { t.Run(scenario.expr, func(t *testing.T) { modExpr, err := queryexpr.Parse(scenario.expr) assert.NoError(t, err) - res, err := modExpr.WithTestTimeSource(timeNow).EvalItem(item) + res, err := modExpr. + WithTestTimeSource(timeNow). + WithCurrentResultSet(&contextResultSet). + EvalItem(item) assert.NoError(t, err) assert.Equal(t, scenario.expected, res) @@ -646,6 +668,10 @@ func TestQueryExpr_EvalItem(t *testing.T) { }{ {expr: `alpha.bravo`, expectedError: queryexpr.ValueNotAMapError([]string{"alpha", "bravo"})}, {expr: `charlie.tree.bla`, expectedError: queryexpr.ValueNotAMapError([]string{"charlie", "tree", "bla"})}, + + {expr: `missing="no"`, expectedError: queryexpr.ValuesNotComparable{Right: &types.AttributeValueMemberS{Value: "no"}}}, + {expr: `missing!="no"`, expectedError: queryexpr.ValuesNotComparable{Right: &types.AttributeValueMemberS{Value: "no"}}}, + {expr: `missing^="no"`, expectedError: queryexpr.ValueNotConvertableToString{nil}}, } for _, scenario := range scenarios { diff --git a/internal/dynamo-browse/models/queryexpr/fncall.go b/internal/dynamo-browse/models/queryexpr/fncall.go index 69d6790..5a092af 100644 --- a/internal/dynamo-browse/models/queryexpr/fncall.go +++ b/internal/dynamo-browse/models/queryexpr/fncall.go @@ -2,11 +2,12 @@ package queryexpr import ( "context" + "strings" + "github.com/aws/aws-sdk-go-v2/feature/dynamodb/expression" "github.com/lmika/dynamo-browse/internal/common/sliceutils" "github.com/lmika/dynamo-browse/internal/dynamo-browse/models" "github.com/pkg/errors" - "strings" ) func (a *astFunctionCall) evalToIR(ctx *evalContext, info *models.TableInfo) (irAtom, error) { @@ -88,6 +89,7 @@ func (a *astFunctionCall) evalItem(ctx *evalContext, item models.Item) (exprValu } cCtx := context.WithValue(context.Background(), timeSourceContextKey, ctx.timeSource) + cCtx = context.WithValue(cCtx, currentResultSetContextKey, ctx.ctxResultSet) return fn(cCtx, args) } diff --git a/internal/dynamo-browse/models/queryexpr/helpers_test.go b/internal/dynamo-browse/models/queryexpr/helpers_test.go index e856529..fe495da 100644 --- a/internal/dynamo-browse/models/queryexpr/helpers_test.go +++ b/internal/dynamo-browse/models/queryexpr/helpers_test.go @@ -14,3 +14,4 @@ func (a *QueryExpr) WithTestTimeSource(timeNow time.Time) *QueryExpr { a.timeSource = testTimeSource(timeNow) return a } + diff --git a/internal/dynamo-browse/models/queryexpr/is.go b/internal/dynamo-browse/models/queryexpr/is.go index c7cd392..cc2bc88 100644 --- a/internal/dynamo-browse/models/queryexpr/is.go +++ b/internal/dynamo-browse/models/queryexpr/is.go @@ -127,7 +127,7 @@ func (a *astIsOp) evalItem(ctx *evalContext, item models.Item) (exprValue, error var resultOfIs bool if typeInfo.isAny { - resultOfIs = ref != nil + resultOfIs = ref != undefinedExprValue{} } else { refType := reflect.TypeOf(ref) diff --git a/internal/dynamo-browse/models/queryexpr/types.go b/internal/dynamo-browse/models/queryexpr/types.go index 32e39ae..2e55c7a 100644 --- a/internal/dynamo-browse/models/queryexpr/types.go +++ b/internal/dynamo-browse/models/queryexpr/types.go @@ -83,6 +83,20 @@ func newExprValueFromAttributeValue(ev types.AttributeValue) (exprValue, error) return nil, errors.New("cannot convert to expr value") } +type undefinedExprValue struct{} + +func (b undefinedExprValue) asGoValue() any { + return nil +} + +func (b undefinedExprValue) asAttributeValue() types.AttributeValue { + return nil +} + +func (s undefinedExprValue) typeName() string { + return "UNDEFINED" +} + type stringExprValue string func (s stringExprValue) asGoValue() any { diff --git a/internal/dynamo-browse/providers/pasteboardprovider/nilprovider.go b/internal/dynamo-browse/providers/pasteboardprovider/nilprovider.go new file mode 100644 index 0000000..7285aff --- /dev/null +++ b/internal/dynamo-browse/providers/pasteboardprovider/nilprovider.go @@ -0,0 +1,11 @@ +package pasteboardprovider + +type NilProvider struct{} + +func (NilProvider) ReadText() (string, bool) { + return "", false +} + +func (n NilProvider) WriteText(bts []byte) error { + return nil +} diff --git a/internal/dynamo-browse/providers/pasteboardprovider/providers.go b/internal/dynamo-browse/providers/pasteboardprovider/providers.go new file mode 100644 index 0000000..fb53ac2 --- /dev/null +++ b/internal/dynamo-browse/providers/pasteboardprovider/providers.go @@ -0,0 +1,55 @@ +package pasteboardprovider + +import ( + "github.com/pkg/errors" + "golang.design/x/clipboard" + "sync" +) + +type Provider struct { + mutex *sync.Mutex + clipboardInit bool +} + +func New() *Provider { + return &Provider{ + mutex: new(sync.Mutex), + } +} + +func (c *Provider) initClipboard() error { + c.mutex.Lock() + defer c.mutex.Unlock() + + if c.clipboardInit { + return nil + } + + if err := clipboard.Init(); err != nil { + return errors.Wrap(err, "unable to enable clipboard") + } + c.clipboardInit = true + return nil +} + +func (c *Provider) WriteText(bts []byte) error { + if err := c.initClipboard(); err != nil { + return err + } + + clipboard.Write(clipboard.FmtText, bts) + return nil +} + +func (c *Provider) ReadText() (string, bool) { + if err := c.initClipboard(); err != nil { + return "", false + } + + content := clipboard.Read(clipboard.FmtText) + if content == nil { + return "", false + } + + return string(content), true +} diff --git a/internal/dynamo-browse/providers/settingstore/settingstore.go b/internal/dynamo-browse/providers/settingstore/settingstore.go index bcdba19..b22b596 100644 --- a/internal/dynamo-browse/providers/settingstore/settingstore.go +++ b/internal/dynamo-browse/providers/settingstore/settingstore.go @@ -113,7 +113,7 @@ func (c *SettingStore) SetDefaultLimit(limit int) error { func (c *SettingStore) getStringValue(key string, def string) (string, error) { var val string - if err := c.ws.Get(settingBucket, keyTableReadOnly, &val); err != nil { + if err := c.ws.Get(settingBucket, key, &val); err != nil { if errors.Is(err, storm.ErrNotFound) { return def, nil } diff --git a/internal/dynamo-browse/services/pasteboardprovider.go b/internal/dynamo-browse/services/pasteboardprovider.go new file mode 100644 index 0000000..18b3549 --- /dev/null +++ b/internal/dynamo-browse/services/pasteboardprovider.go @@ -0,0 +1,6 @@ +package services + +type PasteboardProvider interface { + ReadText() (string, bool) + WriteText(bts []byte) error +} diff --git a/internal/dynamo-browse/services/scriptmanager/builtins.go b/internal/dynamo-browse/services/scriptmanager/builtins.go index 9674c99..87e2852 100644 --- a/internal/dynamo-browse/services/scriptmanager/builtins.go +++ b/internal/dynamo-browse/services/scriptmanager/builtins.go @@ -7,7 +7,8 @@ package scriptmanager import ( "context" - "github.com/cloudcmds/tamarin/object" + "fmt" + "github.com/risor-io/risor/object" "log" ) @@ -53,3 +54,19 @@ func printfBuiltin(ctx context.Context, args ...object.Object) object.Object { log.Printf("%s "+format, values...) return object.Nil } + +// This is taken from the args package +func require(funcName string, count int, args []object.Object) *object.Error { + nArgs := len(args) + if nArgs != count { + if count == 1 { + return object.Errorf( + fmt.Sprintf("type error: %s() takes exactly 1 argument (%d given)", + funcName, nArgs)) + } + return object.Errorf( + fmt.Sprintf("type error: %s() takes exactly %d arguments (%d given)", + funcName, count, nArgs)) + } + return nil +} diff --git a/internal/dynamo-browse/services/scriptmanager/modext.go b/internal/dynamo-browse/services/scriptmanager/modext.go index c581d17..f08051a 100644 --- a/internal/dynamo-browse/services/scriptmanager/modext.go +++ b/internal/dynamo-browse/services/scriptmanager/modext.go @@ -3,10 +3,8 @@ package scriptmanager import ( "context" "fmt" - "github.com/cloudcmds/tamarin/arg" - "github.com/cloudcmds/tamarin/object" - "github.com/cloudcmds/tamarin/scope" "github.com/pkg/errors" + "github.com/risor-io/risor/object" "regexp" ) @@ -18,22 +16,17 @@ type extModule struct { scriptPlugin *ScriptPlugin } -func (m *extModule) register(scp *scope.Scope) { - modScope := scope.New(scope.Opts{}) - mod := object.NewModule("ext", modScope) - - modScope.AddBuiltins([]*object.Builtin{ - object.NewBuiltin("command", m.command, mod), - object.NewBuiltin("key_binding", m.keyBinding, mod), +func (m *extModule) register() *object.Module { + return object.NewBuiltinsModule("ext", map[string]object.Object{ + "command": object.NewBuiltin("command", m.command), + "key_binding": object.NewBuiltin("key_binding", m.keyBinding), }) - - scp.Declare("ext", mod, true) } func (m *extModule) command(ctx context.Context, args ...object.Object) object.Object { thisEnv := scriptEnvFromCtx(ctx) - if err := arg.Require("ext.command", 2, args); err != nil { + if err := require("ext.command", 2, args); err != nil { return err } @@ -62,8 +55,10 @@ func (m *extModule) command(ctx context.Context, args ...object.Object) object.O newEnv.options = m.scriptPlugin.scriptService.options ctx = ctxWithScriptEnv(ctx, newEnv) - res := callFn(ctx, fnRes.Scope(), fnRes, objArgs) - if object.IsError(res) { + res, err := callFn(ctx, fnRes, objArgs) + if err != nil { + return errors.Errorf("command error '%v':%v - %v", m.scriptPlugin.name, cmdName, err) + } else if object.IsError(res) { errObj := res.(*object.Error) return errors.Errorf("command error '%v':%v - %v", m.scriptPlugin.name, cmdName, errObj.Inspect()) } @@ -80,7 +75,7 @@ func (m *extModule) command(ctx context.Context, args ...object.Object) object.O func (m *extModule) keyBinding(ctx context.Context, args ...object.Object) object.Object { thisEnv := scriptEnvFromCtx(ctx) - if err := arg.Require("ext.key_binding", 3, args); err != nil { + if err := require("ext.key_binding", 3, args); err != nil { return err } @@ -122,8 +117,10 @@ func (m *extModule) keyBinding(ctx context.Context, args ...object.Object) objec newEnv.options = m.scriptPlugin.scriptService.options ctx = ctxWithScriptEnv(ctx, newEnv) - res := callFn(ctx, fnRes.Scope(), fnRes, objArgs) - if object.IsError(res) { + res, err := callFn(ctx, fnRes, objArgs) + if err != nil { + return errors.Errorf("command error '%v':%v - %v", m.scriptPlugin.name, bindingName, err) + } else if object.IsError(res) { errObj := res.(*object.Error) return errors.Errorf("command error '%v':%v - %v", m.scriptPlugin.name, bindingName, errObj.Inspect()) } diff --git a/internal/dynamo-browse/services/scriptmanager/modos.go b/internal/dynamo-browse/services/scriptmanager/modos.go index b050fc4..a89d130 100644 --- a/internal/dynamo-browse/services/scriptmanager/modos.go +++ b/internal/dynamo-browse/services/scriptmanager/modos.go @@ -2,9 +2,7 @@ package scriptmanager import ( "context" - "github.com/cloudcmds/tamarin/arg" - "github.com/cloudcmds/tamarin/object" - "github.com/cloudcmds/tamarin/scope" + "github.com/risor-io/risor/object" "os" "os/exec" ) @@ -13,7 +11,7 @@ type osModule struct { } func (om *osModule) exec(ctx context.Context, args ...object.Object) object.Object { - if err := arg.Require("os.exec", 1, args); err != nil { + if err := require("os.exec", 1, args); err != nil { return err } @@ -24,20 +22,20 @@ func (om *osModule) exec(ctx context.Context, args ...object.Object) object.Obje opts := scriptEnvFromCtx(ctx).options if !opts.Permissions.AllowShellCommands { - return object.NewErrResult(object.Errorf("permission error: no permission to shell out")) + return object.Errorf("permission error: no permission to shell out") } cmd := exec.Command(opts.OSExecShell, "-c", cmdExec) out, err := cmd.Output() if err != nil { - return object.NewErrResult(object.NewError(err)) + return object.NewError(err) } - return object.NewOkResult(object.NewString(string(out))) + return object.NewString(string(out)) } func (om *osModule) env(ctx context.Context, args ...object.Object) object.Object { - if err := arg.Require("os.env", 1, args); err != nil { + if err := require("os.env", 1, args); err != nil { return err } @@ -58,14 +56,9 @@ func (om *osModule) env(ctx context.Context, args ...object.Object) object.Objec return object.NewString(envVal) } -func (om *osModule) register(scp *scope.Scope) { - modScope := scope.New(scope.Opts{}) - mod := object.NewModule("os", modScope) - - modScope.AddBuiltins([]*object.Builtin{ - object.NewBuiltin("exec", om.exec, mod), - object.NewBuiltin("env", om.env, mod), +func (om *osModule) register() *object.Module { + return object.NewBuiltinsModule("os", map[string]object.Object{ + "exec": object.NewBuiltin("exec", om.exec), + "env": object.NewBuiltin("env", om.env), }) - - scp.Declare("os", mod, true) } diff --git a/internal/dynamo-browse/services/scriptmanager/modos_test.go b/internal/dynamo-browse/services/scriptmanager/modos_test.go index 63dfe32..49d2edf 100644 --- a/internal/dynamo-browse/services/scriptmanager/modos_test.go +++ b/internal/dynamo-browse/services/scriptmanager/modos_test.go @@ -68,13 +68,11 @@ func TestOSModule_Env(t *testing.T) { func TestOSModule_Exec(t *testing.T) { t.Run("should run command and return stdout", func(t *testing.T) { mockedUIService := mocks.NewUIService(t) - mockedUIService.EXPECT().PrintMessage(mock.Anything, "false") mockedUIService.EXPECT().PrintMessage(mock.Anything, "hello world\n") testFS := testScriptFile(t, "test.tm", ` res := os.exec('echo "hello world"') - ui.print(res.is_err()) - ui.print(res.unwrap()) + ui.print(res) `) srv := scriptmanager.New(scriptmanager.WithFS(testFS)) @@ -97,11 +95,11 @@ func TestOSModule_Exec(t *testing.T) { t.Run("should refuse to execute command if do not have permissions", func(t *testing.T) { mockedUIService := mocks.NewUIService(t) - mockedUIService.EXPECT().PrintMessage(mock.Anything, "true") + mockedUIService.EXPECT().PrintMessage(mock.Anything, "failed") testFS := testScriptFile(t, "test.tm", ` - res := os.exec('echo "hello world"') - ui.print(res.is_err()) + res := try(func() { return os.exec('echo "hello world"') }, "failed") + ui.print(res) `) srv := scriptmanager.New(scriptmanager.WithFS(testFS)) @@ -125,14 +123,13 @@ func TestOSModule_Exec(t *testing.T) { t.Run("should be able to change permissions which will affect plugins", func(t *testing.T) { mockedUIService := mocks.NewUIService(t) mockedUIService.EXPECT().PrintMessage(mock.Anything, "Loaded the plugin\n") - mockedUIService.EXPECT().PrintMessage(mock.Anything, "true") testFS := testScriptFile(t, "test.tm", ` ext.command("mycommand", func() { - ui.print(os.exec('echo "this cannot run"').is_err()) + ui.print(os.exec('echo "this cannot run"')) }) - ui.print(os.exec('echo "Loaded the plugin"').unwrap()) + ui.print(os.exec('echo "Loaded the plugin"')) `) srv := scriptmanager.New(scriptmanager.WithFS(testFS)) @@ -159,7 +156,7 @@ func TestOSModule_Exec(t *testing.T) { errChan := make(chan error) assert.NoError(t, srv.LookupCommand("mycommand").Invoke(ctx, []string{}, errChan)) - assert.NoError(t, waitForErr(t, errChan)) + assert.Error(t, waitForErr(t, errChan)) mockedUIService.AssertExpectations(t) }) diff --git a/internal/dynamo-browse/services/scriptmanager/modsession.go b/internal/dynamo-browse/services/scriptmanager/modsession.go index d8e65b6..95c16c7 100644 --- a/internal/dynamo-browse/services/scriptmanager/modsession.go +++ b/internal/dynamo-browse/services/scriptmanager/modsession.go @@ -4,10 +4,8 @@ import ( "context" "fmt" "github.com/aws/aws-sdk-go-v2/service/dynamodb/types" - "github.com/cloudcmds/tamarin/arg" - "github.com/cloudcmds/tamarin/object" - "github.com/cloudcmds/tamarin/scope" "github.com/pkg/errors" + "github.com/risor-io/risor/object" ) type sessionModule struct { @@ -77,13 +75,13 @@ func (um *sessionModule) query(ctx context.Context, args ...object.Object) objec resp, err := um.sessionService.Query(ctx, expr, options) if err != nil { - return object.NewErrResult(object.NewError(err)) + return object.NewError(err) } - return object.NewOkResult(&resultSetProxy{resultSet: resp}) + return &resultSetProxy{resultSet: resp} } func (um *sessionModule) resultSet(ctx context.Context, args ...object.Object) object.Object { - if err := arg.Require("session.result_set", 0, args); err != nil { + if err := require("session.result_set", 0, args); err != nil { return err } @@ -95,7 +93,7 @@ func (um *sessionModule) resultSet(ctx context.Context, args ...object.Object) o } func (um *sessionModule) selectedItem(ctx context.Context, args ...object.Object) object.Object { - if err := arg.Require("session.result_set", 0, args); err != nil { + if err := require("session.result_set", 0, args); err != nil { return err } @@ -110,7 +108,7 @@ func (um *sessionModule) selectedItem(ctx context.Context, args ...object.Object } func (um *sessionModule) setResultSet(ctx context.Context, args ...object.Object) object.Object { - if err := arg.Require("session.set_result_set", 1, args); err != nil { + if err := require("session.set_result_set", 1, args); err != nil { return err } @@ -124,7 +122,7 @@ func (um *sessionModule) setResultSet(ctx context.Context, args ...object.Object } func (um *sessionModule) currentTable(ctx context.Context, args ...object.Object) object.Object { - if err := arg.Require("session.current_table", 0, args); err != nil { + if err := require("session.current_table", 0, args); err != nil { return err } @@ -136,17 +134,12 @@ func (um *sessionModule) currentTable(ctx context.Context, args ...object.Object return &tableProxy{table: rs.TableInfo} } -func (um *sessionModule) register(scp *scope.Scope) { - modScope := scope.New(scope.Opts{}) - mod := object.NewModule("session", modScope) - - modScope.AddBuiltins([]*object.Builtin{ - object.NewBuiltin("query", um.query, mod), - object.NewBuiltin("current_table", um.currentTable, mod), - object.NewBuiltin("result_set", um.resultSet, mod), - object.NewBuiltin("selected_item", um.selectedItem, mod), - object.NewBuiltin("set_result_set", um.setResultSet, mod), +func (um *sessionModule) register() *object.Module { + return object.NewBuiltinsModule("session", map[string]object.Object{ + "query": object.NewBuiltin("query", um.query), + "current_table": object.NewBuiltin("current_table", um.currentTable), + "result_set": object.NewBuiltin("result_set", um.resultSet), + "selected_item": object.NewBuiltin("selected_item", um.selectedItem), + "set_result_set": object.NewBuiltin("set_result_set", um.setResultSet), }) - - scp.Declare("session", mod, true) } diff --git a/internal/dynamo-browse/services/scriptmanager/modsession_test.go b/internal/dynamo-browse/services/scriptmanager/modsession_test.go index 947efe0..8712cf5 100644 --- a/internal/dynamo-browse/services/scriptmanager/modsession_test.go +++ b/internal/dynamo-browse/services/scriptmanager/modsession_test.go @@ -102,7 +102,7 @@ func TestModSession_Query(t *testing.T) { mockedUIService.EXPECT().PrintMessage(mock.Anything, "res[1].attr('size(pk)') = 4") testFS := testScriptFile(t, "test.tm", ` - res := session.query("some expr").unwrap() + res := session.query("some expr") ui.print(res.length) ui.print("res[0]['pk'].S = ", res[0].attr("pk")) ui.print("res[1]['pk'].S = ", res[1].attr("pk")) @@ -128,13 +128,9 @@ func TestModSession_Query(t *testing.T) { mockedSessionService.EXPECT().Query(mock.Anything, "some expr", scriptmanager.QueryOptions{}).Return(nil, errors.New("bang")) mockedUIService := mocks.NewUIService(t) - mockedUIService.EXPECT().PrintMessage(mock.Anything, "true") - mockedUIService.EXPECT().PrintMessage(mock.Anything, "err(\"bang\")") testFS := testScriptFile(t, "test.tm", ` res := session.query("some expr") - ui.print(res.is_err()) - ui.print(res) `) srv := scriptmanager.New(scriptmanager.WithFS(testFS)) @@ -145,7 +141,7 @@ func TestModSession_Query(t *testing.T) { ctx := context.Background() err := <-srv.RunAdHocScript(ctx, "test.tm") - assert.NoError(t, err) + assert.Error(t, err) mockedUIService.AssertExpectations(t) mockedSessionService.AssertExpectations(t) @@ -165,7 +161,7 @@ func TestModSession_Query(t *testing.T) { res := session.query("some expr", { table: "some-table", }) - assert(!res.is_err()) + assert(res) `) srv := scriptmanager.New(scriptmanager.WithFS(testFS)) @@ -201,7 +197,7 @@ func TestModSession_Query(t *testing.T) { res := session.query("some expr", { table: session.result_set().table, }) - assert(!res.is_err()) + assert(res) `) srv := scriptmanager.New(scriptmanager.WithFS(testFS)) @@ -242,7 +238,7 @@ func TestModSession_Query(t *testing.T) { value: "world", }, }) - assert(!res.is_err()) + assert(res) `) srv := scriptmanager.New(scriptmanager.WithFS(testFS)) @@ -288,7 +284,7 @@ func TestModSession_Query(t *testing.T) { "nil": nil, }, }) - assert(!res.is_err()) + assert(res) `) srv := scriptmanager.New(scriptmanager.WithFS(testFS)) @@ -315,7 +311,6 @@ func TestModSession_Query(t *testing.T) { "bad": func() { }, }, }) - assert(res.is_err()) `) srv := scriptmanager.New(scriptmanager.WithFS(testFS)) @@ -411,7 +406,7 @@ func TestModSession_SetResultSet(t *testing.T) { mockedUIService := mocks.NewUIService(t) testFS := testScriptFile(t, "test.tm", ` - res := session.query("some expr").unwrap() + res := session.query("some expr") session.set_result_set(res) `) diff --git a/internal/dynamo-browse/services/scriptmanager/modui.go b/internal/dynamo-browse/services/scriptmanager/modui.go index 081eaa5..d53b2e4 100644 --- a/internal/dynamo-browse/services/scriptmanager/modui.go +++ b/internal/dynamo-browse/services/scriptmanager/modui.go @@ -2,10 +2,9 @@ package scriptmanager import ( "context" - "github.com/cloudcmds/tamarin/arg" - "github.com/cloudcmds/tamarin/object" - "github.com/cloudcmds/tamarin/scope" "strings" + + "github.com/risor-io/risor/object" ) type uiModule struct { @@ -15,6 +14,10 @@ type uiModule struct { func (um *uiModule) print(ctx context.Context, args ...object.Object) object.Object { var msg strings.Builder for _, arg := range args { + if arg == nil { + continue + } + switch a := arg.(type) { case *object.String: msg.WriteString(a.Value()) @@ -28,7 +31,7 @@ func (um *uiModule) print(ctx context.Context, args ...object.Object) object.Obj } func (um *uiModule) prompt(ctx context.Context, args ...object.Object) object.Object { - if err := arg.Require("ui.prompt", 1, args); err != nil { + if err := require("ui.prompt", 1, args); err != nil { return err } @@ -47,14 +50,9 @@ func (um *uiModule) prompt(ctx context.Context, args ...object.Object) object.Ob } } -func (um *uiModule) register(scp *scope.Scope) { - modScope := scope.New(scope.Opts{}) - mod := object.NewModule("ui", modScope) - - modScope.AddBuiltins([]*object.Builtin{ - object.NewBuiltin("print", um.print, mod), - object.NewBuiltin("prompt", um.prompt, mod), +func (um *uiModule) register() *object.Module { + return object.NewBuiltinsModule("ui", map[string]object.Object{ + "print": object.NewBuiltin("print", um.print), + "prompt": object.NewBuiltin("prompt", um.prompt), }) - - scp.Declare("ui", mod, true) } diff --git a/internal/dynamo-browse/services/scriptmanager/opts.go b/internal/dynamo-browse/services/scriptmanager/opts.go index 9d8e489..9ef4408 100644 --- a/internal/dynamo-browse/services/scriptmanager/opts.go +++ b/internal/dynamo-browse/services/scriptmanager/opts.go @@ -3,6 +3,8 @@ package scriptmanager import ( "context" "os" + + "github.com/risor-io/risor/limits" ) type Options struct { @@ -50,5 +52,7 @@ func scriptEnvFromCtx(ctx context.Context) scriptEnv { } func ctxWithScriptEnv(ctx context.Context, perms scriptEnv) context.Context { - return context.WithValue(ctx, scriptEnvKey, perms) + newCtx := context.WithValue(ctx, scriptEnvKey, perms) + newCtx = limits.WithLimits(newCtx, limits.New()) + return newCtx } diff --git a/internal/dynamo-browse/services/scriptmanager/resultsetproxy.go b/internal/dynamo-browse/services/scriptmanager/resultsetproxy.go index d3ee331..0d6be7d 100644 --- a/internal/dynamo-browse/services/scriptmanager/resultsetproxy.go +++ b/internal/dynamo-browse/services/scriptmanager/resultsetproxy.go @@ -2,17 +2,33 @@ package scriptmanager import ( "context" - "github.com/cloudcmds/tamarin/arg" - "github.com/cloudcmds/tamarin/object" + "time" + + "github.com/aws/aws-sdk-go-v2/service/dynamodb/types" "github.com/lmika/dynamo-browse/internal/dynamo-browse/models" + "github.com/lmika/dynamo-browse/internal/dynamo-browse/models/attrutils" "github.com/lmika/dynamo-browse/internal/dynamo-browse/models/queryexpr" "github.com/pkg/errors" + "github.com/risor-io/risor/object" + "github.com/risor-io/risor/op" ) type resultSetProxy struct { resultSet *models.ResultSet } +func (r *resultSetProxy) SetAttr(name string, value object.Object) error { + return errors.Errorf("attribute error: %v", name) +} + +func (r *resultSetProxy) RunOperation(opType op.BinaryOpType, right object.Object) object.Object { + return object.Errorf("op error: unsupported %v", opType) +} + +func (r *resultSetProxy) Cost() int { + return len(r.resultSet.Items()) +} + func (r *resultSetProxy) Interface() interface{} { return r.resultSet } @@ -95,17 +111,105 @@ func (r *resultSetProxy) GetAttr(name string) (object.Object, bool) { return &tableProxy{table: r.resultSet.TableInfo}, true case "length": return object.NewInt(int64(len(r.resultSet.Items()))), true + case "find": + return object.NewBuiltin("find", r.find), true + case "merge": + return object.NewBuiltin("merge", r.merge), true } return nil, false } +func (i *resultSetProxy) find(ctx context.Context, args ...object.Object) object.Object { + if objErr := require("resultset.find", 1, args); objErr != nil { + return objErr + } + + str, objErr := object.AsString(args[0]) + if objErr != nil { + return objErr + } + + modExpr, err := queryexpr.Parse(str) + if err != nil { + return object.Errorf("arg error: invalid path expression: %v", err) + } + + for idx, item := range i.resultSet.Items() { + rs, err := modExpr.EvalItem(item) + if err != nil { + continue + } + + if attrutils.Truthy(rs) { + return newItemProxy(i, idx) + } + } + + return object.Nil +} + +func (i *resultSetProxy) merge(ctx context.Context, args ...object.Object) object.Object { + type pksk struct { + pk types.AttributeValue + sk types.AttributeValue + } + + if objErr := require("resultset.merge", 1, args); objErr != nil { + return objErr + } + + otherRS, isRS := args[0].(*resultSetProxy) + if !isRS { + return object.NewError(errors.Errorf("type error: expected a resultset (got %v)", args[0].Type())) + } + + if !i.resultSet.TableInfo.Equal(otherRS.resultSet.TableInfo) { + return object.Nil + } + + itemsInI := make(map[pksk]models.Item) + newItems := make([]models.Item, 0, len(i.resultSet.Items())+len(otherRS.resultSet.Items())) + for _, item := range i.resultSet.Items() { + pk, sk := item.PKSK(i.resultSet.TableInfo) + itemsInI[pksk{pk, sk}] = item + newItems = append(newItems, item) + } + + for _, item := range otherRS.resultSet.Items() { + pk, sk := item.PKSK(i.resultSet.TableInfo) + if _, hasItem := itemsInI[pksk{pk, sk}]; !hasItem { + newItems = append(newItems, item) + } + } + + newResultSet := &models.ResultSet{ + Created: time.Now(), + TableInfo: i.resultSet.TableInfo, + } + newResultSet.SetItems(newItems) + + return &resultSetProxy{resultSet: newResultSet} +} + type itemProxy struct { resultSetProxy *resultSetProxy itemIndex int item models.Item } +func (i *itemProxy) SetAttr(name string, value object.Object) error { + return errors.Errorf("attribute error: %v", name) +} + +func (i *itemProxy) RunOperation(opType op.BinaryOpType, right object.Object) object.Object { + return object.Errorf("op error: unsupported %v", opType) +} + +func (i *itemProxy) Cost() int { + return len(i.item) +} + func newItemProxy(rs *resultSetProxy, itemIndex int) *itemProxy { return &itemProxy{ resultSetProxy: rs, @@ -154,7 +258,7 @@ func (i *itemProxy) GetAttr(name string) (object.Object, bool) { } func (i *itemProxy) value(ctx context.Context, args ...object.Object) object.Object { - if objErr := arg.Require("item.attr", 1, args); objErr != nil { + if objErr := require("item.attr", 1, args); objErr != nil { return objErr } @@ -180,7 +284,7 @@ func (i *itemProxy) value(ctx context.Context, args ...object.Object) object.Obj } func (i *itemProxy) setValue(ctx context.Context, args ...object.Object) object.Object { - if objErr := arg.Require("item.set_attr", 2, args); objErr != nil { + if objErr := require("item.set_attr", 2, args); objErr != nil { return objErr } @@ -207,7 +311,7 @@ func (i *itemProxy) setValue(ctx context.Context, args ...object.Object) object. } func (i *itemProxy) deleteAttr(ctx context.Context, args ...object.Object) object.Object { - if objErr := arg.Require("item.delete_attr", 1, args); objErr != nil { + if objErr := require("item.delete_attr", 1, args); objErr != nil { return objErr } diff --git a/internal/dynamo-browse/services/scriptmanager/resultsetproxy_test.go b/internal/dynamo-browse/services/scriptmanager/resultsetproxy_test.go index c52ea3e..3b7f354 100644 --- a/internal/dynamo-browse/services/scriptmanager/resultsetproxy_test.go +++ b/internal/dynamo-browse/services/scriptmanager/resultsetproxy_test.go @@ -2,13 +2,14 @@ package scriptmanager_test import ( "context" + "testing" + "github.com/aws/aws-sdk-go-v2/service/dynamodb/types" "github.com/lmika/dynamo-browse/internal/dynamo-browse/models" "github.com/lmika/dynamo-browse/internal/dynamo-browse/services/scriptmanager" "github.com/lmika/dynamo-browse/internal/dynamo-browse/services/scriptmanager/mocks" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/mock" - "testing" ) func TestResultSetProxy(t *testing.T) { @@ -29,7 +30,7 @@ func TestResultSetProxy(t *testing.T) { mockedUIService := mocks.NewUIService(t) testFS := testScriptFile(t, "test.tm", ` - res := session.query("some expr").unwrap() + res := session.query("some expr") // Test properties of the result set assert(res.table.name, "hello") @@ -60,6 +61,123 @@ func TestResultSetProxy(t *testing.T) { }) } +func TestResultSetProxy_Find(t *testing.T) { + t.Run("should return the first item that matches the given expression", func(t *testing.T) { + rs := &models.ResultSet{} + rs.SetItems([]models.Item{ + {"pk": &types.AttributeValueMemberS{Value: "abc"}}, + {"pk": &types.AttributeValueMemberS{Value: "abc"}, "sk": &types.AttributeValueMemberS{Value: "abc"}, "primary": &types.AttributeValueMemberS{Value: "yes"}}, + {"pk": &types.AttributeValueMemberS{Value: "1232"}, "findMe": &types.AttributeValueMemberS{Value: "yes"}}, + {"pk": &types.AttributeValueMemberS{Value: "2345"}, "findMe": &types.AttributeValueMemberS{Value: "second"}}, + }) + + mockedSessionService := mocks.NewSessionService(t) + mockedSessionService.EXPECT().Query(mock.Anything, "some expr", scriptmanager.QueryOptions{}).Return(rs, nil) + + testFS := testScriptFile(t, "test.tm", ` + res := session.query("some expr") + + assert(res.find('findMe is "any"').attr("pk") == "1232") + assert(res.find('findMe = "second"').attr("pk") == "2345") + assert(res.find('pk = sk').attr("primary") == "yes") + + assert(res.find('findMe = "missing"') == nil) + `) + + srv := scriptmanager.New(scriptmanager.WithFS(testFS)) + srv.SetIFaces(scriptmanager.Ifaces{ + Session: mockedSessionService, + }) + + ctx := context.Background() + err := <-srv.RunAdHocScript(ctx, "test.tm") + assert.NoError(t, err) + + mockedSessionService.AssertExpectations(t) + }) +} + +func TestResultSetProxy_Merge(t *testing.T) { + t.Run("should return a result set with items from both if both are from the same table", func(t *testing.T) { + td := &models.TableInfo{Name: "test", Keys: models.KeyAttribute{PartitionKey: "pk", SortKey: "sk"}} + + rs1 := &models.ResultSet{TableInfo: td} + rs1.SetItems([]models.Item{ + {"pk": &types.AttributeValueMemberS{Value: "abc"}, "sk": &types.AttributeValueMemberS{Value: "123"}}, + }) + + rs2 := &models.ResultSet{TableInfo: td} + rs2.SetItems([]models.Item{ + {"pk": &types.AttributeValueMemberS{Value: "bcd"}, "sk": &types.AttributeValueMemberS{Value: "234"}}, + }) + + mockedSessionService := mocks.NewSessionService(t) + mockedSessionService.EXPECT().Query(mock.Anything, "rs1", scriptmanager.QueryOptions{}).Return(rs1, nil) + mockedSessionService.EXPECT().Query(mock.Anything, "rs2", scriptmanager.QueryOptions{}).Return(rs2, nil) + + testFS := testScriptFile(t, "test.tm", ` + r1 := session.query("rs1") + r2 := session.query("rs2") + + res := r1.merge(r2) + + assert(res[0].attr("pk") == "abc") + assert(res[0].attr("sk") == "123") + assert(res[1].attr("pk") == "bcd") + assert(res[1].attr("sk") == "234") + `) + + srv := scriptmanager.New(scriptmanager.WithFS(testFS)) + srv.SetIFaces(scriptmanager.Ifaces{ + Session: mockedSessionService, + }) + + ctx := context.Background() + err := <-srv.RunAdHocScript(ctx, "test.tm") + assert.NoError(t, err) + + mockedSessionService.AssertExpectations(t) + }) + + t.Run("should return nil if result-sets are from different tables", func(t *testing.T) { + td1 := &models.TableInfo{Name: "test", Keys: models.KeyAttribute{PartitionKey: "pk", SortKey: "sk"}} + rs1 := &models.ResultSet{TableInfo: td1} + rs1.SetItems([]models.Item{ + {"pk": &types.AttributeValueMemberS{Value: "abc"}, "sk": &types.AttributeValueMemberS{Value: "123"}}, + }) + + td2 := &models.TableInfo{Name: "test2", Keys: models.KeyAttribute{PartitionKey: "pk2", SortKey: "sk"}} + rs2 := &models.ResultSet{TableInfo: td2} + rs2.SetItems([]models.Item{ + {"pk": &types.AttributeValueMemberS{Value: "bcd"}, "sk": &types.AttributeValueMemberS{Value: "234"}}, + }) + + mockedSessionService := mocks.NewSessionService(t) + mockedSessionService.EXPECT().Query(mock.Anything, "rs1", scriptmanager.QueryOptions{}).Return(rs1, nil) + mockedSessionService.EXPECT().Query(mock.Anything, "rs2", scriptmanager.QueryOptions{}).Return(rs2, nil) + + testFS := testScriptFile(t, "test.tm", ` + r1 := session.query("rs1") + r2 := session.query("rs2") + + res := r1.merge(r2) + + assert(res == nil) + `) + + srv := scriptmanager.New(scriptmanager.WithFS(testFS)) + srv.SetIFaces(scriptmanager.Ifaces{ + Session: mockedSessionService, + }) + + ctx := context.Background() + err := <-srv.RunAdHocScript(ctx, "test.tm") + assert.NoError(t, err) + + mockedSessionService.AssertExpectations(t) + }) +} + func TestResultSetProxy_GetAttr(t *testing.T) { t.Run("should return the value of items within a result set", func(t *testing.T) { rs := &models.ResultSet{} @@ -87,7 +205,7 @@ func TestResultSetProxy_GetAttr(t *testing.T) { mockedSessionService.EXPECT().Query(mock.Anything, "some expr", scriptmanager.QueryOptions{}).Return(rs, nil) testFS := testScriptFile(t, "test.tm", ` - res := session.query("some expr").unwrap() + res := session.query("some expr") assert(res[0].attr("pk") == "abc", "str attr") assert(res[0].attr("sk") == 123, "num attr") @@ -164,7 +282,7 @@ func TestResultSetProxy_SetAttr(t *testing.T) { mockedUIService := mocks.NewUIService(t) testFS := testScriptFile(t, "test.tm", ` - res := session.query("some expr").unwrap() + res := session.query("some expr") res[0].set_attr("pk", "bla-di-bla") res[0].set_attr("num", 123) @@ -215,7 +333,7 @@ func TestResultSetProxy_DeleteAttr(t *testing.T) { mockedUIService := mocks.NewUIService(t) testFS := testScriptFile(t, "test.tm", ` - res := session.query("some expr").unwrap() + res := session.query("some expr") res[0].delete_attr("deleteMe") session.set_result_set(res) `) diff --git a/internal/dynamo-browse/services/scriptmanager/service.go b/internal/dynamo-browse/services/scriptmanager/service.go index ce8ec78..d90629f 100644 --- a/internal/dynamo-browse/services/scriptmanager/service.go +++ b/internal/dynamo-browse/services/scriptmanager/service.go @@ -2,16 +2,16 @@ package scriptmanager import ( "context" - "github.com/cloudcmds/tamarin/exec" - "github.com/cloudcmds/tamarin/object" - "github.com/cloudcmds/tamarin/scope" - "github.com/lmika/dynamo-browse/internal/dynamo-browse/services/keybindings" - "github.com/pkg/errors" "io/fs" "log" "os" "path/filepath" "strings" + + "github.com/lmika/dynamo-browse/internal/dynamo-browse/services/keybindings" + "github.com/pkg/errors" + "github.com/risor-io/risor" + "github.com/risor-io/risor/object" ) type Service struct { @@ -94,19 +94,14 @@ func (s *Service) startAdHocScript(ctx context.Context, filename string, errChan return } - scp := scope.New(scope.Opts{Parent: s.parentScope()}) - ctx = ctxWithScriptEnv(ctx, scriptEnv{filename: filepath.Base(filename), options: s.options}) - if _, err = exec.Execute(ctx, exec.Opts{ - Input: string(code), - File: filename, - Scope: scp, - Builtins: []*object.Builtin{ - object.NewBuiltin("print", printBuiltin), - object.NewBuiltin("printf", printfBuiltin), - }, - }); err != nil { + if _, err := risor.Eval(ctx, code, + risor.WithGlobals(s.builtins()), + // risor.WithDefaultBuiltins(), + // risor.WithDefaultModules(), + // risor.WithBuiltins(s.builtins()), + ); err != nil { errChan <- errors.Wrapf(err, "script %v", filename) return } @@ -131,17 +126,17 @@ func (s *Service) loadScript(ctx context.Context, filename string, resChan chan scriptService: s, } - scp := scope.New(scope.Opts{Parent: s.parentScope()}) - - (&extModule{scriptPlugin: newPlugin}).register(scp) - ctx = ctxWithScriptEnv(ctx, scriptEnv{filename: filepath.Base(filename), options: s.options}) - if _, err = exec.Execute(ctx, exec.Opts{ - Input: string(code), - File: filename, - Scope: scp, - }); err != nil { + if _, err := risor.Eval(ctx, code, + // risor.WithDefaultBuiltins(), + // risor.WithDefaultModules(), + // risor.WithBuiltins(s.builtins()), + risor.WithGlobals(s.builtins()), + risor.WithGlobals(map[string]any{ + "ext": (&extModule{scriptPlugin: newPlugin}).register(), + }), + ); err != nil { resChan <- loadedScriptResult{err: errors.Wrapf(err, "script %v", filename)} return } @@ -149,7 +144,7 @@ func (s *Service) loadScript(ctx context.Context, filename string, resChan chan resChan <- loadedScriptResult{scriptPlugin: newPlugin} } -func (s *Service) readScript(filename string, allowCwd bool) ([]byte, error) { +func (s *Service) readScript(filename string, allowCwd bool) (string, error) { if allowCwd { if cwd, err := os.Getwd(); err == nil { fullScriptPath := filepath.Join(cwd, filename) @@ -157,9 +152,9 @@ func (s *Service) readScript(filename string, allowCwd bool) ([]byte, error) { if stat, err := os.Stat(fullScriptPath); err == nil && !stat.IsDir() { code, err := os.ReadFile(filename) if err != nil { - return nil, err + return "", err } - return code, nil + return string(code), nil } } else { log.Printf("warn: cannot get cwd for reading script %v: %v", filename, err) @@ -169,9 +164,9 @@ func (s *Service) readScript(filename string, allowCwd bool) ([]byte, error) { if strings.HasPrefix(filename, string(filepath.Separator)) { code, err := os.ReadFile(filename) if err != nil { - return nil, err + return "", err } - return code, nil + return string(code), nil } for _, currFS := range s.lookupPaths { @@ -181,7 +176,7 @@ func (s *Service) readScript(filename string, allowCwd bool) ([]byte, error) { if errors.Is(err, os.ErrNotExist) { continue } else { - return nil, err + return "", err } } else if stat.IsDir() { continue @@ -189,13 +184,13 @@ func (s *Service) readScript(filename string, allowCwd bool) ([]byte, error) { code, err := fs.ReadFile(currFS, filename) if err == nil { - return code, nil + return string(code), nil } else { - return nil, err + return "", err } } - return nil, os.ErrNotExist + return "", os.ErrNotExist } // LookupCommand looks up a command defined by a script. @@ -252,10 +247,12 @@ func (s *Service) RebindKeyBinding(keyBinding string, newKey string) error { return keybindings.InvalidBindingError(keyBinding) } -func (s *Service) parentScope() *scope.Scope { - scp := scope.New(scope.Opts{}) - (&uiModule{uiService: s.ifaces.UI}).register(scp) - (&sessionModule{sessionService: s.ifaces.Session}).register(scp) - (&osModule{}).register(scp) - return scp +func (s *Service) builtins() map[string]any { + return map[string]any{ + "ui": (&uiModule{uiService: s.ifaces.UI}).register(), + "session": (&sessionModule{sessionService: s.ifaces.Session}).register(), + "os": (&osModule{}).register(), + "print": object.NewBuiltin("print", printBuiltin), + "printf": object.NewBuiltin("printf", printfBuiltin), + } } diff --git a/internal/dynamo-browse/services/scriptmanager/tableproxy.go b/internal/dynamo-browse/services/scriptmanager/tableproxy.go index c626085..252348f 100644 --- a/internal/dynamo-browse/services/scriptmanager/tableproxy.go +++ b/internal/dynamo-browse/services/scriptmanager/tableproxy.go @@ -1,9 +1,11 @@ package scriptmanager import ( - "github.com/cloudcmds/tamarin/object" "github.com/lmika/dynamo-browse/internal/common/sliceutils" "github.com/lmika/dynamo-browse/internal/dynamo-browse/models" + "github.com/pkg/errors" + "github.com/risor-io/risor/object" + "github.com/risor-io/risor/op" "reflect" ) @@ -16,6 +18,18 @@ type tableProxy struct { table *models.TableInfo } +func (t *tableProxy) SetAttr(name string, value object.Object) error { + return errors.Errorf("attribute error: %v", name) +} + +func (t *tableProxy) RunOperation(opType op.BinaryOpType, right object.Object) object.Object { + return object.Errorf("op error: unsupported %v", opType) +} + +func (t *tableProxy) Cost() int { + return 0 +} + func (t *tableProxy) Type() object.Type { return "table" } @@ -68,6 +82,18 @@ type tableIndexProxy struct { gsi models.TableGSI } +func (t tableIndexProxy) SetAttr(name string, value object.Object) error { + return errors.Errorf("attribute error: %v", name) +} + +func (t tableIndexProxy) RunOperation(opType op.BinaryOpType, right object.Object) object.Object { + return object.Errorf("op error: unsupported %v", opType) +} + +func (t tableIndexProxy) Cost() int { + return 0 +} + func newTableIndexProxy(gsi models.TableGSI) object.Object { return tableIndexProxy{gsi: gsi} } diff --git a/internal/dynamo-browse/services/scriptmanager/typemapping.go b/internal/dynamo-browse/services/scriptmanager/typemapping.go index 26cec3c..d7b8a47 100644 --- a/internal/dynamo-browse/services/scriptmanager/typemapping.go +++ b/internal/dynamo-browse/services/scriptmanager/typemapping.go @@ -3,10 +3,10 @@ package scriptmanager import ( "fmt" "github.com/aws/aws-sdk-go-v2/service/dynamodb/types" - "github.com/cloudcmds/tamarin/object" "github.com/lmika/dynamo-browse/internal/common/maputils" "github.com/lmika/dynamo-browse/internal/common/sliceutils" "github.com/pkg/errors" + "github.com/risor-io/risor/object" "regexp" "strconv" ) diff --git a/internal/dynamo-browse/services/tables/service.go b/internal/dynamo-browse/services/tables/service.go index c49075a..993b4be 100644 --- a/internal/dynamo-browse/services/tables/service.go +++ b/internal/dynamo-browse/services/tables/service.go @@ -79,6 +79,7 @@ func (s *Service) doScan( if err != nil && len(results) == 0 { return &models.ResultSet{ TableInfo: tableInfo, + Created: time.Now(), Query: expr, ExclusiveStartKey: exclusiveStartKey, LastEvaluatedKey: lastEvalKey, @@ -89,6 +90,7 @@ func (s *Service) doScan( resultSet := &models.ResultSet{ TableInfo: tableInfo, + Created: time.Now(), Query: expr, ExclusiveStartKey: exclusiveStartKey, LastEvaluatedKey: lastEvalKey, diff --git a/internal/dynamo-browse/ui/keybindings/defaults.go b/internal/dynamo-browse/ui/keybindings/defaults.go index 639a369..87e2757 100644 --- a/internal/dynamo-browse/ui/keybindings/defaults.go +++ b/internal/dynamo-browse/ui/keybindings/defaults.go @@ -25,6 +25,7 @@ func Default() *KeyBindings { }, View: &ViewKeyBindings{ Mark: key.NewBinding(key.WithKeys("m"), key.WithHelp("m", "mark")), + ToggleMarkedItems: key.NewBinding(key.WithKeys("M"), key.WithHelp("M", "toggle marged items")), CopyItemToClipboard: key.NewBinding(key.WithKeys("c"), key.WithHelp("c", "copy item to clipboard")), Rescan: key.NewBinding(key.WithKeys("R"), key.WithHelp("R", "rescan")), PromptForQuery: key.NewBinding(key.WithKeys("?"), key.WithHelp("?", "prompt for query")), diff --git a/internal/dynamo-browse/ui/keybindings/keybindings.go b/internal/dynamo-browse/ui/keybindings/keybindings.go index 26178fd..d8257de 100644 --- a/internal/dynamo-browse/ui/keybindings/keybindings.go +++ b/internal/dynamo-browse/ui/keybindings/keybindings.go @@ -31,7 +31,9 @@ type TableKeyBinding struct { type ViewKeyBindings struct { Mark key.Binding `keymap:"mark"` + ToggleMarkedItems key.Binding `keymap:"toggle-marked-items"` CopyItemToClipboard key.Binding `keymap:"copy-item-to-clipboard"` + CopyTableToClipboard key.Binding `keymap:"copy-table-to-clipboard"` Rescan key.Binding `keymap:"rescan"` PromptForQuery key.Binding `keymap:"prompt-for-query"` PromptForFilter key.Binding `keymap:"prompt-for-filter"` diff --git a/internal/dynamo-browse/ui/model.go b/internal/dynamo-browse/ui/model.go index 35aa9cd..c41b572 100644 --- a/internal/dynamo-browse/ui/model.go +++ b/internal/dynamo-browse/ui/model.go @@ -7,6 +7,7 @@ import ( "github.com/lmika/dynamo-browse/internal/common/ui/events" "github.com/lmika/dynamo-browse/internal/dynamo-browse/controllers" "github.com/lmika/dynamo-browse/internal/dynamo-browse/models" + "github.com/lmika/dynamo-browse/internal/dynamo-browse/services" "github.com/lmika/dynamo-browse/internal/dynamo-browse/services/itemrenderer" "github.com/lmika/dynamo-browse/internal/dynamo-browse/ui/keybindings" "github.com/lmika/dynamo-browse/internal/dynamo-browse/ui/teamodels/colselector" @@ -74,6 +75,7 @@ func NewModel( scriptController *controllers.ScriptController, eventBus *bus.Bus, keyBindingController *controllers.KeyBindingController, + pasteboardProvider services.PasteboardProvider, defaultKeyMap *keybindings.KeyBindings, ) Model { uiStyles := styles.DefaultStyles @@ -84,7 +86,7 @@ func NewModel( colSelector := colselector.New(mainView, defaultKeyMap, columnsController) itemEdit := dynamoitemedit.NewModel(colSelector) - statusAndPrompt := statusandprompt.New(itemEdit, "", uiStyles.StatusAndPrompt) + statusAndPrompt := statusandprompt.New(itemEdit, pasteboardProvider, "", uiStyles.StatusAndPrompt) dialogPrompt := dialogprompt.New(statusAndPrompt) tableSelect := tableselect.New(dialogPrompt, uiStyles) @@ -126,7 +128,12 @@ func NewModel( } } - return rc.Mark(markOp) + var whereExpr = "" + if len(args) == 3 && args[1] == "-where" { + whereExpr = args[2] + } + + return rc.Mark(markOp, whereExpr) }, "next-page": func(ctx commandctrl.ExecContext, args []string) tea.Msg { return rc.NextPage() @@ -135,6 +142,9 @@ func NewModel( // TEMP "new-item": commandctrl.NoArgCommand(wc.NewItem), + "clone": func(ctx commandctrl.ExecContext, args []string) tea.Msg { + return wc.CloneItem(dtv.SelectedItemIndex()) + }, "set-attr": func(ctx commandctrl.ExecContext, args []string) tea.Msg { if len(args) == 0 { return events.Error(errors.New("expected field")) @@ -263,10 +273,14 @@ func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { if idx := m.tableView.SelectedItemIndex(); idx >= 0 { return m, events.SetTeaMessage(m.tableWriteController.ToggleMark(idx)) } + case key.Matches(msg, m.keyMap.ToggleMarkedItems): + return m, events.SetTeaMessage(m.tableReadController.Mark(controllers.MarkOpToggle, "")) case key.Matches(msg, m.keyMap.CopyItemToClipboard): if idx := m.tableView.SelectedItemIndex(); idx >= 0 { return m, events.SetTeaMessage(m.tableReadController.CopyItemToClipboard(idx)) } + case key.Matches(msg, m.keyMap.CopyTableToClipboard): + return m, events.SetTeaMessage(m.exportController.ExportCSVToClipboard()) case key.Matches(msg, m.keyMap.Rescan): return m, m.tableReadController.Rescan case key.Matches(msg, m.keyMap.PromptForQuery): diff --git a/internal/dynamo-browse/ui/teamodels/statusandprompt/model.go b/internal/dynamo-browse/ui/teamodels/statusandprompt/model.go index 7bdab7c..cb4b838 100644 --- a/internal/dynamo-browse/ui/teamodels/statusandprompt/model.go +++ b/internal/dynamo-browse/ui/teamodels/statusandprompt/model.go @@ -9,14 +9,17 @@ import ( "github.com/lmika/dynamo-browse/internal/common/ui/events" "github.com/lmika/dynamo-browse/internal/dynamo-browse/ui/teamodels/layout" "github.com/lmika/dynamo-browse/internal/dynamo-browse/ui/teamodels/utils" + "strings" ) // StatusAndPrompt is a resizing model which displays a submodel and a status bar. When the start prompt // event is received, focus will be torn away and the user will be given a prompt the enter text. type StatusAndPrompt struct { model layout.ResizingModel + pasteboardProvider PasteboardProvider style Style modeLine string + rightModeLine string statusMessage string spinner spinner.Model spinnerVisible bool @@ -30,15 +33,17 @@ type Style struct { ModeLine lipgloss.Style } -func New(model layout.ResizingModel, initialMsg string, style Style) *StatusAndPrompt { +func New(model layout.ResizingModel, pasteboardProvider PasteboardProvider, initialMsg string, style Style) *StatusAndPrompt { textInput := textinput.New() return &StatusAndPrompt{ - model: model, - style: style, - statusMessage: initialMsg, - modeLine: "", - spinner: spinner.New(spinner.WithSpinner(spinner.Line)), - textInput: textInput, + model: model, + pasteboardProvider: pasteboardProvider, + style: style, + statusMessage: initialMsg, + modeLine: "", + rightModeLine: "", + spinner: spinner.New(spinner.WithSpinner(spinner.Line)), + textInput: textInput, } } @@ -73,6 +78,11 @@ func (s *StatusAndPrompt) Update(msg tea.Msg) (tea.Model, tea.Cmd) { if hasModeMessage, ok := msg.(events.MessageWithMode); ok { s.modeLine = hasModeMessage.ModeMessage() } + if rightModeMessage, ok := msg.(events.MessageWithRightMode); ok { + s.rightModeLine = rightModeMessage.RightModeMessage() + } else { + s.rightModeLine = "" + } s.statusMessage = msg.StatusMessage() case events.PromptForInputMsg: if s.pendingInput != nil { @@ -96,6 +106,24 @@ func (s *StatusAndPrompt) Update(msg tea.Msg) (tea.Model, tea.Cmd) { }) } s.pendingInput = nil + case tea.KeyCtrlV: + if content, ok := s.pasteboardProvider.ReadText(); ok { + pasteContent := strings.TrimSpace(content) + + cursorPos := s.textInput.Cursor() + beforeValue := s.textInput.Value()[:cursorPos] + pasteContent + newValue := beforeValue + s.textInput.Value()[cursorPos:] + + s.textInput.SetValue(newValue) + s.textInput.SetCursor(len(beforeValue)) + } + case tea.KeyTab: + if tabCompletion := s.pendingInput.originalMsg.OnTabComplete; tabCompletion != nil { + if completion, ok := tabCompletion(s.textInput.Value()); ok { + s.textInput.SetValue(completion) + s.textInput.SetCursor(len(s.textInput.Value())) + } + } case tea.KeyEnter: pendingInput := s.pendingInput s.pendingInput = nil @@ -176,7 +204,10 @@ func (s *StatusAndPrompt) Resize(w, h int) layout.ResizingModel { } func (s *StatusAndPrompt) viewStatus() string { - modeLine := s.style.ModeLine.Render(lipgloss.PlaceHorizontal(s.width, lipgloss.Left, s.modeLine, lipgloss.WithWhitespaceChars(" "))) + rightModeLine := s.style.ModeLine.Render(s.rightModeLine) + modeLine := s.style.ModeLine.Render( + lipgloss.PlaceHorizontal(s.width-lipgloss.Width(rightModeLine), lipgloss.Left, s.modeLine, lipgloss.WithWhitespaceChars(" ")), + ) + rightModeLine var statusLine string if s.pendingInput != nil { diff --git a/internal/dynamo-browse/ui/teamodels/statusandprompt/types.go b/internal/dynamo-browse/ui/teamodels/statusandprompt/types.go index 8f3ccc3..654b303 100644 --- a/internal/dynamo-browse/ui/teamodels/statusandprompt/types.go +++ b/internal/dynamo-browse/ui/teamodels/statusandprompt/types.go @@ -10,3 +10,7 @@ type pendingInputState struct { func newPendingInputState(msg events.PromptForInputMsg) *pendingInputState { return &pendingInputState{originalMsg: msg, historyIdx: -1} } + +type PasteboardProvider interface { + ReadText() (string, bool) +} From ceb064a346b19238868885c4e983c27c5c14721b Mon Sep 17 00:00:00 2001 From: Leon Mika Date: Sun, 3 Mar 2024 08:34:41 +1100 Subject: [PATCH 06/10] Upgraded to Risor 1.4.0 --- go.mod | 6 +++--- go.sum | 2 ++ 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/go.mod b/go.mod index 728a385..96398dc 100644 --- a/go.mod +++ b/go.mod @@ -1,8 +1,8 @@ module github.com/lmika/dynamo-browse -go 1.21 +go 1.22 -toolchain go1.21.1 +toolchain go1.22.0 require ( github.com/alecthomas/participle/v2 v2.0.0-beta.5 @@ -98,7 +98,7 @@ require ( github.com/muesli/cancelreader v0.2.2 // indirect github.com/muesli/termenv v0.13.0 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect - github.com/risor-io/risor v1.1.1 // indirect + github.com/risor-io/risor v1.4.0 // indirect github.com/rivo/uniseg v0.4.2 // indirect github.com/sahilm/fuzzy v0.1.0 // indirect github.com/stretchr/objx v0.5.0 // indirect diff --git a/go.sum b/go.sum index b97ea59..7d71b0d 100644 --- a/go.sum +++ b/go.sum @@ -268,6 +268,8 @@ github.com/risor-io/risor v0.8.0 h1:G0fpHMGztvocKVu8egkKNbvLy4Rsjkuk+0zReu2JSn8= github.com/risor-io/risor v0.8.0/go.mod h1:lvatEIYxs6HL+X/Bm0R+Mq4Z9a5Y036mniw6DwUnqs0= github.com/risor-io/risor v1.1.1 h1:J8rIZX/0HXhg/t2+QygksvP65XCWhg5QxRZrwZabhxE= github.com/risor-io/risor v1.1.1/go.mod h1:0UMw7ZMbUKSPFgQyuHCFe7UuBUewBKX4K3By4ba1CBA= +github.com/risor-io/risor v1.4.0 h1:G17pWgq+N06jWvnaJVwos89tC5C4VMjqwGYRrTWleRM= +github.com/risor-io/risor v1.4.0/go.mod h1:+s/FeK0CdsTCCNZsHSp8EJa3u3mMrhqtNGLCv/GcW8Y= github.com/rivo/uniseg v0.1.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= github.com/rivo/uniseg v0.4.2 h1:YwD0ulJSJytLpiaWua0sBDusfsCZohxjxzVTYjwxfV8= From 12909c89eed0f6e075842e6ec371d31583e0895f Mon Sep 17 00:00:00 2001 From: Leon Mika Date: Sun, 3 Mar 2024 08:54:57 +1100 Subject: [PATCH 07/10] Removed internal os module Module "os" is no longer needed since Risor comes with an "os" and "exec" module out of the box now. --- internal/dynamo-browse/controllers/scripts.go | 7 - .../services/scriptmanager/modext.go | 2 - .../services/scriptmanager/modos.go | 64 --------- .../services/scriptmanager/modos_test.go | 121 +----------------- .../services/scriptmanager/opts.go | 32 ----- .../services/scriptmanager/service.go | 16 +-- 6 files changed, 9 insertions(+), 233 deletions(-) delete mode 100644 internal/dynamo-browse/services/scriptmanager/modos.go diff --git a/internal/dynamo-browse/controllers/scripts.go b/internal/dynamo-browse/controllers/scripts.go index 2bb24fc..ccbf941 100644 --- a/internal/dynamo-browse/controllers/scripts.go +++ b/internal/dynamo-browse/controllers/scripts.go @@ -61,13 +61,6 @@ func (sc *ScriptController) Init() { } else { log.Printf("warn: script lookup paths are invalid: %v", err) } - sc.scriptManager.SetDefaultOptions(scriptmanager.Options{ - OSExecShell: "/bin/bash", - Permissions: scriptmanager.Permissions{ - AllowShellCommands: true, - AllowEnv: true, - }, - }) } func (sc *ScriptController) SetMessageSender(sendMsg func(msg tea.Msg)) { diff --git a/internal/dynamo-browse/services/scriptmanager/modext.go b/internal/dynamo-browse/services/scriptmanager/modext.go index f08051a..0c858cf 100644 --- a/internal/dynamo-browse/services/scriptmanager/modext.go +++ b/internal/dynamo-browse/services/scriptmanager/modext.go @@ -52,7 +52,6 @@ func (m *extModule) command(ctx context.Context, args ...object.Object) object.O } newEnv := thisEnv - newEnv.options = m.scriptPlugin.scriptService.options ctx = ctxWithScriptEnv(ctx, newEnv) res, err := callFn(ctx, fnRes, objArgs) @@ -114,7 +113,6 @@ func (m *extModule) keyBinding(ctx context.Context, args ...object.Object) objec } newEnv := thisEnv - newEnv.options = m.scriptPlugin.scriptService.options ctx = ctxWithScriptEnv(ctx, newEnv) res, err := callFn(ctx, fnRes, objArgs) diff --git a/internal/dynamo-browse/services/scriptmanager/modos.go b/internal/dynamo-browse/services/scriptmanager/modos.go deleted file mode 100644 index a89d130..0000000 --- a/internal/dynamo-browse/services/scriptmanager/modos.go +++ /dev/null @@ -1,64 +0,0 @@ -package scriptmanager - -import ( - "context" - "github.com/risor-io/risor/object" - "os" - "os/exec" -) - -type osModule struct { -} - -func (om *osModule) exec(ctx context.Context, args ...object.Object) object.Object { - if err := require("os.exec", 1, args); err != nil { - return err - } - - cmdExec, objErr := object.AsString(args[0]) - if objErr != nil { - return objErr - } - - opts := scriptEnvFromCtx(ctx).options - if !opts.Permissions.AllowShellCommands { - return object.Errorf("permission error: no permission to shell out") - } - - cmd := exec.Command(opts.OSExecShell, "-c", cmdExec) - out, err := cmd.Output() - if err != nil { - return object.NewError(err) - } - - return object.NewString(string(out)) -} - -func (om *osModule) env(ctx context.Context, args ...object.Object) object.Object { - if err := require("os.env", 1, args); err != nil { - return err - } - - cmdEnvName, objErr := object.AsString(args[0]) - if objErr != nil { - return objErr - } - - opts := scriptEnvFromCtx(ctx).options - if !opts.Permissions.AllowEnv { - return object.Nil - } - - envVal, hasVal := os.LookupEnv(cmdEnvName) - if !hasVal { - return object.Nil - } - return object.NewString(envVal) -} - -func (om *osModule) register() *object.Module { - return object.NewBuiltinsModule("os", map[string]object.Object{ - "exec": object.NewBuiltin("exec", om.exec), - "env": object.NewBuiltin("env", om.env), - }) -} diff --git a/internal/dynamo-browse/services/scriptmanager/modos_test.go b/internal/dynamo-browse/services/scriptmanager/modos_test.go index 49d2edf..455125b 100644 --- a/internal/dynamo-browse/services/scriptmanager/modos_test.go +++ b/internal/dynamo-browse/services/scriptmanager/modos_test.go @@ -15,49 +15,16 @@ func TestOSModule_Env(t *testing.T) { t.Setenv("EMPTY_VALUE", "") testFS := testScriptFile(t, "test.tm", ` - assert(os.env("FULL_VALUE") == "this is a value") - assert(os.env("EMPTY_VALUE") == "") - assert(os.env("MISSING_VALUE") == nil) + assert(os.getenv("FULL_VALUE") == "this is a value") + assert(os.getenv("EMPTY_VALUE") == "") + assert(os.getenv("MISSING_VALUE") == "") - assert(bool(os.env("FULL_VALUE")) == true) - assert(bool(os.env("EMPTY_VALUE")) == false) - assert(bool(os.env("MISSING_VALUE")) == false) + assert(bool(os.getenv("FULL_VALUE")) == true) + assert(bool(os.getenv("EMPTY_VALUE")) == false) + assert(bool(os.getenv("MISSING_VALUE")) == false) `) srv := scriptmanager.New(scriptmanager.WithFS(testFS)) - srv.SetDefaultOptions(scriptmanager.Options{ - OSExecShell: "/bin/bash", - Permissions: scriptmanager.Permissions{ - AllowEnv: true, - }, - }) - - ctx := context.Background() - err := <-srv.RunAdHocScript(ctx, "test.tm") - assert.NoError(t, err) - }) - - t.Run("should return nil when no access to environment variables", func(t *testing.T) { - t.Setenv("FULL_VALUE", "this is a value") - t.Setenv("EMPTY_VALUE", "") - - testFS := testScriptFile(t, "test.tm", ` - assert(os.env("FULL_VALUE") == nil) - assert(os.env("EMPTY_VALUE") == nil) - assert(os.env("MISSING_VALUE") == nil) - - assert(bool(os.env("FULL_VALUE")) == false) - assert(bool(os.env("EMPTY_VALUE")) == false) - assert(bool(os.env("MISSING_VALUE")) == false) - `) - - srv := scriptmanager.New(scriptmanager.WithFS(testFS)) - srv.SetDefaultOptions(scriptmanager.Options{ - OSExecShell: "/bin/bash", - Permissions: scriptmanager.Permissions{ - AllowEnv: false, - }, - }) ctx := context.Background() err := <-srv.RunAdHocScript(ctx, "test.tm") @@ -71,17 +38,11 @@ func TestOSModule_Exec(t *testing.T) { mockedUIService.EXPECT().PrintMessage(mock.Anything, "hello world\n") testFS := testScriptFile(t, "test.tm", ` - res := os.exec('echo "hello world"') + res := exec('echo', ["hello world"]).stdout ui.print(res) `) srv := scriptmanager.New(scriptmanager.WithFS(testFS)) - srv.SetDefaultOptions(scriptmanager.Options{ - OSExecShell: "/bin/bash", - Permissions: scriptmanager.Permissions{ - AllowShellCommands: true, - }, - }) srv.SetIFaces(scriptmanager.Ifaces{ UI: mockedUIService, }) @@ -92,72 +53,4 @@ func TestOSModule_Exec(t *testing.T) { mockedUIService.AssertExpectations(t) }) - - t.Run("should refuse to execute command if do not have permissions", func(t *testing.T) { - mockedUIService := mocks.NewUIService(t) - mockedUIService.EXPECT().PrintMessage(mock.Anything, "failed") - - testFS := testScriptFile(t, "test.tm", ` - res := try(func() { return os.exec('echo "hello world"') }, "failed") - ui.print(res) - `) - - srv := scriptmanager.New(scriptmanager.WithFS(testFS)) - srv.SetDefaultOptions(scriptmanager.Options{ - OSExecShell: "/bin/bash", - Permissions: scriptmanager.Permissions{ - AllowShellCommands: false, - }, - }) - srv.SetIFaces(scriptmanager.Ifaces{ - UI: mockedUIService, - }) - - ctx := context.Background() - err := <-srv.RunAdHocScript(ctx, "test.tm") - assert.NoError(t, err) - - mockedUIService.AssertExpectations(t) - }) - - t.Run("should be able to change permissions which will affect plugins", func(t *testing.T) { - mockedUIService := mocks.NewUIService(t) - mockedUIService.EXPECT().PrintMessage(mock.Anything, "Loaded the plugin\n") - - testFS := testScriptFile(t, "test.tm", ` - ext.command("mycommand", func() { - ui.print(os.exec('echo "this cannot run"')) - }) - - ui.print(os.exec('echo "Loaded the plugin"')) - `) - - srv := scriptmanager.New(scriptmanager.WithFS(testFS)) - srv.SetDefaultOptions(scriptmanager.Options{ - OSExecShell: "/bin/bash", - Permissions: scriptmanager.Permissions{ - AllowShellCommands: true, - }, - }) - srv.SetIFaces(scriptmanager.Ifaces{ - UI: mockedUIService, - }) - - ctx := context.Background() - _, err := srv.LoadScript(ctx, "test.tm") - assert.NoError(t, err) - - srv.SetDefaultOptions(scriptmanager.Options{ - OSExecShell: "/bin/bash", - Permissions: scriptmanager.Permissions{ - AllowShellCommands: false, - }, - }) - - errChan := make(chan error) - assert.NoError(t, srv.LookupCommand("mycommand").Invoke(ctx, []string{}, errChan)) - assert.Error(t, waitForErr(t, errChan)) - - mockedUIService.AssertExpectations(t) - }) } diff --git a/internal/dynamo-browse/services/scriptmanager/opts.go b/internal/dynamo-browse/services/scriptmanager/opts.go index 9ef4408..39d961f 100644 --- a/internal/dynamo-browse/services/scriptmanager/opts.go +++ b/internal/dynamo-browse/services/scriptmanager/opts.go @@ -2,44 +2,12 @@ package scriptmanager import ( "context" - "os" - "github.com/risor-io/risor/limits" ) -type Options struct { - // OSExecShell is the shell to use for calls to 'os.exec'. If not defined, - // it will use the value of the SHELL environment variable, otherwise it will - // default to '/bin/bash' - OSExecShell string - - // Permissions are the permissions the script can execute in - Permissions Permissions -} - -func (opts Options) configuredShell() string { - if opts.OSExecShell != "" { - return opts.OSExecShell - } - if shell, hasShell := os.LookupEnv("SHELL"); hasShell { - return shell - } - return "/bin/bash" -} - -// Permissions control the set of permissions of a script -type Permissions struct { - // AllowShellCommands determines whether or not a script can execute shell commands. - AllowShellCommands bool - - // AllowEnv determines whether or not a script can access environment variables - AllowEnv bool -} - // scriptEnv is the runtime environment for a particular script execution type scriptEnv struct { filename string - options Options } type scriptEnvKeyType struct{} diff --git a/internal/dynamo-browse/services/scriptmanager/service.go b/internal/dynamo-browse/services/scriptmanager/service.go index d90629f..8e17ae0 100644 --- a/internal/dynamo-browse/services/scriptmanager/service.go +++ b/internal/dynamo-browse/services/scriptmanager/service.go @@ -17,7 +17,6 @@ import ( type Service struct { lookupPaths []fs.FS ifaces Ifaces - options Options sched *scriptScheduler plugins []*ScriptPlugin } @@ -37,10 +36,6 @@ func (s *Service) SetLookupPaths(fs []fs.FS) { s.lookupPaths = fs } -func (s *Service) SetDefaultOptions(options Options) { - s.options = options -} - func (s *Service) SetIFaces(ifaces Ifaces) { s.ifaces = ifaces } @@ -94,13 +89,10 @@ func (s *Service) startAdHocScript(ctx context.Context, filename string, errChan return } - ctx = ctxWithScriptEnv(ctx, scriptEnv{filename: filepath.Base(filename), options: s.options}) + ctx = ctxWithScriptEnv(ctx, scriptEnv{filename: filepath.Base(filename)}) if _, err := risor.Eval(ctx, code, risor.WithGlobals(s.builtins()), - // risor.WithDefaultBuiltins(), - // risor.WithDefaultModules(), - // risor.WithBuiltins(s.builtins()), ); err != nil { errChan <- errors.Wrapf(err, "script %v", filename) return @@ -126,12 +118,9 @@ func (s *Service) loadScript(ctx context.Context, filename string, resChan chan scriptService: s, } - ctx = ctxWithScriptEnv(ctx, scriptEnv{filename: filepath.Base(filename), options: s.options}) + ctx = ctxWithScriptEnv(ctx, scriptEnv{filename: filepath.Base(filename)}) if _, err := risor.Eval(ctx, code, - // risor.WithDefaultBuiltins(), - // risor.WithDefaultModules(), - // risor.WithBuiltins(s.builtins()), risor.WithGlobals(s.builtins()), risor.WithGlobals(map[string]any{ "ext": (&extModule{scriptPlugin: newPlugin}).register(), @@ -251,7 +240,6 @@ func (s *Service) builtins() map[string]any { return map[string]any{ "ui": (&uiModule{uiService: s.ifaces.UI}).register(), "session": (&sessionModule{sessionService: s.ifaces.Session}).register(), - "os": (&osModule{}).register(), "print": object.NewBuiltin("print", printBuiltin), "printf": object.NewBuiltin("printf", printfBuiltin), } From 5d95d44a9719912fb7be89569375af16f7652045 Mon Sep 17 00:00:00 2001 From: Leon Mika Date: Sun, 3 Mar 2024 09:20:28 +1100 Subject: [PATCH 08/10] Added the rel-picker which can quickly goto related tables * New rel-picker that can be opened using Shift+O and allows for quickly going to related tables. --- cmd/dynamo-browse/main.go | 10 +- internal/dynamo-browse/controllers/events.go | 12 +- internal/dynamo-browse/controllers/iface.go | 8 +- internal/dynamo-browse/controllers/scripts.go | 53 +++++- .../dynamo-browse/controllers/tableread.go | 3 + .../controllers/tablewrite_test.go | 3 +- .../dynamo-browse/models/relitems/relitem.go | 12 ++ .../services/scriptmanager/builtins.go | 32 +++- .../services/scriptmanager/modext.go | 138 +++++++++++++++- .../services/scriptmanager/modext_test.go | 151 ++++++++++++++++++ .../services/scriptmanager/relitem.go | 57 +++++++ .../services/scriptmanager/resultsetproxy.go | 4 + .../services/scriptmanager/service.go | 6 +- .../services/scriptmanager/types.go | 9 +- .../dynamo-browse/ui/keybindings/defaults.go | 1 + .../ui/keybindings/keybindings.go | 1 + internal/dynamo-browse/ui/model.go | 19 ++- .../ui/teamodels/colselector/colmodel.go | 2 - .../ui/teamodels/relselector/itemmdl.go | 17 ++ .../ui/teamodels/relselector/listmdl.go | 132 +++++++++++++++ .../ui/teamodels/relselector/model.go | 73 +++++++++ .../ui/teamodels/utils/minmax.go | 7 + test.tm | 8 + 23 files changed, 730 insertions(+), 28 deletions(-) create mode 100644 internal/dynamo-browse/models/relitems/relitem.go create mode 100644 internal/dynamo-browse/services/scriptmanager/modext_test.go create mode 100644 internal/dynamo-browse/services/scriptmanager/relitem.go create mode 100644 internal/dynamo-browse/ui/teamodels/relselector/itemmdl.go create mode 100644 internal/dynamo-browse/ui/teamodels/relselector/listmdl.go create mode 100644 internal/dynamo-browse/ui/teamodels/relselector/model.go create mode 100644 test.tm diff --git a/cmd/dynamo-browse/main.go b/cmd/dynamo-browse/main.go index c059b3a..ccaf034 100644 --- a/cmd/dynamo-browse/main.go +++ b/cmd/dynamo-browse/main.go @@ -4,6 +4,10 @@ import ( "context" "flag" "fmt" + "log" + "net" + "os" + "github.com/aws/aws-sdk-go-v2/config" "github.com/aws/aws-sdk-go-v2/service/dynamodb" tea "github.com/charmbracelet/bubbletea" @@ -30,9 +34,6 @@ import ( "github.com/lmika/dynamo-browse/internal/dynamo-browse/ui/teamodels/styles" bus "github.com/lmika/events" "github.com/lmika/gopkgs/cli" - "log" - "net" - "os" ) func main() { @@ -118,6 +119,7 @@ func main() { inputHistoryService, eventBus, pasteboardProvider, + scriptManagerService, *flagTable, ) tableWriteController := controllers.NewTableWriteController(state, tableService, jobsController, tableReadController, settingStore) @@ -125,7 +127,7 @@ func main() { exportController := controllers.NewExportController(state, tableService, jobsController, columnsController, pasteboardProvider) settingsController := controllers.NewSettingsController(settingStore, eventBus) keyBindings := keybindings.Default() - scriptController := controllers.NewScriptController(scriptManagerService, tableReadController, settingsController, eventBus) + scriptController := controllers.NewScriptController(scriptManagerService, tableReadController, jobsController, settingsController, eventBus) if *flagQuery != "" { if *flagTable == "" { diff --git a/internal/dynamo-browse/controllers/events.go b/internal/dynamo-browse/controllers/events.go index 3d0821f..7af1c41 100644 --- a/internal/dynamo-browse/controllers/events.go +++ b/internal/dynamo-browse/controllers/events.go @@ -2,10 +2,12 @@ package controllers import ( "fmt" - tea "github.com/charmbracelet/bubbletea" - "github.com/lmika/dynamo-browse/internal/dynamo-browse/models" "strings" "time" + + tea "github.com/charmbracelet/bubbletea" + "github.com/lmika/dynamo-browse/internal/dynamo-browse/models" + "github.com/lmika/dynamo-browse/internal/dynamo-browse/models/relitems" ) type SetTableItemView struct { @@ -89,3 +91,9 @@ func (rs ResultSetUpdated) StatusMessage() string { type ShowColumnOverlay struct{} type HideColumnOverlay struct{} + +type ShowRelatedItemsOverlay struct { + Items []relitems.RelatedItem + OnSelected func(item relitems.RelatedItem) tea.Msg +} +type HideRelatedItemsOverlay struct{} diff --git a/internal/dynamo-browse/controllers/iface.go b/internal/dynamo-browse/controllers/iface.go index 101d190..7fa5239 100644 --- a/internal/dynamo-browse/controllers/iface.go +++ b/internal/dynamo-browse/controllers/iface.go @@ -2,10 +2,12 @@ package controllers import ( "context" + "io/fs" + "github.com/aws/aws-sdk-go-v2/service/dynamodb/types" tea "github.com/charmbracelet/bubbletea" "github.com/lmika/dynamo-browse/internal/dynamo-browse/models" - "io/fs" + "github.com/lmika/dynamo-browse/internal/dynamo-browse/models/relitems" ) type TableReadService interface { @@ -33,3 +35,7 @@ type CustomKeyBindingSource interface { UnbindKey(key string) Rebind(bindingName string, newKey string) error } + +type RelatedItemSupplier interface { + RelatedItemOfItem(context.Context, *models.ResultSet, int) ([]relitems.RelatedItem, error) +} diff --git a/internal/dynamo-browse/controllers/scripts.go b/internal/dynamo-browse/controllers/scripts.go index ccbf941..f706ddd 100644 --- a/internal/dynamo-browse/controllers/scripts.go +++ b/internal/dynamo-browse/controllers/scripts.go @@ -3,21 +3,24 @@ package controllers import ( "context" "fmt" + "log" + "strings" + tea "github.com/charmbracelet/bubbletea" "github.com/lmika/dynamo-browse/internal/common/ui/commandctrl" "github.com/lmika/dynamo-browse/internal/common/ui/events" "github.com/lmika/dynamo-browse/internal/dynamo-browse/models" "github.com/lmika/dynamo-browse/internal/dynamo-browse/models/queryexpr" + "github.com/lmika/dynamo-browse/internal/dynamo-browse/models/relitems" "github.com/lmika/dynamo-browse/internal/dynamo-browse/services/scriptmanager" bus "github.com/lmika/events" "github.com/pkg/errors" - "log" - "strings" ) type ScriptController struct { scriptManager *scriptmanager.Service tableReadController *TableReadController + jobController *JobsController settingsController *SettingsController eventBus *bus.Bus sendMsg func(msg tea.Msg) @@ -26,12 +29,14 @@ type ScriptController struct { func NewScriptController( scriptManager *scriptmanager.Service, tableReadController *TableReadController, + jobController *JobsController, settingsController *SettingsController, eventBus *bus.Bus, ) *ScriptController { sc := &ScriptController{ scriptManager: scriptManager, tableReadController: tableReadController, + jobController: jobController, settingsController: settingsController, eventBus: eventBus, } @@ -162,7 +167,6 @@ func (s *sessionImpl) SetResultSet(ctx context.Context, newResultSet *models.Res } func (s *sessionImpl) Query(ctx context.Context, query string, opts scriptmanager.QueryOptions) (*models.ResultSet, error) { - // Parse the query expr, err := queryexpr.Parse(query) if err != nil { @@ -179,11 +183,18 @@ func (s *sessionImpl) Query(ctx context.Context, query string, opts scriptmanage expr = expr.WithIndex(opts.IndexName) } + return s.sc.doQuery(ctx, expr, opts) +} + +func (s *ScriptController) doQuery(ctx context.Context, expr *queryexpr.QueryExpr, opts scriptmanager.QueryOptions) (*models.ResultSet, error) { // Get the table info - var tableInfo *models.TableInfo + var ( + tableInfo *models.TableInfo + err error + ) tableName := opts.TableName - currentResultSet := s.sc.tableReadController.state.ResultSet() + currentResultSet := s.tableReadController.state.ResultSet() if tableName != "" { // Table specified. If it's the same as the current table, then use the existing table info @@ -192,7 +203,7 @@ func (s *sessionImpl) Query(ctx context.Context, query string, opts scriptmanage } // Otherwise, describe the table - tableInfo, err = s.sc.tableReadController.tableService.Describe(ctx, tableName) + tableInfo, err = s.tableReadController.tableService.Describe(ctx, tableName) if err != nil { return nil, errors.Wrapf(err, "cannot describe table '%v'", tableName) } @@ -204,7 +215,7 @@ func (s *sessionImpl) Query(ctx context.Context, query string, opts scriptmanage tableInfo = currentResultSet.TableInfo } - newResultSet, err := s.sc.tableReadController.tableService.ScanOrQuery(ctx, tableInfo, expr, nil) + newResultSet, err := s.tableReadController.tableService.ScanOrQuery(ctx, tableInfo, expr, nil) if err != nil { return nil, err } @@ -240,3 +251,31 @@ func (sc *ScriptController) LookupBinding(theKey string) string { func (sc *ScriptController) UnbindKey(key string) { sc.scriptManager.UnbindKey(key) } + +func (c *ScriptController) LookupRelatedItems(idx int) (res tea.Msg) { + rs := c.tableReadController.state.ResultSet() + + relItems, err := c.scriptManager.RelatedItemOfItem(context.Background(), rs, idx) + if err != nil { + return events.Error(err) + } else if len(relItems) == 0 { + return events.StatusMsg("No related items available") + } + + return ShowRelatedItemsOverlay{ + Items: relItems, + OnSelected: func(item relitems.RelatedItem) tea.Msg { + if item.OnSelect != nil { + return item.OnSelect() + } + + return NewJob(c.jobController, "Running query…", func(ctx context.Context) (*models.ResultSet, error) { + return c.doQuery(ctx, item.Query, scriptmanager.QueryOptions{ + TableName: item.Table, + }) + }).OnDone(func(rs *models.ResultSet) tea.Msg { + return c.tableReadController.setResultSetAndFilter(rs, "", true, resultSetUpdateQuery) + }).Submit() + }, + } +} diff --git a/internal/dynamo-browse/controllers/tableread.go b/internal/dynamo-browse/controllers/tableread.go index f055e7d..bfc650d 100644 --- a/internal/dynamo-browse/controllers/tableread.go +++ b/internal/dynamo-browse/controllers/tableread.go @@ -60,6 +60,7 @@ type TableReadController struct { tableName string loadFromLastView bool pasteboardProvider services.PasteboardProvider + relatedItemSupplier RelatedItemSupplier // state mutex *sync.Mutex @@ -75,6 +76,7 @@ func NewTableReadController( inputHistoryService *inputhistory.Service, eventBus *bus.Bus, pasteboardProvider services.PasteboardProvider, + relatedItemSupplier RelatedItemSupplier, tableName string, ) *TableReadController { return &TableReadController{ @@ -87,6 +89,7 @@ func NewTableReadController( eventBus: eventBus, tableName: tableName, pasteboardProvider: pasteboardProvider, + relatedItemSupplier: relatedItemSupplier, mutex: new(sync.Mutex), } } diff --git a/internal/dynamo-browse/controllers/tablewrite_test.go b/internal/dynamo-browse/controllers/tablewrite_test.go index 493b5c7..ac2d32a 100644 --- a/internal/dynamo-browse/controllers/tablewrite_test.go +++ b/internal/dynamo-browse/controllers/tablewrite_test.go @@ -627,13 +627,14 @@ func newService(t *testing.T, cfg serviceConfig) *services { inputHistoryService, eventBus, pasteboardprovider.NilProvider{}, + nil, cfg.tableName, ) writeController := controllers.NewTableWriteController(state, service, jobsController, readController, settingStore) settingsController := controllers.NewSettingsController(settingStore, eventBus) columnsController := controllers.NewColumnsController(eventBus) exportController := controllers.NewExportController(state, service, jobsController, columnsController, pasteboardprovider.NilProvider{}) - scriptController := controllers.NewScriptController(scriptService, readController, settingsController, eventBus) + scriptController := controllers.NewScriptController(scriptService, readController, jobsController, settingsController, eventBus) commandController := commandctrl.NewCommandController(inputHistoryService) commandController.AddCommandLookupExtension(scriptController) diff --git a/internal/dynamo-browse/models/relitems/relitem.go b/internal/dynamo-browse/models/relitems/relitem.go new file mode 100644 index 0000000..3a46733 --- /dev/null +++ b/internal/dynamo-browse/models/relitems/relitem.go @@ -0,0 +1,12 @@ +package relitems + +import ( + "github.com/lmika/dynamo-browse/internal/dynamo-browse/models/queryexpr" +) + +type RelatedItem struct { + Name string + Table string + Query *queryexpr.QueryExpr + OnSelect func() error +} diff --git a/internal/dynamo-browse/services/scriptmanager/builtins.go b/internal/dynamo-browse/services/scriptmanager/builtins.go index 87e2852..93c6e78 100644 --- a/internal/dynamo-browse/services/scriptmanager/builtins.go +++ b/internal/dynamo-browse/services/scriptmanager/builtins.go @@ -8,8 +8,10 @@ package scriptmanager import ( "context" "fmt" - "github.com/risor-io/risor/object" "log" + + "github.com/pkg/errors" + "github.com/risor-io/risor/object" ) func printBuiltin(ctx context.Context, args ...object.Object) object.Object { @@ -70,3 +72,31 @@ func require(funcName string, count int, args []object.Object) *object.Error { } return nil } + +func bindArgs(funcName string, args []object.Object, bindArgs ...any) *object.Error { + if err := require(funcName, len(bindArgs), args); err != nil { + return err + } + + for i, bindArg := range bindArgs { + switch t := bindArg.(type) { + case *string: + str, err := object.AsString(args[i]) + if err != nil { + return err + } + + *t = str + case **object.Function: + fnRes, isFnRes := args[i].(*object.Function) + if !isFnRes { + return object.NewError(errors.Errorf("expected arg %v to be a function, was %T", i, bindArg)) + } + + *t = fnRes + default: + return object.NewError(errors.Errorf("unhandled arg type %v", i)) + } + } + return nil +} diff --git a/internal/dynamo-browse/services/scriptmanager/modext.go b/internal/dynamo-browse/services/scriptmanager/modext.go index 0c858cf..4ad97d4 100644 --- a/internal/dynamo-browse/services/scriptmanager/modext.go +++ b/internal/dynamo-browse/services/scriptmanager/modext.go @@ -3,9 +3,13 @@ package scriptmanager import ( "context" "fmt" + "regexp" + + "github.com/aws/aws-sdk-go-v2/service/dynamodb/types" + "github.com/lmika/dynamo-browse/internal/dynamo-browse/models" + "github.com/lmika/dynamo-browse/internal/dynamo-browse/models/queryexpr" "github.com/pkg/errors" "github.com/risor-io/risor/object" - "regexp" ) var ( @@ -18,8 +22,9 @@ type extModule struct { func (m *extModule) register() *object.Module { return object.NewBuiltinsModule("ext", map[string]object.Object{ - "command": object.NewBuiltin("command", m.command), - "key_binding": object.NewBuiltin("key_binding", m.keyBinding), + "command": object.NewBuiltin("command", m.command), + "key_binding": object.NewBuiltin("key_binding", m.keyBinding), + "related_items": object.NewBuiltin("related_items", m.relatedItem), }) } @@ -136,3 +141,130 @@ func (m *extModule) keyBinding(ctx context.Context, args ...object.Object) objec m.scriptPlugin.keyToKeyBinding[defaultKey] = fullBindingName return nil } + +func (m *extModule) relatedItem(ctx context.Context, args ...object.Object) object.Object { + thisEnv := scriptEnvFromCtx(ctx) + + var ( + tableName string + callbackFn *object.Function + ) + if err := bindArgs("ext.related_items", args, &tableName, &callbackFn); err != nil { + return err + } + + callFn, hasCallFn := object.GetCallFunc(ctx) + if !hasCallFn { + return object.NewError(errors.New("no callFn found in context")) + } + + newHandler := func(ctx context.Context, rs *models.ResultSet, index int) ([]relatedItem, error) { + newEnv := thisEnv + ctx = ctxWithScriptEnv(ctx, newEnv) + + res, err := callFn(ctx, callbackFn, []object.Object{ + newItemProxy(newResultSetProxy(rs), index), + }) + + if err != nil { + return nil, errors.Errorf("script error '%v':related_item - %v", m.scriptPlugin.name, err) + } else if object.IsError(res) { + errObj := res.(*object.Error) + return nil, errors.Errorf("script error '%v':related_item - %v", m.scriptPlugin.name, errObj.Inspect()) + } + + itr, objErr := object.AsIterator(res) + if err != nil { + return nil, objErr.Value() + } + + var relItems []relatedItem + for next, hasNext := itr.Next(ctx); hasNext; next, hasNext = itr.Next(ctx) { + var newRelItem relatedItem + + itemMap, objErr := object.AsMap(next) + if err != nil { + return nil, objErr.Value() + } + + labelName, objErr := object.AsString(itemMap.Get("label")) + if objErr != nil { + continue + } + newRelItem.label = labelName + + var tableStr = "" + if itemMap.Get("table") != object.Nil { + tableStr, objErr = object.AsString(itemMap.Get("table")) + if objErr != nil { + continue + } + } + newRelItem.table = tableStr + + if selectFn, ok := itemMap.Get("on_select").(*object.Function); ok { + newRelItem.onSelect = func() error { + thisNewEnv := thisEnv + ctx = ctxWithScriptEnv(ctx, thisNewEnv) + + res, err := callFn(ctx, selectFn, []object.Object{}) + if err != nil { + return errors.Errorf("rel error '%v' - %v", m.scriptPlugin.name, err) + } else if object.IsError(res) { + errObj := res.(*object.Error) + return errors.Errorf("rel error '%v' - %v", m.scriptPlugin.name, errObj.Inspect()) + } + return nil + } + } else { + queryExprStr, objErr := object.AsString(itemMap.Get("query")) + if objErr != nil { + continue + } + + query, err := queryexpr.Parse(queryExprStr) + if err != nil { + continue + } + + // Placeholders + if argsVal, isArgsValMap := object.AsMap(itemMap.Get("args")); isArgsValMap == nil { + namePlaceholders := make(map[string]string) + valuePlaceholders := make(map[string]types.AttributeValue) + + for k, val := range argsVal.Value() { + switch v := val.(type) { + case *object.String: + namePlaceholders[k] = v.Value() + valuePlaceholders[k] = &types.AttributeValueMemberS{Value: v.Value()} + case *object.Int: + valuePlaceholders[k] = &types.AttributeValueMemberN{Value: fmt.Sprint(v.Value())} + case *object.Float: + valuePlaceholders[k] = &types.AttributeValueMemberN{Value: fmt.Sprint(v.Value())} + case *object.Bool: + valuePlaceholders[k] = &types.AttributeValueMemberBOOL{Value: v.Value()} + case *object.NilType: + valuePlaceholders[k] = &types.AttributeValueMemberNULL{Value: true} + default: + continue + } + } + + query = query.WithNameParams(namePlaceholders).WithValueParams(valuePlaceholders) + } + newRelItem.query = query + } + + relItems = append(relItems, newRelItem) + } + + return relItems, nil + } + + m.scriptPlugin.relatedItems = append(m.scriptPlugin.relatedItems, &relatedItemBuilder{ + table: tableName, + itemProduction: newHandler, + }) + + return nil +} diff --git a/internal/dynamo-browse/services/scriptmanager/modext_test.go b/internal/dynamo-browse/services/scriptmanager/modext_test.go new file mode 100644 index 0000000..b3413e6 --- /dev/null +++ b/internal/dynamo-browse/services/scriptmanager/modext_test.go @@ -0,0 +1,151 @@ +package scriptmanager_test + +import ( + "context" + "testing" + + "github.com/aws/aws-sdk-go-v2/service/dynamodb/types" + "github.com/lmika/dynamo-browse/internal/dynamo-browse/models" + "github.com/lmika/dynamo-browse/internal/dynamo-browse/services/scriptmanager" + "github.com/stretchr/testify/assert" +) + +func TestExtModule_RelatedItems(t *testing.T) { + t.Run("should register a function which will return related items for an item", func(t *testing.T) { + scenarios := []struct { + desc string + code string + }{ + { + desc: "single function, table name match", + code: ` + ext.related_items("test-table", func(item) { + print("Hello") + return [ + {"label": "Customer", "query": "pk=$foo", "args": {"foo": "foo"}}, + {"label": "Payment", "query": "fla=$daa", "args": {"daa": "Hello"}}, + ] + }) + `, + }, + { + desc: "single function, table prefix match", + code: ` + ext.related_items("test-*", func(item) { + print("Hello") + return [ + {"label": "Customer", "query": "pk=$foo", "args": {"foo": "foo"}}, + {"label": "Payment", "query": "fla=$daa", "args": {"daa": "Hello"}}, + ] + }) + `, + }, + { + desc: "multi function, table name match", + code: ` + ext.related_items("test-table", func(item) { + print("Hello") + return [ + {"label": "Customer", "query": "pk=$foo", "args": {"foo": "foo"}}, + ] + }) + + ext.related_items("test-table", func(item) { + return [ + {"label": "Payment", "query": "fla=$daa", "args": {"daa": "Hello"}}, + ] + }) + `, + }, + { + desc: "multi function, table name prefix", + code: ` + ext.related_items("test-*", func(item) { + print("Hello") + return [ + {"label": "Customer", "query": "pk=$foo", "args": {"foo": "foo"}}, + ] + }) + + ext.related_items("test-*", func(item) { + return [ + {"label": "Payment", "query": "fla=$daa", "args": {"daa": "Hello"}}, + ] + }) + `, + }, + } + + for _, scenario := range scenarios { + t.Run(scenario.desc, func(t *testing.T) { + // Load the script + srv := scriptmanager.New(scriptmanager.WithFS(testScriptFile(t, "test.tm", scenario.code))) + + ctx := context.Background() + plugin, err := srv.LoadScript(ctx, "test.tm") + assert.NoError(t, err) + assert.NotNil(t, plugin) + + // Get related items of result set + rs := &models.ResultSet{ + TableInfo: &models.TableInfo{ + Name: "test-table", + }, + } + rs.SetItems([]models.Item{ + {"pk": &types.AttributeValueMemberS{Value: "abc"}}, + {"pk": &types.AttributeValueMemberS{Value: "1232"}}, + }) + + relItems, err := srv.RelatedItemOfItem(context.Background(), rs, 0) + assert.NoError(t, err) + assert.Len(t, relItems, 2) + + assert.Equal(t, "Customer", relItems[0].Name) + assert.Equal(t, "pk=$foo", relItems[0].Query.String()) + assert.Equal(t, "foo", relItems[0].Query.ValueParamOrNil("foo").(*types.AttributeValueMemberS).Value) + + assert.Equal(t, "Payment", relItems[1].Name) + assert.Equal(t, "fla=$daa", relItems[1].Query.String()) + assert.Equal(t, "Hello", relItems[1].Query.ValueParamOrNil("daa").(*types.AttributeValueMemberS).Value) + }) + } + }) + + t.Run("should support rel_items with on select", func(t *testing.T) { + // Load the script + srv := scriptmanager.New(scriptmanager.WithFS(testScriptFile(t, "test.tm", ` + ext.related_items("test-table", func(item) { + print("Hello") + return [ + {"label": "Customer", "on_select": func() { + print("Selected") + }}, + ] + }) + `))) + + ctx := context.Background() + plugin, err := srv.LoadScript(ctx, "test.tm") + assert.NoError(t, err) + assert.NotNil(t, plugin) + + // Get related items of result set + rs := &models.ResultSet{ + TableInfo: &models.TableInfo{ + Name: "test-table", + }, + } + rs.SetItems([]models.Item{ + {"pk": &types.AttributeValueMemberS{Value: "abc"}}, + {"pk": &types.AttributeValueMemberS{Value: "1232"}}, + }) + + relItems, err := srv.RelatedItemOfItem(context.Background(), rs, 0) + assert.NoError(t, err) + assert.Len(t, relItems, 1) + + assert.Equal(t, "Customer", relItems[0].Name) + assert.NoError(t, relItems[0].OnSelect()) + }) +} diff --git a/internal/dynamo-browse/services/scriptmanager/relitem.go b/internal/dynamo-browse/services/scriptmanager/relitem.go new file mode 100644 index 0000000..63d7629 --- /dev/null +++ b/internal/dynamo-browse/services/scriptmanager/relitem.go @@ -0,0 +1,57 @@ +package scriptmanager + +import ( + "context" + "log" + "path" + + "github.com/lmika/dynamo-browse/internal/dynamo-browse/models" + "github.com/lmika/dynamo-browse/internal/dynamo-browse/models/queryexpr" + "github.com/lmika/dynamo-browse/internal/dynamo-browse/models/relitems" +) + +type relatedItem struct { + label string + table string + query *queryexpr.QueryExpr + onSelect func() error +} + +type relatedItemBuilder struct { + table string + itemProduction func(ctx context.Context, rs *models.ResultSet, index int) ([]relatedItem, error) +} + +func (s *Service) RelatedItemOfItem(ctx context.Context, rs *models.ResultSet, index int) ([]relitems.RelatedItem, error) { + riModels := []relitems.RelatedItem{} + + for _, plugin := range s.plugins { + for _, rb := range plugin.relatedItems { + // TODO: should support matching + match, _ := tableMatchesGlob(rb.table, rs.TableInfo.Name) + log.Printf("RelatedItemOfItem: table = '%v', pattern = '%v', match = '%v'", rb.table, rs.TableInfo.Name, match) + if match { + relatedItems, err := rb.itemProduction(ctx, rs, index) + if err != nil { + // TODO: should probably return error if no rel items were found and an error was raised + return nil, err + } + + // TODO: make this nicer + for _, ri := range relatedItems { + riModels = append(riModels, relitems.RelatedItem{ + Name: ri.label, + Query: ri.query, + Table: ri.table, + OnSelect: ri.onSelect, + }) + } + } + } + } + return riModels, nil +} + +func tableMatchesGlob(tableName, pattern string) (bool, error) { + return path.Match(tableName, pattern) +} diff --git a/internal/dynamo-browse/services/scriptmanager/resultsetproxy.go b/internal/dynamo-browse/services/scriptmanager/resultsetproxy.go index 0d6be7d..953a3e4 100644 --- a/internal/dynamo-browse/services/scriptmanager/resultsetproxy.go +++ b/internal/dynamo-browse/services/scriptmanager/resultsetproxy.go @@ -17,6 +17,10 @@ type resultSetProxy struct { resultSet *models.ResultSet } +func newResultSetProxy(rs *models.ResultSet) *resultSetProxy { + return &resultSetProxy{resultSet: rs} +} + func (r *resultSetProxy) SetAttr(name string, value object.Object) error { return errors.Errorf("attribute error: %v", name) } diff --git a/internal/dynamo-browse/services/scriptmanager/service.go b/internal/dynamo-browse/services/scriptmanager/service.go index 8e17ae0..eac6638 100644 --- a/internal/dynamo-browse/services/scriptmanager/service.go +++ b/internal/dynamo-browse/services/scriptmanager/service.go @@ -14,6 +14,10 @@ import ( "github.com/risor-io/risor/object" ) +var ( + relPrefix = "." + string(filepath.Separator) +) + type Service struct { lookupPaths []fs.FS ifaces Ifaces @@ -150,7 +154,7 @@ func (s *Service) readScript(filename string, allowCwd bool) (string, error) { } } - if strings.HasPrefix(filename, string(filepath.Separator)) { + if strings.HasPrefix(filename, string(filepath.Separator)) || strings.HasPrefix(filename, relPrefix) { code, err := os.ReadFile(filename) if err != nil { return "", err diff --git a/internal/dynamo-browse/services/scriptmanager/types.go b/internal/dynamo-browse/services/scriptmanager/types.go index b0bf1a0..442f03b 100644 --- a/internal/dynamo-browse/services/scriptmanager/types.go +++ b/internal/dynamo-browse/services/scriptmanager/types.go @@ -1,6 +1,8 @@ package scriptmanager -import "context" +import ( + "context" +) type ScriptPlugin struct { scriptService *Service @@ -8,6 +10,7 @@ type ScriptPlugin struct { definedCommands map[string]*Command definedKeyBindings map[string]*Command keyToKeyBinding map[string]string + relatedItems []*relatedItemBuilder } func (sp *ScriptPlugin) Name() string { @@ -26,3 +29,7 @@ func (c *Command) Invoke(ctx context.Context, args []string, errChan chan error) errChan <- c.cmdFn(ctx, args) }) } + +//func (c *Command) LookupRelevantItems(ctx context.Context, table *models.TableInfo, item *models.Item) error { +// +//} diff --git a/internal/dynamo-browse/ui/keybindings/defaults.go b/internal/dynamo-browse/ui/keybindings/defaults.go index 87e2757..08a80e7 100644 --- a/internal/dynamo-browse/ui/keybindings/defaults.go +++ b/internal/dynamo-browse/ui/keybindings/defaults.go @@ -37,6 +37,7 @@ func Default() *KeyBindings { CycleLayoutBackwards: key.NewBinding(key.WithKeys("W"), key.WithHelp("W", "cycle layout backward")), PromptForCommand: key.NewBinding(key.WithKeys(":"), key.WithHelp(":", "prompt for command")), ShowColumnOverlay: key.NewBinding(key.WithKeys("f"), key.WithHelp("f", "show column overlay")), + ShowRelItemsOverlay: key.NewBinding(key.WithKeys("O"), key.WithHelp("O", "show related items overlay")), CancelRunningJob: key.NewBinding(key.WithKeys("ctrl+c"), key.WithHelp("ctrl+c", "cancel running job or quit")), Quit: key.NewBinding(key.WithKeys("esc"), key.WithHelp("esc", "quit")), }, diff --git a/internal/dynamo-browse/ui/keybindings/keybindings.go b/internal/dynamo-browse/ui/keybindings/keybindings.go index d8257de..d0c7fc3 100644 --- a/internal/dynamo-browse/ui/keybindings/keybindings.go +++ b/internal/dynamo-browse/ui/keybindings/keybindings.go @@ -45,6 +45,7 @@ type ViewKeyBindings struct { CycleLayoutBackwards key.Binding `keymap:"cycle-layout-backwards"` PromptForCommand key.Binding `keymap:"prompt-for-command"` ShowColumnOverlay key.Binding `keymap:"show-fields-popup"` + ShowRelItemsOverlay key.Binding `keymap:"show-rel-items-popup"` CancelRunningJob key.Binding `keymap:"cancel-running-job"` Quit key.Binding `keymap:"quit"` } diff --git a/internal/dynamo-browse/ui/model.go b/internal/dynamo-browse/ui/model.go index c41b572..cb07519 100644 --- a/internal/dynamo-browse/ui/model.go +++ b/internal/dynamo-browse/ui/model.go @@ -1,6 +1,10 @@ package ui import ( + "log" + "os" + "strings" + "github.com/charmbracelet/bubbles/key" tea "github.com/charmbracelet/bubbletea" "github.com/lmika/dynamo-browse/internal/common/ui/commandctrl" @@ -16,15 +20,13 @@ import ( "github.com/lmika/dynamo-browse/internal/dynamo-browse/ui/teamodels/dynamoitemview" "github.com/lmika/dynamo-browse/internal/dynamo-browse/ui/teamodels/dynamotableview" "github.com/lmika/dynamo-browse/internal/dynamo-browse/ui/teamodels/layout" + "github.com/lmika/dynamo-browse/internal/dynamo-browse/ui/teamodels/relselector" "github.com/lmika/dynamo-browse/internal/dynamo-browse/ui/teamodels/statusandprompt" "github.com/lmika/dynamo-browse/internal/dynamo-browse/ui/teamodels/styles" "github.com/lmika/dynamo-browse/internal/dynamo-browse/ui/teamodels/tableselect" "github.com/lmika/dynamo-browse/internal/dynamo-browse/ui/teamodels/utils" bus "github.com/lmika/events" "github.com/pkg/errors" - "log" - "os" - "strings" ) const ( @@ -48,6 +50,7 @@ type Model struct { scriptController *controllers.ScriptController jobController *controllers.JobsController colSelector *colselector.Model + relSelector *relselector.Model itemEdit *dynamoitemedit.Model statusAndPrompt *statusandprompt.StatusAndPrompt tableSelect *tableselect.Model @@ -85,7 +88,8 @@ func NewModel( mainView := layout.NewVBox(layout.LastChildFixedAt(14), dtv, div) colSelector := colselector.New(mainView, defaultKeyMap, columnsController) - itemEdit := dynamoitemedit.NewModel(colSelector) + relSelector := relselector.New(colSelector) + itemEdit := dynamoitemedit.NewModel(relSelector) statusAndPrompt := statusandprompt.New(itemEdit, pasteboardProvider, "", uiStyles.StatusAndPrompt) dialogPrompt := dialogprompt.New(statusAndPrompt) tableSelect := tableselect.New(dialogPrompt, uiStyles) @@ -244,6 +248,7 @@ func NewModel( jobController: jobController, itemEdit: itemEdit, colSelector: colSelector, + relSelector: relSelector, statusAndPrompt: statusAndPrompt, tableSelect: tableSelect, root: root, @@ -267,7 +272,7 @@ func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { ) case tea.KeyMsg: // TODO: use modes here - if !m.statusAndPrompt.InPrompt() && !m.tableSelect.Visible() && !m.colSelector.ColSelectorVisible() { + if !m.statusAndPrompt.InPrompt() && !m.tableSelect.Visible() && !m.colSelector.ColSelectorVisible() && !m.relSelector.SelectorVisible() { switch { case key.Matches(msg, m.keyMap.Mark): if idx := m.tableView.SelectedItemIndex(); idx >= 0 { @@ -302,6 +307,10 @@ func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { // return m, nil case key.Matches(msg, m.keyMap.ShowColumnOverlay): return m, events.SetTeaMessage(controllers.ShowColumnOverlay{}) + case key.Matches(msg, m.keyMap.ShowRelItemsOverlay): + if idx := m.tableView.SelectedItemIndex(); idx >= 0 { + return m, events.SetTeaMessage(m.scriptController.LookupRelatedItems(idx)) + } case key.Matches(msg, m.keyMap.PromptForCommand): return m, m.commandController.Prompt case key.Matches(msg, m.keyMap.PromptForTable): diff --git a/internal/dynamo-browse/ui/teamodels/colselector/colmodel.go b/internal/dynamo-browse/ui/teamodels/colselector/colmodel.go index a261032..f6221b0 100644 --- a/internal/dynamo-browse/ui/teamodels/colselector/colmodel.go +++ b/internal/dynamo-browse/ui/teamodels/colselector/colmodel.go @@ -10,7 +10,6 @@ import ( "github.com/lmika/dynamo-browse/internal/dynamo-browse/ui/keybindings" "github.com/lmika/dynamo-browse/internal/dynamo-browse/ui/teamodels/layout" table "github.com/lmika/go-bubble-table" - "log" "strings" ) @@ -49,7 +48,6 @@ func (m *colListModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) { switch msg := msg.(type) { case controllers.SetSelectedColumnInColSelector: // HACK: this needs to work for all cases - log.Printf("%d == %d?", int(msg), m.table.Cursor()+1) if int(msg) == m.table.Cursor()+1 { m.table.GoDown() } diff --git a/internal/dynamo-browse/ui/teamodels/relselector/itemmdl.go b/internal/dynamo-browse/ui/teamodels/relselector/itemmdl.go new file mode 100644 index 0000000..79372c7 --- /dev/null +++ b/internal/dynamo-browse/ui/teamodels/relselector/itemmdl.go @@ -0,0 +1,17 @@ +package relselector + +type relItemModel struct { + name string +} + +func (ti relItemModel) FilterValue() string { + return ti.name +} + +func (ti relItemModel) Title() string { + return ti.name +} + +func (ti relItemModel) Description() string { + return ti.name +} diff --git a/internal/dynamo-browse/ui/teamodels/relselector/listmdl.go b/internal/dynamo-browse/ui/teamodels/relselector/listmdl.go new file mode 100644 index 0000000..3bd601f --- /dev/null +++ b/internal/dynamo-browse/ui/teamodels/relselector/listmdl.go @@ -0,0 +1,132 @@ +package relselector + +import ( + "strings" + + "github.com/charmbracelet/bubbles/key" + "github.com/charmbracelet/bubbles/list" + tea "github.com/charmbracelet/bubbletea" + "github.com/charmbracelet/lipgloss" + "github.com/lmika/dynamo-browse/internal/common/sliceutils" + "github.com/lmika/dynamo-browse/internal/common/ui/events" + "github.com/lmika/dynamo-browse/internal/dynamo-browse/controllers" + "github.com/lmika/dynamo-browse/internal/dynamo-browse/models/relitems" + "github.com/lmika/dynamo-browse/internal/dynamo-browse/ui/teamodels/layout" + "github.com/lmika/dynamo-browse/internal/dynamo-browse/ui/teamodels/utils" +) + +var ( + frameColor = lipgloss.Color("63") + + frameStyle = lipgloss.NewStyle(). + Foreground(frameColor) + style = lipgloss.NewStyle(). + BorderStyle(lipgloss.NormalBorder()). + BorderForeground(frameColor) + + keyEsc = key.NewBinding(key.WithKeys(tea.KeyEsc.String())) + keyEnter = key.NewBinding(key.WithKeys(tea.KeyEnter.String())) +) + +type listModel struct { + event controllers.ShowRelatedItemsOverlay + list list.Model + height int +} + +func newListModel() *listModel { + items := []list.Item{} + + delegate := list.NewDefaultDelegate() + delegate.ShowDescription = false + delegate.Styles.SelectedTitle = lipgloss.NewStyle(). + Border(lipgloss.NormalBorder(), false, false, false, true). + BorderForeground(lipgloss.Color("#2c5fb7")). + Foreground(lipgloss.Color("#2c5fb7")). + Padding(0, 0, 0, 1) + delegate.Styles.SelectedDesc = lipgloss.NewStyle(). + Border(lipgloss.NormalBorder(), false, false, false, true). + BorderForeground(lipgloss.Color("#2c5fb7")). + Foreground(lipgloss.Color("#5277b7")). + Padding(0, 0, 0, 1) + + list := list.New(items, delegate, overlayWidth, overlayHeight-4) + list.KeyMap.CursorUp = key.NewBinding( + key.WithKeys("up", "i"), + key.WithHelp("↑/i", "up"), + ) + list.KeyMap.CursorDown = key.NewBinding( + key.WithKeys("down", "k"), + key.WithHelp("↓/k", "down"), + ) + list.KeyMap.PrevPage = key.NewBinding( + key.WithKeys("left", "j", "pgup", "b", "u"), + key.WithHelp("←/j/pgup", "prev page"), + ) + list.KeyMap.NextPage = key.NewBinding( + key.WithKeys("right", "l", "pgdown", "f", "d"), + key.WithHelp("→/l/pgdn", "next page"), + ) + list.SetShowTitle(false) + list.SetShowHelp(false) + //list.DisableQuitKeybindings() + + return &listModel{ + list: list, + } +} + +func (m *listModel) Init() tea.Cmd { + return nil +} + +func (m *listModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) { + var cc utils.CmdCollector + + switch msg := msg.(type) { + case tea.KeyMsg: + switch { + case key.Matches(msg, keyEnter): + if onSel := m.event.OnSelected; onSel != nil { + cc.Add(events.SetTeaMessage(onSel(m.event.Items[m.list.Index()]))) + } + return m, events.SetTeaMessage(controllers.HideColumnOverlay{}) + case key.Matches(msg, keyEsc): + return m, events.SetTeaMessage(controllers.HideColumnOverlay{}) + default: + m.list = cc.Collect(m.list.Update(msg)).(list.Model) + } + default: + m.list = cc.Collect(m.list.Update(msg)).(list.Model) + } + return m, cc.Cmd() +} + +func (m *listModel) View() string { + innerView := lipgloss.JoinVertical( + lipgloss.Top, + lipgloss.PlaceHorizontal(overlayWidth-2, lipgloss.Center, "Related Items"), + frameStyle.Render(strings.Repeat(lipgloss.NormalBorder().Top, overlayWidth-2)), + m.list.View(), + ) + + view := style.Width(overlayWidth - 2).Height(m.height - 2).Render(innerView) + + return view +} + +func (m *listModel) Resize(w, h int) layout.ResizingModel { + return m +} + +func (m *listModel) setItems(event controllers.ShowRelatedItemsOverlay, newHeight int) { + listItems := sliceutils.Map(event.Items, func(item relitems.RelatedItem) list.Item { + return relItemModel{name: item.Name} + }) + m.event = event + m.list.SetItems(listItems) + m.list.Select(0) + m.list.SetHeight(newHeight - 4) + + m.height = newHeight +} diff --git a/internal/dynamo-browse/ui/teamodels/relselector/model.go b/internal/dynamo-browse/ui/teamodels/relselector/model.go new file mode 100644 index 0000000..45658ec --- /dev/null +++ b/internal/dynamo-browse/ui/teamodels/relselector/model.go @@ -0,0 +1,73 @@ +package relselector + +import ( + tea "github.com/charmbracelet/bubbletea" + "github.com/lmika/dynamo-browse/internal/dynamo-browse/controllers" + "github.com/lmika/dynamo-browse/internal/dynamo-browse/ui/teamodels/layout" + "github.com/lmika/dynamo-browse/internal/dynamo-browse/ui/teamodels/utils" +) + +const ( + overlayWidth = 50 + + overlayHeight = 8 + overlayHeightExtra2 = 2 + maxItems = 8 +) + +type Model struct { + subModel tea.Model + compositor *layout.Compositor + listModel *listModel + w, h int +} + +func New(subModel tea.Model) *Model { + compositor := layout.NewCompositor(subModel) + listModel := newListModel() + + return &Model{ + subModel: subModel, + listModel: listModel, + compositor: compositor, + } +} + +func (m *Model) Init() tea.Cmd { + return m.compositor.Init() +} + +func (m *Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { + var cc utils.CmdCollector + switch msg := msg.(type) { + case controllers.ShowRelatedItemsOverlay: + newHeight := overlayHeight + utils.Min(len(msg.Items), maxItems)*overlayHeightExtra2 + + m.listModel.setItems(msg, newHeight) + m.compositor.SetOverlay(m.listModel, m.w/2-overlayWidth/2, m.h/2-newHeight/2, overlayWidth, newHeight) + case controllers.HideColumnOverlay: + m.compositor.ClearOverlay() + case tea.KeyMsg: + m.compositor = cc.Collect(m.compositor.Update(msg)).(*layout.Compositor) + default: + m.subModel = cc.Collect(m.subModel.Update(msg)).(tea.Model) + } + return m, cc.Cmd() +} + +func (m *Model) View() string { + return m.compositor.View() +} + +func (m *Model) Resize(w, h int) layout.ResizingModel { + m.w, m.h = w, h + m.compositor.MoveOverlay(m.w/2-overlayWidth/2, m.h/2-overlayHeight/2) + m.listModel.Resize(w, h) + m.subModel = layout.Resize(m.subModel, w, h) + m.listModel = layout.Resize(m.listModel, w, h).(*listModel) + return m +} + +func (m *Model) SelectorVisible() bool { + return m.compositor.HasOverlay() +} diff --git a/internal/dynamo-browse/ui/teamodels/utils/minmax.go b/internal/dynamo-browse/ui/teamodels/utils/minmax.go index 2fbb42f..17ffd5e 100644 --- a/internal/dynamo-browse/ui/teamodels/utils/minmax.go +++ b/internal/dynamo-browse/ui/teamodels/utils/minmax.go @@ -1,5 +1,12 @@ package utils +func Min(x, y int) int { + if x < y { + return x + } + return y +} + func Max(x, y int) int { if x > y { return x diff --git a/test.tm b/test.tm new file mode 100644 index 0000000..1fa251d --- /dev/null +++ b/test.tm @@ -0,0 +1,8 @@ +ext.related_items("business-addresses", func(item) { + print("Hello") + return [ + {"label": "Customer", "query": `city="Austin"`, "args": {"foo": "foo"}}, + {"label": "Payment", "query": `officeOpened=false`, "args": {"daa": "Hello"}}, + {"label": "Thing", "query": `colors.door^="P"`, "args": {"daa": "Hello"}}, + ] +}) \ No newline at end of file From f5bf31a903f379c85fb960ba66bf5a2737822b37 Mon Sep 17 00:00:00 2001 From: Leon Mika Date: Sun, 3 Mar 2024 09:48:03 +1100 Subject: [PATCH 09/10] Fixed release CI/CD and added 'C' as copy to table binding --- .github/workflows/ci.yaml | 2 +- .github/workflows/release.yaml | 10 +++++----- internal/dynamo-browse/ui/keybindings/defaults.go | 1 + macos.goreleaser.yml | 2 +- 4 files changed, 8 insertions(+), 7 deletions(-) diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index de99ca1..580bbae 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -24,7 +24,7 @@ jobs: - name: Setup Go uses: actions/setup-go@v3 with: - go-version: 1.21 + go-version: 1.22 - name: Configure run: | git config --global url."https://${{ secrets.GO_MODULES_TOKEN }}:x-oauth-basic@github.com/lmika".insteadOf "https://github.com/lmika" diff --git a/.github/workflows/release.yaml b/.github/workflows/release.yaml index b880db3..870d41f 100644 --- a/.github/workflows/release.yaml +++ b/.github/workflows/release.yaml @@ -20,7 +20,7 @@ jobs: - name: Setup Go uses: actions/setup-go@v3 with: - go-version: 1.21 + go-version: 1.22 - name: Configure run: | git config --global url."https://${{ secrets.GO_MODULES_TOKEN }}:x-oauth-basic@github.com/lmika".insteadOf "https://github.com/lmika" @@ -41,7 +41,7 @@ jobs: - name: Setup Go uses: actions/setup-go@v3 with: - go-version: 1.18 + go-version: 1.22 - name: Configure run: | git config --global url."https://${{ secrets.GO_MODULES_TOKEN }}:x-oauth-basic@github.com/lmika".insteadOf "https://github.com/lmika" @@ -52,7 +52,7 @@ jobs: - name: Release if: startsWith(github.ref, 'refs/tags/') run: | - goreleaser release -f macos.goreleaser.yml --skip-validate --rm-dist + goreleaser release -f macos.goreleaser.yml --skip=validate --clean env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} HOMEBREW_GITHUB_TOKEN: ${{ secrets.HOMEBREW_GITHUB_TOKEN }} @@ -66,7 +66,7 @@ jobs: - name: Setup Go uses: actions/setup-go@v3 with: - go-version: 1.18 + go-version: 1.22 - name: Configure run: | git config --global url."https://${{ secrets.GO_MODULES_TOKEN }}:x-oauth-basic@github.com/lmika".insteadOf "https://github.com/lmika" @@ -75,7 +75,7 @@ jobs: if: startsWith(github.ref, 'refs/tags/') with: version: latest - args: release -f linux.goreleaser.yml --skip-validate --rm-dist + args: release -f linux.goreleaser.yml --skip=validate --clean env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} HOMEBREW_GITHUB_TOKEN: ${{ secrets.HOMEBREW_GITHUB_TOKEN }} \ No newline at end of file diff --git a/internal/dynamo-browse/ui/keybindings/defaults.go b/internal/dynamo-browse/ui/keybindings/defaults.go index 08a80e7..cc49dd1 100644 --- a/internal/dynamo-browse/ui/keybindings/defaults.go +++ b/internal/dynamo-browse/ui/keybindings/defaults.go @@ -27,6 +27,7 @@ func Default() *KeyBindings { Mark: key.NewBinding(key.WithKeys("m"), key.WithHelp("m", "mark")), ToggleMarkedItems: key.NewBinding(key.WithKeys("M"), key.WithHelp("M", "toggle marged items")), CopyItemToClipboard: key.NewBinding(key.WithKeys("c"), key.WithHelp("c", "copy item to clipboard")), + CopyTableToClipboard: key.NewBinding(key.WithKeys("C"), key.WithHelp("C", "copy table to clipboard")), Rescan: key.NewBinding(key.WithKeys("R"), key.WithHelp("R", "rescan")), PromptForQuery: key.NewBinding(key.WithKeys("?"), key.WithHelp("?", "prompt for query")), PromptForFilter: key.NewBinding(key.WithKeys("/"), key.WithHelp("/", "filter")), diff --git a/macos.goreleaser.yml b/macos.goreleaser.yml index cf3bd2e..e124d05 100644 --- a/macos.goreleaser.yml +++ b/macos.goreleaser.yml @@ -17,7 +17,7 @@ archives: format: tar.gz brews: - name: audax - tap: + repository: owner: lmika name: homebrew-audax token: "{{ .Env.HOMEBREW_GITHUB_TOKEN }}" From e37b8099a38bb37f3eca45d6bc6ac7dfaacd2521 Mon Sep 17 00:00:00 2001 From: Leon Mika Date: Tue, 2 Apr 2024 23:00:19 +1100 Subject: [PATCH 10/10] Fixed a glaring error where the user cannot close the column selector Cause of this was that the close event type was also being used by the related overlay, and the event was being caught by that even though the overlay was hidden. Also started working on changing the sort order within the column selector by pressing S. --- cmd/dynamo-browse/main.go | 2 +- internal/dynamo-browse/controllers/columns.go | 40 ++++++++- .../dynamo-browse/controllers/tableread.go | 8 ++ .../controllers/tablewrite_test.go | 2 +- .../dynamo-browse/models/columns/columns.go | 29 +------ internal/dynamo-browse/models/evaluators.go | 15 ++++ .../dynamo-browse/models/evaluators/equals.go | 25 ++++++ internal/dynamo-browse/models/models.go | 12 ++- .../models/queryexpr/expr_test.go | 6 ++ .../models/queryexpr/fieldevaluator.go | 15 ++++ internal/dynamo-browse/models/sorted.go | 84 ++++++++++++++----- internal/dynamo-browse/models/sorted_test.go | 6 +- .../dynamo-browse/services/tables/service.go | 3 +- .../dynamo-browse/ui/keybindings/defaults.go | 1 + .../ui/keybindings/keybindings.go | 1 + .../ui/teamodels/colselector/colmodel.go | 9 +- .../ui/teamodels/colselector/model.go | 1 + .../ui/teamodels/colselector/tblmodel.go | 15 +++- .../ui/teamodels/relselector/listmdl.go | 4 +- .../ui/teamodels/relselector/model.go | 2 +- 20 files changed, 213 insertions(+), 67 deletions(-) create mode 100644 internal/dynamo-browse/models/evaluators.go create mode 100644 internal/dynamo-browse/models/evaluators/equals.go create mode 100644 internal/dynamo-browse/models/queryexpr/fieldevaluator.go diff --git a/cmd/dynamo-browse/main.go b/cmd/dynamo-browse/main.go index ccaf034..8adcce4 100644 --- a/cmd/dynamo-browse/main.go +++ b/cmd/dynamo-browse/main.go @@ -123,7 +123,7 @@ func main() { *flagTable, ) tableWriteController := controllers.NewTableWriteController(state, tableService, jobsController, tableReadController, settingStore) - columnsController := controllers.NewColumnsController(eventBus) + columnsController := controllers.NewColumnsController(tableReadController, eventBus) exportController := controllers.NewExportController(state, tableService, jobsController, columnsController, pasteboardProvider) settingsController := controllers.NewSettingsController(settingStore, eventBus) keyBindings := keybindings.Default() diff --git a/internal/dynamo-browse/controllers/columns.go b/internal/dynamo-browse/controllers/columns.go index 8080477..23dbd35 100644 --- a/internal/dynamo-browse/controllers/columns.go +++ b/internal/dynamo-browse/controllers/columns.go @@ -5,19 +5,22 @@ import ( "github.com/lmika/dynamo-browse/internal/common/ui/events" "github.com/lmika/dynamo-browse/internal/dynamo-browse/models" "github.com/lmika/dynamo-browse/internal/dynamo-browse/models/columns" + "github.com/lmika/dynamo-browse/internal/dynamo-browse/models/evaluators" "github.com/lmika/dynamo-browse/internal/dynamo-browse/models/queryexpr" bus "github.com/lmika/events" "strings" ) type ColumnsController struct { + tr *TableReadController + // State colModel *columns.Columns resultSet *models.ResultSet } -func NewColumnsController(eventBus *bus.Bus) *ColumnsController { - cc := &ColumnsController{} +func NewColumnsController(tr *TableReadController, eventBus *bus.Bus) *ColumnsController { + cc := &ColumnsController{tr: tr} eventBus.On(newResultSetEvent, cc.onNewResultSet) return cc @@ -80,7 +83,7 @@ func (cc *ColumnsController) AddColumn(afterIndex int) tea.Msg { newCol := columns.Column{ Name: colExpr.String(), - Evaluator: columns.ExprFieldValueEvaluator{Expr: colExpr}, + Evaluator: queryexpr.ExprFieldValueEvaluator{Expr: colExpr}, } if afterIndex >= len(cc.colModel.Columns)-1 { @@ -117,6 +120,25 @@ func (cc *ColumnsController) DeleteColumn(afterIndex int) tea.Msg { return ColumnsUpdated{} } +func (cc *ColumnsController) SortByColumn(index int) tea.Msg { + if index >= len(cc.colModel.Columns) { + return nil + } + + column := cc.colModel.Columns[index] + newCriteria := models.SortCriteria{ + Fields: []models.SortField{ + {Field: column.Evaluator, Asc: true}, + }, + } + if ff := cc.SortCriteria().FirstField(); evaluators.Equals(ff.Field, column.Evaluator) { + newCriteria.Fields[0].Asc = !ff.Asc + } + + cc.SetSortCriteria(newCriteria) + return ColumnsUpdated{} +} + func (c *ColumnsController) AttributesWithPrefix(prefix string) []string { options := make([]string, 0) for _, col := range c.resultSet.Columns() { @@ -126,3 +148,15 @@ func (c *ColumnsController) AttributesWithPrefix(prefix string) []string { } return options } + +func (cc *ColumnsController) SortCriteria() models.SortCriteria { + if cc.resultSet == nil { + return models.SortCriteria{} + } + + return cc.resultSet.SortCriteria() +} + +func (cc *ColumnsController) SetSortCriteria(criteria models.SortCriteria) { + cc.tr.SortResultSet(criteria) +} diff --git a/internal/dynamo-browse/controllers/tableread.go b/internal/dynamo-browse/controllers/tableread.go index bfc650d..6ef6e18 100644 --- a/internal/dynamo-browse/controllers/tableread.go +++ b/internal/dynamo-browse/controllers/tableread.go @@ -35,6 +35,7 @@ const ( resultSetUpdateTouch resultSetUpdateNextPage resultSetUpdateScript + resultSetUpdateResort ) type MarkOp int @@ -150,6 +151,13 @@ func (c *TableReadController) ScanTable(name string) tea.Msg { }).OnEither(c.handleResultSetFromJobResult(c.state.Filter(), true, false, resultSetUpdateInit)).Submit() } +func (c *TableReadController) SortResultSet(newCriteria models.SortCriteria) { + c.state.withResultSet(func(rs *models.ResultSet) { + rs.Sort(newCriteria.Append(models.PKSKSortFilter(rs.TableInfo))) + }) + c.eventBus.Fire(newResultSetEvent, c.state.resultSet, resultSetUpdateResort) +} + func (c *TableReadController) PromptForQuery() tea.Msg { return events.PromptForInputMsg{ Prompt: "query: ", diff --git a/internal/dynamo-browse/controllers/tablewrite_test.go b/internal/dynamo-browse/controllers/tablewrite_test.go index ac2d32a..efd0c14 100644 --- a/internal/dynamo-browse/controllers/tablewrite_test.go +++ b/internal/dynamo-browse/controllers/tablewrite_test.go @@ -632,7 +632,7 @@ func newService(t *testing.T, cfg serviceConfig) *services { ) writeController := controllers.NewTableWriteController(state, service, jobsController, readController, settingStore) settingsController := controllers.NewSettingsController(settingStore, eventBus) - columnsController := controllers.NewColumnsController(eventBus) + columnsController := controllers.NewColumnsController(readController, eventBus) exportController := controllers.NewExportController(state, service, jobsController, columnsController, pasteboardprovider.NilProvider{}) scriptController := controllers.NewScriptController(scriptService, readController, jobsController, settingsController, eventBus) diff --git a/internal/dynamo-browse/models/columns/columns.go b/internal/dynamo-browse/models/columns/columns.go index bdf290d..3342a95 100644 --- a/internal/dynamo-browse/models/columns/columns.go +++ b/internal/dynamo-browse/models/columns/columns.go @@ -1,9 +1,7 @@ package columns import ( - "github.com/aws/aws-sdk-go-v2/service/dynamodb/types" "github.com/lmika/dynamo-browse/internal/dynamo-browse/models" - "github.com/lmika/dynamo-browse/internal/dynamo-browse/models/queryexpr" ) type Columns struct { @@ -19,7 +17,7 @@ func NewColumnsFromResultSet(rs *models.ResultSet) *Columns { for i, c := range rsCols { cols[i] = Column{ Name: c, - Evaluator: SimpleFieldValueEvaluator(c), + Evaluator: models.SimpleFieldValueEvaluator(c), } } @@ -44,7 +42,7 @@ func (cols *Columns) AddMissingColumns(rs *models.ResultSet) { if _, hasCol := existingColumns[c]; !hasCol { newCols = append(newCols, Column{ Name: c, - Evaluator: SimpleFieldValueEvaluator(c), + Evaluator: models.SimpleFieldValueEvaluator(c), }) } } @@ -56,7 +54,7 @@ func (cols *Columns) AddMissingColumns(rs *models.ResultSet) { } else { newCols[i] = Column{ Name: c, - Evaluator: SimpleFieldValueEvaluator(c), + Evaluator: models.SimpleFieldValueEvaluator(c), } } } @@ -82,25 +80,6 @@ func (cols *Columns) VisibleColumns() []Column { type Column struct { Name string - Evaluator FieldValueEvaluator + Evaluator models.FieldValueEvaluator Hidden bool } - -type FieldValueEvaluator interface { - EvaluateForItem(item models.Item) types.AttributeValue -} - -type SimpleFieldValueEvaluator string - -func (sfve SimpleFieldValueEvaluator) EvaluateForItem(item models.Item) types.AttributeValue { - return item[string(sfve)] -} - -type ExprFieldValueEvaluator struct { - Expr *queryexpr.QueryExpr -} - -func (sfve ExprFieldValueEvaluator) EvaluateForItem(item models.Item) types.AttributeValue { - val, _ := sfve.Expr.EvalItem(item) - return val -} diff --git a/internal/dynamo-browse/models/evaluators.go b/internal/dynamo-browse/models/evaluators.go new file mode 100644 index 0000000..85ffa26 --- /dev/null +++ b/internal/dynamo-browse/models/evaluators.go @@ -0,0 +1,15 @@ +package models + +import ( + "github.com/aws/aws-sdk-go-v2/service/dynamodb/types" +) + +type FieldValueEvaluator interface { + EvaluateForItem(item Item) types.AttributeValue +} + +type SimpleFieldValueEvaluator string + +func (sfve SimpleFieldValueEvaluator) EvaluateForItem(item Item) types.AttributeValue { + return item[string(sfve)] +} diff --git a/internal/dynamo-browse/models/evaluators/equals.go b/internal/dynamo-browse/models/evaluators/equals.go new file mode 100644 index 0000000..ec1f0dc --- /dev/null +++ b/internal/dynamo-browse/models/evaluators/equals.go @@ -0,0 +1,25 @@ +package evaluators + +import ( + "github.com/lmika/dynamo-browse/internal/dynamo-browse/models" + "github.com/lmika/dynamo-browse/internal/dynamo-browse/models/queryexpr" +) + +func Equals(x, y models.FieldValueEvaluator) bool { + if x == nil { + return y == nil + } + + switch xt := x.(type) { + case models.SimpleFieldValueEvaluator: + if yt, ok := y.(models.SimpleFieldValueEvaluator); ok { + return xt == yt + } + case queryexpr.ExprFieldValueEvaluator: + if yt, ok := y.(queryexpr.ExprFieldValueEvaluator); ok { + return xt.Expr.Equal(yt.Expr) + } + } + + return false +} diff --git a/internal/dynamo-browse/models/models.go b/internal/dynamo-browse/models/models.go index 0aed4f1..03d0421 100644 --- a/internal/dynamo-browse/models/models.go +++ b/internal/dynamo-browse/models/models.go @@ -18,7 +18,8 @@ type ResultSet struct { items []Item attributes []ItemAttribute - columns []string + columns []string + sortCriteria SortCriteria } type Queryable interface { @@ -48,6 +49,10 @@ func (rs *ResultSet) SetItems(items []Item) { rs.attributes = make([]ItemAttribute, len(items)) } +func (rs *ResultSet) SortCriteria() SortCriteria { + return rs.sortCriteria +} + func (rs *ResultSet) AddNewItem(item Item, attrs ItemAttribute) { rs.items = append(rs.items, item) rs.attributes = append(rs.attributes, attrs) @@ -141,3 +146,8 @@ func (rs *ResultSet) RefreshColumns() { func (rs *ResultSet) HasNextPage() bool { return rs.LastEvaluatedKey != nil } + +func (rs *ResultSet) Sort(criteria SortCriteria) { + rs.sortCriteria = criteria + Sort(rs.items, criteria) +} diff --git a/internal/dynamo-browse/models/queryexpr/expr_test.go b/internal/dynamo-browse/models/queryexpr/expr_test.go index 76e7a02..2352329 100644 --- a/internal/dynamo-browse/models/queryexpr/expr_test.go +++ b/internal/dynamo-browse/models/queryexpr/expr_test.go @@ -53,6 +53,11 @@ func TestModExpr_Query(t *testing.T) { `#0 = :0`, exprNameIsString(0, 0, "pk", "prefix"), ), + //scanCase("when request pk is fixed (reverse)", + // `prefix="pk"`, + // `#0 = :0`, + // exprNameIsString(0, 0, "pk", "prefix"), + //), scanCase("when request pk is fixed in parens #1", `(pk="prefix")`, `#0 = :0`, @@ -513,6 +518,7 @@ func TestQueryExpr_EvalItem(t *testing.T) { {expr: "three <= 2", expected: &types.AttributeValueMemberBOOL{Value: false}}, // Between + {expr: "3 between 1 and 5", expected: &types.AttributeValueMemberBOOL{Value: true}}, {expr: "three between 1 and 5", expected: &types.AttributeValueMemberBOOL{Value: true}}, {expr: "three between one and five", expected: &types.AttributeValueMemberBOOL{Value: true}}, {expr: "three between 10 and 15", expected: &types.AttributeValueMemberBOOL{Value: false}}, diff --git a/internal/dynamo-browse/models/queryexpr/fieldevaluator.go b/internal/dynamo-browse/models/queryexpr/fieldevaluator.go new file mode 100644 index 0000000..616e339 --- /dev/null +++ b/internal/dynamo-browse/models/queryexpr/fieldevaluator.go @@ -0,0 +1,15 @@ +package queryexpr + +import ( + "github.com/aws/aws-sdk-go-v2/service/dynamodb/types" + "github.com/lmika/dynamo-browse/internal/dynamo-browse/models" +) + +type ExprFieldValueEvaluator struct { + Expr *QueryExpr +} + +func (sfve ExprFieldValueEvaluator) EvaluateForItem(item models.Item) types.AttributeValue { + val, _ := sfve.Expr.EvalItem(item) + return val +} diff --git a/internal/dynamo-browse/models/sorted.go b/internal/dynamo-browse/models/sorted.go index dd8e200..29c5e49 100644 --- a/internal/dynamo-browse/models/sorted.go +++ b/internal/dynamo-browse/models/sorted.go @@ -8,13 +8,60 @@ import ( // sortedItems is a collection of items that is sorted. // Items are sorted based on the PK, and SK in ascending order type sortedItems struct { - tableInfo *TableInfo - items []Item + criteria SortCriteria + items []Item +} + +type SortField struct { + Field FieldValueEvaluator + Asc bool +} + +type SortCriteria struct { + Fields []SortField +} + +func (sc SortCriteria) FirstField() SortField { + if len(sc.Fields) == 0 { + return SortField{} + } + return sc.Fields[0] +} + +func (sc SortCriteria) Equals(osc SortCriteria) bool { + if len(sc.Fields) != len(osc.Fields) { + return false + } + + for i := range osc.Fields { + if sc.Fields[i].Field != osc.Fields[i].Field || + sc.Fields[i].Asc != osc.Fields[i].Asc { + return false + } + } + + return true +} + +func (sc SortCriteria) Append(osc SortCriteria) SortCriteria { + newItems := make([]SortField, 0, len(osc.Fields)) + newItems = append(newItems, sc.Fields...) + newItems = append(newItems, osc.Fields...) + return SortCriteria{Fields: newItems} +} + +func PKSKSortFilter(ti *TableInfo) SortCriteria { + return SortCriteria{ + Fields: []SortField{ + {Field: SimpleFieldValueEvaluator(ti.Keys.PartitionKey), Asc: true}, + {Field: SimpleFieldValueEvaluator(ti.Keys.SortKey), Asc: true}, + }, + } } // Sort sorts the items in place -func Sort(items []Item, tableInfo *TableInfo) { - si := sortedItems{items: items, tableInfo: tableInfo} +func Sort(items []Item, criteria SortCriteria) { + si := sortedItems{items: items, criteria: criteria} sort.Sort(&si) } @@ -23,30 +70,21 @@ func (si *sortedItems) Len() int { } func (si *sortedItems) Less(i, j int) bool { - // Compare primary keys - pv1, pv2 := si.items[i][si.tableInfo.Keys.PartitionKey], si.items[j][si.tableInfo.Keys.PartitionKey] - pc, ok := attrutils.CompareScalarAttributes(pv1, pv2) - if !ok { - return i < j - } - - if pc < 0 { - return true - } else if pc > 0 { - return false - } - - // Partition keys are equal, compare sort key - if sortKey := si.tableInfo.Keys.SortKey; sortKey != "" { - sv1, sv2 := si.items[i][sortKey], si.items[j][sortKey] - sc, ok := attrutils.CompareScalarAttributes(sv1, sv2) + for _, field := range si.criteria.Fields { + // Compare primary keys + pv1, pv2 := field.Field.EvaluateForItem(si.items[i]), field.Field.EvaluateForItem(si.items[j]) + pc, ok := attrutils.CompareScalarAttributes(pv1, pv2) if !ok { return i < j } - if sc < 0 { + if !field.Asc { + pc = -pc + } + + if pc < 0 { return true - } else if sc > 0 { + } else if pc > 0 { return false } } diff --git a/internal/dynamo-browse/models/sorted_test.go b/internal/dynamo-browse/models/sorted_test.go index 5cb1fc6..5b39d6b 100644 --- a/internal/dynamo-browse/models/sorted_test.go +++ b/internal/dynamo-browse/models/sorted_test.go @@ -15,7 +15,7 @@ func TestSort(t *testing.T) { items := make([]models.Item, len(testStringData)) copy(items, testStringData) - models.Sort(items, tableInfo) + models.Sort(items, models.PKSKSortFilter(tableInfo)) assert.Equal(t, items[0], testStringData[1]) assert.Equal(t, items[1], testStringData[2]) @@ -28,7 +28,7 @@ func TestSort(t *testing.T) { items := make([]models.Item, len(testNumberData)) copy(items, testNumberData) - models.Sort(items, tableInfo) + models.Sort(items, models.PKSKSortFilter(tableInfo)) assert.Equal(t, items[0], testNumberData[2]) assert.Equal(t, items[1], testNumberData[1]) @@ -41,7 +41,7 @@ func TestSort(t *testing.T) { items := make([]models.Item, len(testBoolData)) copy(items, testBoolData) - models.Sort(items, tableInfo) + models.Sort(items, models.PKSKSortFilter(tableInfo)) assert.Equal(t, items[0], testBoolData[2]) assert.Equal(t, items[1], testBoolData[1]) diff --git a/internal/dynamo-browse/services/tables/service.go b/internal/dynamo-browse/services/tables/service.go index 993b4be..52f97bb 100644 --- a/internal/dynamo-browse/services/tables/service.go +++ b/internal/dynamo-browse/services/tables/service.go @@ -86,8 +86,6 @@ func (s *Service) doScan( }, errors.Wrapf(err, "unable to scan table %v", tableInfo.Name) } - models.Sort(results, tableInfo) - resultSet := &models.ResultSet{ TableInfo: tableInfo, Created: time.Now(), @@ -97,6 +95,7 @@ func (s *Service) doScan( } resultSet.SetItems(results) resultSet.RefreshColumns() + resultSet.Sort(models.PKSKSortFilter(tableInfo)) return resultSet, err } diff --git a/internal/dynamo-browse/ui/keybindings/defaults.go b/internal/dynamo-browse/ui/keybindings/defaults.go index cc49dd1..818919a 100644 --- a/internal/dynamo-browse/ui/keybindings/defaults.go +++ b/internal/dynamo-browse/ui/keybindings/defaults.go @@ -12,6 +12,7 @@ func Default() *KeyBindings { ResetColumns: key.NewBinding(key.WithKeys("R", "reset columns")), AddColumn: key.NewBinding(key.WithKeys("a", "add new column")), DeleteColumn: key.NewBinding(key.WithKeys("d", "delete column")), + SortByColumn: key.NewBinding(key.WithKeys("s", "sort by column")), }, TableView: &TableKeyBinding{ MoveUp: key.NewBinding(key.WithKeys("i", "up")), diff --git a/internal/dynamo-browse/ui/keybindings/keybindings.go b/internal/dynamo-browse/ui/keybindings/keybindings.go index d0c7fc3..96c24b3 100644 --- a/internal/dynamo-browse/ui/keybindings/keybindings.go +++ b/internal/dynamo-browse/ui/keybindings/keybindings.go @@ -16,6 +16,7 @@ type FieldsPopupBinding struct { ResetColumns key.Binding `keymap:"reset-columns"` AddColumn key.Binding `keymap:"add-column"` DeleteColumn key.Binding `keymap:"delete-column"` + SortByColumn key.Binding `keymap:"sort-by-column"` } type TableKeyBinding struct { diff --git a/internal/dynamo-browse/ui/teamodels/colselector/colmodel.go b/internal/dynamo-browse/ui/teamodels/colselector/colmodel.go index f6221b0..514c8ac 100644 --- a/internal/dynamo-browse/ui/teamodels/colselector/colmodel.go +++ b/internal/dynamo-browse/ui/teamodels/colselector/colmodel.go @@ -6,6 +6,7 @@ import ( "github.com/charmbracelet/lipgloss" "github.com/lmika/dynamo-browse/internal/common/ui/events" "github.com/lmika/dynamo-browse/internal/dynamo-browse/controllers" + "github.com/lmika/dynamo-browse/internal/dynamo-browse/models" "github.com/lmika/dynamo-browse/internal/dynamo-browse/models/columns" "github.com/lmika/dynamo-browse/internal/dynamo-browse/ui/keybindings" "github.com/lmika/dynamo-browse/internal/dynamo-browse/ui/teamodels/layout" @@ -25,8 +26,9 @@ type colListModel struct { keyBinding *keybindings.KeyBindings colController *controllers.ColumnsController - rows []table.Row - table table.Model + rows []table.Row + table table.Model + sortCriteria models.SortCriteria } func newColListModel(keyBinding *keybindings.KeyBindings, colController *controllers.ColumnsController) *colListModel { @@ -68,6 +70,8 @@ func (m *colListModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) { return m, events.SetTeaMessage(m.colController.AddColumn(m.table.Cursor())) case key.Matches(msg, m.keyBinding.ColumnPopup.DeleteColumn): return m, events.SetTeaMessage(m.colController.DeleteColumn(m.table.Cursor())) + case key.Matches(msg, m.keyBinding.ColumnPopup.SortByColumn): + return m, events.SetTeaMessage(m.colController.SortByColumn(m.table.Cursor())) // Main table nav case key.Matches(msg, m.keyBinding.TableView.ColLeft): @@ -122,6 +126,7 @@ func (c *colListModel) Resize(w, h int) layout.ResizingModel { func (c *colListModel) refreshTable() { colsFromController := c.colController.Columns() + c.sortCriteria = c.colController.SortCriteria() if len(c.rows) != len(colsFromController.Columns) { c.setColumnsFromModel(colsFromController) } diff --git a/internal/dynamo-browse/ui/teamodels/colselector/model.go b/internal/dynamo-browse/ui/teamodels/colselector/model.go index 2b572d7..8a6aabb 100644 --- a/internal/dynamo-browse/ui/teamodels/colselector/model.go +++ b/internal/dynamo-browse/ui/teamodels/colselector/model.go @@ -42,6 +42,7 @@ func (m *Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { var cc utils.CmdCollector switch msg := msg.(type) { case controllers.ShowColumnOverlay: + m.colListModel.sortCriteria = m.columnsController.SortCriteria() m.colListModel.setColumnsFromModel(m.columnsController.Columns()) m.compositor.SetOverlay(m.colListModel, m.w/2-overlayWidth/2, m.h/2-overlayHeight/2, overlayWidth, overlayHeight) case controllers.HideColumnOverlay: diff --git a/internal/dynamo-browse/ui/teamodels/colselector/tblmodel.go b/internal/dynamo-browse/ui/teamodels/colselector/tblmodel.go index 505d2e7..6d4b3b4 100644 --- a/internal/dynamo-browse/ui/teamodels/colselector/tblmodel.go +++ b/internal/dynamo-browse/ui/teamodels/colselector/tblmodel.go @@ -3,6 +3,7 @@ package colselector import ( "fmt" "github.com/charmbracelet/lipgloss" + "github.com/lmika/dynamo-browse/internal/dynamo-browse/models/evaluators" table "github.com/lmika/go-bubble-table" "io" ) @@ -23,9 +24,17 @@ func (clr colListRowModel) Render(w io.Writer, model table.Model, index int) { } col := clr.m.colController.Columns().Columns[index] - if !col.Hidden { - fmt.Fprintln(w, style.Render(fmt.Sprintf("⋅\t%v", col.Name))) - } else { + ff := clr.m.sortCriteria.FirstField() + switch { + case col.Hidden: fmt.Fprintln(w, style.Render(fmt.Sprintf("✕\t%v", col.Name))) + case evaluators.Equals(ff.Field, col.Evaluator): + if ff.Asc { + fmt.Fprintln(w, style.Render(fmt.Sprintf("v\t%v", col.Name))) + } else { + fmt.Fprintln(w, style.Render(fmt.Sprintf("^\t%v", col.Name))) + } + default: + fmt.Fprintln(w, style.Render(fmt.Sprintf("⋅\t%v", col.Name))) } } diff --git a/internal/dynamo-browse/ui/teamodels/relselector/listmdl.go b/internal/dynamo-browse/ui/teamodels/relselector/listmdl.go index 3bd601f..dc91665 100644 --- a/internal/dynamo-browse/ui/teamodels/relselector/listmdl.go +++ b/internal/dynamo-browse/ui/teamodels/relselector/listmdl.go @@ -90,9 +90,9 @@ func (m *listModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) { if onSel := m.event.OnSelected; onSel != nil { cc.Add(events.SetTeaMessage(onSel(m.event.Items[m.list.Index()]))) } - return m, events.SetTeaMessage(controllers.HideColumnOverlay{}) + return m, events.SetTeaMessage(controllers.HideRelatedItemsOverlay{}) case key.Matches(msg, keyEsc): - return m, events.SetTeaMessage(controllers.HideColumnOverlay{}) + return m, events.SetTeaMessage(controllers.HideRelatedItemsOverlay{}) default: m.list = cc.Collect(m.list.Update(msg)).(list.Model) } diff --git a/internal/dynamo-browse/ui/teamodels/relselector/model.go b/internal/dynamo-browse/ui/teamodels/relselector/model.go index 45658ec..6721763 100644 --- a/internal/dynamo-browse/ui/teamodels/relselector/model.go +++ b/internal/dynamo-browse/ui/teamodels/relselector/model.go @@ -45,7 +45,7 @@ func (m *Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { m.listModel.setItems(msg, newHeight) m.compositor.SetOverlay(m.listModel, m.w/2-overlayWidth/2, m.h/2-newHeight/2, overlayWidth, newHeight) - case controllers.HideColumnOverlay: + case controllers.HideRelatedItemsOverlay: m.compositor.ClearOverlay() case tea.KeyMsg: m.compositor = cc.Collect(m.compositor.Update(msg)).(*layout.Compositor)