309 lines
7.5 KiB
Go
309 lines
7.5 KiB
Go
package cmdpacks
|
|
|
|
import (
|
|
"context"
|
|
"github.com/aws/aws-sdk-go-v2/service/dynamodb/types"
|
|
"lmika.dev/cmd/dynamo-browse/internal/common/ui/commandctrl"
|
|
"lmika.dev/cmd/dynamo-browse/internal/dynamo-browse/controllers"
|
|
"lmika.dev/cmd/dynamo-browse/internal/dynamo-browse/models"
|
|
"lmika.dev/cmd/dynamo-browse/internal/dynamo-browse/models/queryexpr"
|
|
"lmika.dev/cmd/dynamo-browse/internal/dynamo-browse/services/tables"
|
|
"github.com/pkg/errors"
|
|
"time"
|
|
"ucl.lmika.dev/repl"
|
|
"ucl.lmika.dev/ucl"
|
|
)
|
|
|
|
type rsModule struct {
|
|
tableService *tables.Service
|
|
state *controllers.State
|
|
}
|
|
|
|
var rsNewDoc = repl.Doc{
|
|
Brief: "Creates a new, empty result set",
|
|
Usage: "[-table NAME]",
|
|
Detailed: `
|
|
The result set assumes the details of the current table. If no table is specified,
|
|
the command will return an error.
|
|
`,
|
|
}
|
|
|
|
func (rs *rsModule) rsNew(ctx context.Context, args ucl.CallArgs) (_ any, err error) {
|
|
var tableInfo *models.TableInfo
|
|
if args.HasSwitch("table") {
|
|
var tblName string
|
|
if err := args.BindSwitch("table", &tblName); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
tableInfo, err = rs.tableService.Describe(ctx, tblName)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
} else if currRs := rs.state.ResultSet(); currRs != nil && currRs.TableInfo != nil {
|
|
tableInfo = currRs.TableInfo
|
|
} else {
|
|
return nil, errors.New("no table specified")
|
|
}
|
|
|
|
return newResultSetProxy(&models.ResultSet{
|
|
TableInfo: tableInfo,
|
|
Created: time.Now(),
|
|
}), nil
|
|
}
|
|
|
|
var rsQueryDoc = repl.Doc{
|
|
Brief: "Runs a query and returns the results as a result-set",
|
|
Usage: "QUERY [ARGS] [-table NAME]",
|
|
Args: []repl.ArgDoc{
|
|
{Name: "query", Brief: "Query expression to run"},
|
|
{Name: "args", Brief: "Hash of argument values to substitute into the query"},
|
|
{Name: "-table", Brief: "Optional table name to use for the query"},
|
|
},
|
|
Detailed: `
|
|
If no table is specified, then the value of @table will be used. If this is unavailable,
|
|
the command will return an error.
|
|
`,
|
|
}
|
|
|
|
func parseQuery(
|
|
ctx context.Context,
|
|
args ucl.CallArgs,
|
|
currentRS *models.ResultSet,
|
|
tablesService *tables.Service,
|
|
) (*queryexpr.QueryExpr, *models.TableInfo, error) {
|
|
var expr string
|
|
if err := args.Bind(&expr); err != nil {
|
|
return nil, nil, err
|
|
}
|
|
|
|
q, err := queryexpr.Parse(expr)
|
|
if err != nil {
|
|
return nil, nil, err
|
|
}
|
|
|
|
if args.NArgs() > 0 {
|
|
var queryArgs ucl.Hashable
|
|
if err := args.Bind(&queryArgs); err != nil {
|
|
return nil, nil, err
|
|
}
|
|
|
|
queryNames := map[string]string{}
|
|
queryValues := map[string]types.AttributeValue{}
|
|
queryArgs.Each(func(k string, v ucl.Object) error {
|
|
if v == nil {
|
|
return nil
|
|
}
|
|
|
|
queryNames[k] = v.String()
|
|
|
|
switch v.(type) {
|
|
case ucl.StringObject:
|
|
queryValues[k] = &types.AttributeValueMemberS{Value: v.String()}
|
|
case ucl.IntObject:
|
|
queryValues[k] = &types.AttributeValueMemberN{Value: v.String()}
|
|
// TODO: other types
|
|
}
|
|
return nil
|
|
})
|
|
|
|
q = q.WithNameParams(queryNames).WithValueParams(queryValues)
|
|
}
|
|
|
|
var tableInfo *models.TableInfo
|
|
if args.HasSwitch("table") {
|
|
var tblName string
|
|
if err := args.BindSwitch("table", &tblName); err != nil {
|
|
return nil, nil, err
|
|
}
|
|
|
|
tableInfo, err = tablesService.Describe(ctx, tblName)
|
|
if err != nil {
|
|
return nil, nil, err
|
|
}
|
|
} else if currentRS != nil && currentRS.TableInfo != nil {
|
|
tableInfo = currentRS.TableInfo
|
|
} else {
|
|
return nil, nil, errors.New("no table specified")
|
|
}
|
|
|
|
return q, tableInfo, nil
|
|
}
|
|
|
|
func (rs *rsModule) rsQuery(ctx context.Context, args ucl.CallArgs) (any, error) {
|
|
q, tableInfo, err := parseQuery(ctx, args, rs.state.ResultSet(), rs.tableService)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
newResultSet, err := rs.tableService.ScanOrQuery(context.Background(), tableInfo, q, nil)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
return newResultSetProxy(newResultSet), nil
|
|
}
|
|
|
|
var rsScanDoc = repl.Doc{
|
|
Brief: "Performs a scan of the table and returns the results as a result-set",
|
|
Usage: "[-table NAME]",
|
|
Args: []repl.ArgDoc{
|
|
{Name: "-table", Brief: "Optional table name to use for the query"},
|
|
},
|
|
Detailed: `
|
|
If no table is specified, then the value of @table will be used. If this is unavailable,
|
|
the command will return an error.
|
|
`,
|
|
}
|
|
|
|
func (rs *rsModule) rsScan(ctx context.Context, args ucl.CallArgs) (_ any, err error) {
|
|
var tableInfo *models.TableInfo
|
|
if args.HasSwitch("table") {
|
|
var tblName string
|
|
if err := args.BindSwitch("table", &tblName); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
tableInfo, err = rs.tableService.Describe(ctx, tblName)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
} else if currRs := rs.state.ResultSet(); currRs != nil && currRs.TableInfo != nil {
|
|
tableInfo = currRs.TableInfo
|
|
} else {
|
|
return nil, errors.New("no table specified")
|
|
}
|
|
|
|
newResultSet, err := rs.tableService.Scan(context.Background(), tableInfo)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
return newResultSetProxy(newResultSet), nil
|
|
}
|
|
|
|
func (rs *rsModule) rsFilter(ctx context.Context, args ucl.CallArgs) (any, error) {
|
|
var (
|
|
rsProxy SimpleProxy[*models.ResultSet]
|
|
filter string
|
|
)
|
|
|
|
if err := args.Bind(&rsProxy, &filter); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
newResultSet := rs.tableService.Filter(rsProxy.ProxyValue(), filter)
|
|
return newResultSetProxy(newResultSet), nil
|
|
}
|
|
|
|
var rsNextPageDoc = repl.Doc{
|
|
Brief: "Returns the next page of the passed in result-set",
|
|
Usage: "RESULT_SET",
|
|
Args: []repl.ArgDoc{
|
|
{Name: "result-set", Brief: "Result set to fetch the next page of"},
|
|
},
|
|
Detailed: `
|
|
If no next page exists, the command will return nil.
|
|
`,
|
|
}
|
|
|
|
func (rs *rsModule) rsNextPage(ctx context.Context, args ucl.CallArgs) (_ any, err error) {
|
|
var rsProxy SimpleProxy[*models.ResultSet]
|
|
|
|
if err := args.Bind(&rsProxy); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
if !rsProxy.value.HasNextPage() {
|
|
return nil, nil
|
|
}
|
|
|
|
nextPage, err := rs.tableService.NextPage(ctx, rsProxy.value)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
return newResultSetProxy(nextPage), nil
|
|
}
|
|
|
|
func (rs *rsModule) rsUnion(ctx context.Context, args ucl.CallArgs) (_ any, err error) {
|
|
var rsProxy1, rsProxy2 SimpleProxy[*models.ResultSet]
|
|
|
|
if err := args.Bind(&rsProxy1, &rsProxy2); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
return newResultSetProxy(rsProxy1.ProxyValue().MergeWith(rsProxy2.ProxyValue())), nil
|
|
}
|
|
|
|
func (rs *rsModule) rsSet(ctx context.Context, args ucl.CallArgs) (_ any, err error) {
|
|
var (
|
|
item itemProxy
|
|
expr string
|
|
val ucl.Object
|
|
)
|
|
|
|
if err := args.Bind(&item, &expr, &val); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
q, err := queryexpr.Parse(expr)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
// TEMP: attribute is always S
|
|
if err := q.SetEvalItem(item.item, &types.AttributeValueMemberS{Value: val.String()}); err != nil {
|
|
return nil, err
|
|
}
|
|
item.resultSet.SetDirty(item.idx, true)
|
|
commandctrl.QueueRefresh(ctx)
|
|
|
|
return item, nil
|
|
}
|
|
|
|
func (rs *rsModule) rsDel(ctx context.Context, args ucl.CallArgs) (_ any, err error) {
|
|
var (
|
|
item itemProxy
|
|
expr string
|
|
)
|
|
|
|
if err := args.Bind(&item, &expr); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
q, err := queryexpr.Parse(expr)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
if err := q.DeleteAttribute(item.item); err != nil {
|
|
return nil, err
|
|
}
|
|
item.resultSet.SetDirty(item.idx, true)
|
|
commandctrl.QueueRefresh(ctx)
|
|
|
|
return item, nil
|
|
}
|
|
|
|
func moduleRS(tableService *tables.Service, state *controllers.State) ucl.Module {
|
|
m := &rsModule{
|
|
tableService: tableService,
|
|
state: state,
|
|
}
|
|
|
|
return ucl.Module{
|
|
Name: "rs",
|
|
Builtins: map[string]ucl.BuiltinHandler{
|
|
"new": m.rsNew,
|
|
"query": m.rsQuery,
|
|
"scan": m.rsScan,
|
|
"filter": m.rsFilter,
|
|
"next-page": m.rsNextPage,
|
|
"union": m.rsUnion,
|
|
"set": m.rsSet,
|
|
"del": m.rsDel,
|
|
},
|
|
}
|
|
}
|