| 
									
										
										
										
											2025-05-17 01:11:04 +00:00
										 |  |  | package cmdpacks | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | import ( | 
					
						
							|  |  |  | 	"context" | 
					
						
							|  |  |  | 	"github.com/aws/aws-sdk-go-v2/service/dynamodb/types" | 
					
						
							| 
									
										
										
										
											2025-05-27 11:45:09 +00:00
										 |  |  | 	"github.com/pkg/errors" | 
					
						
							| 
									
										
										
										
											2025-05-26 12:04:23 +00:00
										 |  |  | 	"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" | 
					
						
							| 
									
										
										
										
											2025-05-17 12:16:49 +00:00
										 |  |  | 	"time" | 
					
						
							| 
									
										
										
										
											2025-05-17 01:11:04 +00:00
										 |  |  | 	"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", | 
					
						
							| 
									
										
										
										
											2025-05-17 12:16:49 +00:00
										 |  |  | 	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. | 
					
						
							|  |  |  | 	`, | 
					
						
							| 
									
										
										
										
											2025-05-17 01:11:04 +00:00
										 |  |  | } | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2025-05-17 12:16:49 +00:00
										 |  |  | 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") | 
					
						
							|  |  |  | 	} | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2025-05-18 03:42:44 +00:00
										 |  |  | 	return newResultSetProxy(&models.ResultSet{ | 
					
						
							|  |  |  | 		TableInfo: tableInfo, | 
					
						
							|  |  |  | 		Created:   time.Now(), | 
					
						
							|  |  |  | 	}), nil | 
					
						
							| 
									
										
										
										
											2025-05-17 01:11:04 +00:00
										 |  |  | } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 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. | 
					
						
							|  |  |  |     `, | 
					
						
							|  |  |  | } | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2025-05-23 12:04:41 +00:00
										 |  |  | func parseQuery( | 
					
						
							|  |  |  | 	ctx context.Context, | 
					
						
							|  |  |  | 	args ucl.CallArgs, | 
					
						
							|  |  |  | 	currentRS *models.ResultSet, | 
					
						
							|  |  |  | 	tablesService *tables.Service, | 
					
						
							|  |  |  | ) (*queryexpr.QueryExpr, *models.TableInfo, error) { | 
					
						
							| 
									
										
										
										
											2025-05-17 01:11:04 +00:00
										 |  |  | 	var expr string | 
					
						
							|  |  |  | 	if err := args.Bind(&expr); err != nil { | 
					
						
							| 
									
										
										
										
											2025-05-23 12:04:41 +00:00
										 |  |  | 		return nil, nil, err | 
					
						
							| 
									
										
										
										
											2025-05-17 01:11:04 +00:00
										 |  |  | 	} | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 	q, err := queryexpr.Parse(expr) | 
					
						
							|  |  |  | 	if err != nil { | 
					
						
							| 
									
										
										
										
											2025-05-23 12:04:41 +00:00
										 |  |  | 		return nil, nil, err | 
					
						
							| 
									
										
										
										
											2025-05-17 01:11:04 +00:00
										 |  |  | 	} | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 	if args.NArgs() > 0 { | 
					
						
							|  |  |  | 		var queryArgs ucl.Hashable | 
					
						
							|  |  |  | 		if err := args.Bind(&queryArgs); err != nil { | 
					
						
							| 
									
										
										
										
											2025-05-23 12:04:41 +00:00
										 |  |  | 			return nil, nil, err | 
					
						
							| 
									
										
										
										
											2025-05-17 01:11:04 +00:00
										 |  |  | 		} | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 		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 { | 
					
						
							| 
									
										
										
										
											2025-05-23 12:04:41 +00:00
										 |  |  | 			return nil, nil, err | 
					
						
							| 
									
										
										
										
											2025-05-17 01:11:04 +00:00
										 |  |  | 		} | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2025-05-23 12:04:41 +00:00
										 |  |  | 		tableInfo, err = tablesService.Describe(ctx, tblName) | 
					
						
							| 
									
										
										
										
											2025-05-17 01:11:04 +00:00
										 |  |  | 		if err != nil { | 
					
						
							| 
									
										
										
										
											2025-05-23 12:04:41 +00:00
										 |  |  | 			return nil, nil, err | 
					
						
							| 
									
										
										
										
											2025-05-17 01:11:04 +00:00
										 |  |  | 		} | 
					
						
							| 
									
										
										
										
											2025-05-23 12:04:41 +00:00
										 |  |  | 	} else if currentRS != nil && currentRS.TableInfo != nil { | 
					
						
							|  |  |  | 		tableInfo = currentRS.TableInfo | 
					
						
							| 
									
										
										
										
											2025-05-17 01:11:04 +00:00
										 |  |  | 	} else { | 
					
						
							| 
									
										
										
										
											2025-05-23 12:04:41 +00:00
										 |  |  | 		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 | 
					
						
							| 
									
										
										
										
											2025-05-17 01:11:04 +00:00
										 |  |  | 	} | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 	newResultSet, err := rs.tableService.ScanOrQuery(context.Background(), tableInfo, q, nil) | 
					
						
							|  |  |  | 	if err != nil { | 
					
						
							|  |  |  | 		return nil, err | 
					
						
							|  |  |  | 	} | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2025-05-18 03:42:44 +00:00
										 |  |  | 	return newResultSetProxy(newResultSet), nil | 
					
						
							| 
									
										
										
										
											2025-05-17 01:11:04 +00:00
										 |  |  | } | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2025-05-19 12:14:22 +00:00
										 |  |  | 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 | 
					
						
							|  |  |  | } | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2025-05-23 12:04:41 +00:00
										 |  |  | 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 | 
					
						
							|  |  |  | } | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2025-05-19 12:14:22 +00:00
										 |  |  | 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 | 
					
						
							|  |  |  | } | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2025-05-23 12:04:41 +00:00
										 |  |  | 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 | 
					
						
							|  |  |  | 	} | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2025-05-27 11:45:09 +00:00
										 |  |  | 	vs, err := mapUCLObjectToAttributeType(val) | 
					
						
							|  |  |  | 	if err != nil { | 
					
						
							|  |  |  | 		return nil, err | 
					
						
							|  |  |  | 	} | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 	if err := q.SetEvalItem(item.item, vs); err != nil { | 
					
						
							| 
									
										
										
										
											2025-05-23 12:04:41 +00:00
										 |  |  | 		return nil, err | 
					
						
							|  |  |  | 	} | 
					
						
							|  |  |  | 	item.resultSet.SetDirty(item.idx, true) | 
					
						
							|  |  |  | 	commandctrl.QueueRefresh(ctx) | 
					
						
							| 
									
										
										
										
											2025-05-25 03:31:00 +00:00
										 |  |  | 
 | 
					
						
							|  |  |  | 	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) | 
					
						
							| 
									
										
										
										
											2025-05-23 12:04:41 +00:00
										 |  |  | 
 | 
					
						
							|  |  |  | 	return item, nil | 
					
						
							|  |  |  | } | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2025-05-17 01:11:04 +00:00
										 |  |  | 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{ | 
					
						
							| 
									
										
										
										
											2025-05-19 12:14:22 +00:00
										 |  |  | 			"new":       m.rsNew, | 
					
						
							|  |  |  | 			"query":     m.rsQuery, | 
					
						
							|  |  |  | 			"scan":      m.rsScan, | 
					
						
							| 
									
										
										
										
											2025-05-23 12:04:41 +00:00
										 |  |  | 			"filter":    m.rsFilter, | 
					
						
							| 
									
										
										
										
											2025-05-19 12:14:22 +00:00
										 |  |  | 			"next-page": m.rsNextPage, | 
					
						
							|  |  |  | 			"union":     m.rsUnion, | 
					
						
							| 
									
										
										
										
											2025-05-23 12:04:41 +00:00
										 |  |  | 			"set":       m.rsSet, | 
					
						
							| 
									
										
										
										
											2025-05-25 03:31:00 +00:00
										 |  |  | 			"del":       m.rsDel, | 
					
						
							| 
									
										
										
										
											2025-05-17 01:11:04 +00:00
										 |  |  | 		}, | 
					
						
							|  |  |  | 	} | 
					
						
							|  |  |  | } |