ucl: added more resultset functions
This commit is contained in:
parent
40f8dd76e2
commit
5088009672
2
go.mod
2
go.mod
|
@ -117,5 +117,5 @@ require (
|
||||||
golang.org/x/text v0.9.0 // indirect
|
golang.org/x/text v0.9.0 // indirect
|
||||||
google.golang.org/appengine v1.6.7 // indirect
|
google.golang.org/appengine v1.6.7 // indirect
|
||||||
gopkg.in/yaml.v3 v3.0.1 // indirect
|
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||||
ucl.lmika.dev v0.0.0-20250518033831-f79e91e26d78 // indirect
|
ucl.lmika.dev v0.0.0-20250519120409-53b05b5ba6f8 // indirect
|
||||||
)
|
)
|
||||||
|
|
6
go.sum
6
go.sum
|
@ -454,3 +454,9 @@ ucl.lmika.dev v0.0.0-20250518024533-f4be44fcbc94 h1:x3IRtT1jbedblimi2hesKGBihg24
|
||||||
ucl.lmika.dev v0.0.0-20250518024533-f4be44fcbc94/go.mod h1:/MMZKm6mOMtnY4I8TYEot4Pc8dKEy+/IAQo1VdpA5EY=
|
ucl.lmika.dev v0.0.0-20250518024533-f4be44fcbc94/go.mod h1:/MMZKm6mOMtnY4I8TYEot4Pc8dKEy+/IAQo1VdpA5EY=
|
||||||
ucl.lmika.dev v0.0.0-20250518033831-f79e91e26d78 h1:lbOZUb6whYMLI4win5QL+eLSgqc3N9TtTgT8hTipNl8=
|
ucl.lmika.dev v0.0.0-20250518033831-f79e91e26d78 h1:lbOZUb6whYMLI4win5QL+eLSgqc3N9TtTgT8hTipNl8=
|
||||||
ucl.lmika.dev v0.0.0-20250518033831-f79e91e26d78/go.mod h1:/MMZKm6mOMtnY4I8TYEot4Pc8dKEy+/IAQo1VdpA5EY=
|
ucl.lmika.dev v0.0.0-20250518033831-f79e91e26d78/go.mod h1:/MMZKm6mOMtnY4I8TYEot4Pc8dKEy+/IAQo1VdpA5EY=
|
||||||
|
ucl.lmika.dev v0.0.0-20250519111943-1173d163f5e3 h1:ZMQ1rkcAWa///c3bVvlXbtuqjfAWxDm01abQl3g/YVw=
|
||||||
|
ucl.lmika.dev v0.0.0-20250519111943-1173d163f5e3/go.mod h1:/MMZKm6mOMtnY4I8TYEot4Pc8dKEy+/IAQo1VdpA5EY=
|
||||||
|
ucl.lmika.dev v0.0.0-20250519114239-7ca821016e9a h1:dzBBFCY50+MQcJaQ90swdDyjzag5oIhwdfqbmZkvX3Q=
|
||||||
|
ucl.lmika.dev v0.0.0-20250519114239-7ca821016e9a/go.mod h1:/MMZKm6mOMtnY4I8TYEot4Pc8dKEy+/IAQo1VdpA5EY=
|
||||||
|
ucl.lmika.dev v0.0.0-20250519120409-53b05b5ba6f8 h1:h32JQi0d1MI86RaAMaEU7kvti4uSLX5XYe/nk2abApg=
|
||||||
|
ucl.lmika.dev v0.0.0-20250519120409-53b05b5ba6f8/go.mod h1:/MMZKm6mOMtnY4I8TYEot4Pc8dKEy+/IAQo1VdpA5EY=
|
||||||
|
|
|
@ -129,6 +129,84 @@ func (rs *rsModule) rsQuery(ctx context.Context, args ucl.CallArgs) (any, error)
|
||||||
return newResultSetProxy(newResultSet), nil
|
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
|
||||||
|
}
|
||||||
|
|
||||||
|
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 moduleRS(tableService *tables.Service, state *controllers.State) ucl.Module {
|
func moduleRS(tableService *tables.Service, state *controllers.State) ucl.Module {
|
||||||
m := &rsModule{
|
m := &rsModule{
|
||||||
tableService: tableService,
|
tableService: tableService,
|
||||||
|
@ -140,6 +218,9 @@ func moduleRS(tableService *tables.Service, state *controllers.State) ucl.Module
|
||||||
Builtins: map[string]ucl.BuiltinHandler{
|
Builtins: map[string]ucl.BuiltinHandler{
|
||||||
"new": m.rsNew,
|
"new": m.rsNew,
|
||||||
"query": m.rsQuery,
|
"query": m.rsQuery,
|
||||||
|
"scan": m.rsScan,
|
||||||
|
"next-page": m.rsNextPage,
|
||||||
|
"union": m.rsUnion,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -3,6 +3,7 @@ package cmdpacks_test
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"github.com/lmika/dynamo-browse/internal/common/ui/commandctrl/cmdpacks"
|
"github.com/lmika/dynamo-browse/internal/common/ui/commandctrl/cmdpacks"
|
||||||
|
"github.com/lmika/dynamo-browse/internal/dynamo-browse/models"
|
||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
"testing"
|
"testing"
|
||||||
)
|
)
|
||||||
|
@ -13,7 +14,80 @@ func TestModRS_New(t *testing.T) {
|
||||||
rsProxy, err := svc.CommandController.ExecuteAndWait(t.Context(), `rs:new`)
|
rsProxy, err := svc.CommandController.ExecuteAndWait(t.Context(), `rs:new`)
|
||||||
|
|
||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
assert.IsType(t, rsProxy, &cmdpacks.ResultSetProxy{})
|
assert.IsType(t, rsProxy, cmdpacks.SimpleProxy[*models.ResultSet]{})
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestModRS_NextPage(t *testing.T) {
|
||||||
|
t.Run("multiple pages", func(t *testing.T) {
|
||||||
|
svc := newService(t, withDataGenerator(largeTestData), withTable("large-table"), withDefaultLimit(20))
|
||||||
|
|
||||||
|
hasNextPage, err := svc.CommandController.ExecuteAndWait(t.Context(), `@resultset.HasNextPage`)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
assert.True(t, hasNextPage.(bool))
|
||||||
|
|
||||||
|
// Page 2
|
||||||
|
rsProxy, err := svc.CommandController.ExecuteAndWait(t.Context(), `rs:next-page @resultset`)
|
||||||
|
|
||||||
|
assert.NoError(t, err)
|
||||||
|
assert.IsType(t, cmdpacks.SimpleProxy[*models.ResultSet]{}, rsProxy)
|
||||||
|
assert.Equal(t, 20, len(rsProxy.(cmdpacks.SimpleProxy[*models.ResultSet]).ProxyValue().Items()))
|
||||||
|
|
||||||
|
hasNextPage, err = svc.CommandController.ExecuteAndWait(t.Context(), `(rs:next-page @resultset).HasNextPage`)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
assert.True(t, hasNextPage.(bool))
|
||||||
|
|
||||||
|
// Page 3
|
||||||
|
rsProxy, err = svc.CommandController.ExecuteAndWait(t.Context(), `rs:next-page @resultset | rs:next-page`)
|
||||||
|
|
||||||
|
assert.NoError(t, err)
|
||||||
|
assert.IsType(t, cmdpacks.SimpleProxy[*models.ResultSet]{}, rsProxy)
|
||||||
|
assert.Equal(t, 10, len(rsProxy.(cmdpacks.SimpleProxy[*models.ResultSet]).ProxyValue().Items()))
|
||||||
|
|
||||||
|
hasNextPage, err = svc.CommandController.ExecuteAndWait(t.Context(), `(rs:next-page @resultset | rs:next-page).HasNextPage`)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
assert.False(t, hasNextPage.(bool))
|
||||||
|
|
||||||
|
// Last page
|
||||||
|
rsProxy, err = svc.CommandController.ExecuteAndWait(t.Context(), `rs:next-page (rs:next-page @resultset | rs:next-page)`)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
assert.Nil(t, rsProxy)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("only one page", func(t *testing.T) {
|
||||||
|
svc := newService(t)
|
||||||
|
|
||||||
|
hasNextPage, err := svc.CommandController.ExecuteAndWait(t.Context(), `@resultset.HasNextPage`)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
assert.False(t, hasNextPage.(bool))
|
||||||
|
|
||||||
|
rsProxy, err := svc.CommandController.ExecuteAndWait(t.Context(), `rs:next-page @resultset`)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
assert.Nil(t, rsProxy)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestModRS_Union(t *testing.T) {
|
||||||
|
svc := newService(t, withDefaultLimit(2))
|
||||||
|
|
||||||
|
rsProxy, err := svc.CommandController.ExecuteAndWait(t.Context(), `
|
||||||
|
$mr = rs:union @resultset (rs:next-page @resultset)
|
||||||
|
|
||||||
|
assert (eq (len $mr.Items) 3) "expected len == 3"
|
||||||
|
assert (eq $mr.Items.(0).pk "abc") "expected 0.pk"
|
||||||
|
assert (eq $mr.Items.(0).sk "111") "expected 0.sk"
|
||||||
|
assert (eq $mr.Items.(1).pk "abc") "expected 1.pk"
|
||||||
|
assert (eq $mr.Items.(1).sk "222") "expected 1.sk"
|
||||||
|
assert (eq $mr.Items.(2).pk "bbb") "expected 2.pk"
|
||||||
|
assert (eq $mr.Items.(2).sk "131") "expected 2.sk"
|
||||||
|
|
||||||
|
$mr
|
||||||
|
`)
|
||||||
|
|
||||||
|
assert.NoError(t, err)
|
||||||
|
assert.IsType(t, cmdpacks.SimpleProxy[*models.ResultSet]{}, rsProxy)
|
||||||
|
|
||||||
|
rs := rsProxy.(cmdpacks.SimpleProxy[*models.ResultSet]).ProxyValue()
|
||||||
|
assert.Equal(t, 3, len(rs.Items()))
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestModRS_Query(t *testing.T) {
|
func TestModRS_Query(t *testing.T) {
|
||||||
|
@ -65,11 +139,11 @@ func TestModRS_Query(t *testing.T) {
|
||||||
res, err := svc.CommandController.ExecuteAndWait(t.Context(), tt.cmd)
|
res, err := svc.CommandController.ExecuteAndWait(t.Context(), tt.cmd)
|
||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
|
|
||||||
rs := res.(*cmdpacks.ResultSetProxy).RS
|
rs := res.(cmdpacks.SimpleProxy[*models.ResultSet]).ProxyValue()
|
||||||
assert.Len(t, rs.Items(), len(tt.wantRows))
|
assert.Len(t, rs.Items(), len(tt.wantRows))
|
||||||
|
|
||||||
for i, rowIndex := range tt.wantRows {
|
for i, rowIndex := range tt.wantRows {
|
||||||
for key, want := range testData[0].Data[rowIndex] {
|
for key, want := range svc.testData[0].Data[rowIndex] {
|
||||||
have, ok := rs.Items()[i].AttributeValueAsString(key)
|
have, ok := rs.Items()[i].AttributeValueAsString(key)
|
||||||
assert.True(t, ok)
|
assert.True(t, ok)
|
||||||
assert.Equal(t, fmt.Sprint(want), have)
|
assert.Equal(t, fmt.Sprint(want), have)
|
||||||
|
|
|
@ -9,36 +9,50 @@ import (
|
||||||
"ucl.lmika.dev/ucl"
|
"ucl.lmika.dev/ucl"
|
||||||
)
|
)
|
||||||
|
|
||||||
type proxyFields[T any] map[string]func(t T) ucl.Object
|
type proxyInfo[T comparable] struct {
|
||||||
|
fields map[string]func(t T) ucl.Object
|
||||||
type simpleProxy[T comparable] struct {
|
lenFunc func(t T) int
|
||||||
value T
|
strFunc func(t T) string
|
||||||
fields proxyFields[T]
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (tp simpleProxy[T]) String() string {
|
type SimpleProxy[T comparable] struct {
|
||||||
|
value T
|
||||||
|
proxyInfo *proxyInfo[T]
|
||||||
|
}
|
||||||
|
|
||||||
|
func (tp SimpleProxy[T]) ProxyValue() T {
|
||||||
|
return tp.value
|
||||||
|
}
|
||||||
|
|
||||||
|
func (tp SimpleProxy[T]) String() string {
|
||||||
|
if tp.proxyInfo.strFunc != nil {
|
||||||
|
return tp.proxyInfo.strFunc(tp.value)
|
||||||
|
}
|
||||||
return fmt.Sprint(tp.value)
|
return fmt.Sprint(tp.value)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (tp simpleProxy[T]) Truthy() bool {
|
func (tp SimpleProxy[T]) Truthy() bool {
|
||||||
var zeroT T
|
var zeroT T
|
||||||
return tp.value != zeroT
|
return tp.value != zeroT
|
||||||
}
|
}
|
||||||
|
|
||||||
func (tp simpleProxy[T]) Len() int {
|
func (tp SimpleProxy[T]) Len() int {
|
||||||
return len(tp.fields)
|
if tp.proxyInfo.lenFunc != nil {
|
||||||
|
return tp.proxyInfo.lenFunc(tp.value)
|
||||||
|
}
|
||||||
|
return len(tp.proxyInfo.fields)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (tp simpleProxy[T]) Value(k string) ucl.Object {
|
func (tp SimpleProxy[T]) Value(k string) ucl.Object {
|
||||||
f, ok := tp.fields[k]
|
f, ok := tp.proxyInfo.fields[k]
|
||||||
if !ok {
|
if !ok {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
return f(tp.value)
|
return f(tp.value)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (tp simpleProxy[T]) Each(fn func(k string, v ucl.Object) error) error {
|
func (tp SimpleProxy[T]) Each(fn func(k string, v ucl.Object) error) error {
|
||||||
for key := range maps.Keys(tp.fields) {
|
for key := range maps.Keys(tp.proxyInfo.fields) {
|
||||||
if err := fn(key, tp.Value(key)); err != nil {
|
if err := fn(key, tp.Value(key)); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
@ -72,41 +86,63 @@ func (tp simpleProxyList[T]) Index(k int) ucl.Object {
|
||||||
}
|
}
|
||||||
|
|
||||||
func newResultSetProxy(rs *models.ResultSet) ucl.Object {
|
func newResultSetProxy(rs *models.ResultSet) ucl.Object {
|
||||||
return simpleProxy[*models.ResultSet]{value: rs, fields: resultSetProxyFields}
|
return SimpleProxy[*models.ResultSet]{value: rs, proxyInfo: resultSetProxyFields}
|
||||||
}
|
}
|
||||||
|
|
||||||
var resultSetProxyFields = proxyFields[*models.ResultSet]{
|
var resultSetProxyFields = &proxyInfo[*models.ResultSet]{
|
||||||
|
lenFunc: func(t *models.ResultSet) int { return len(t.Items()) },
|
||||||
|
strFunc: func(t *models.ResultSet) string {
|
||||||
|
return fmt.Sprintf("ResultSet(%v:%d)", t.TableInfo.Name, len(t.Items()))
|
||||||
|
},
|
||||||
|
fields: map[string]func(t *models.ResultSet) ucl.Object{
|
||||||
"Table": func(t *models.ResultSet) ucl.Object { return newTableProxy(t.TableInfo) },
|
"Table": func(t *models.ResultSet) ucl.Object { return newTableProxy(t.TableInfo) },
|
||||||
"Items": func(t *models.ResultSet) ucl.Object { return resultSetItemsProxy{t} },
|
"Items": func(t *models.ResultSet) ucl.Object { return resultSetItemsProxy{t} },
|
||||||
|
"HasNextPage": func(t *models.ResultSet) ucl.Object { return ucl.BoolObject(t.HasNextPage()) },
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
func newTableProxy(table *models.TableInfo) ucl.Object {
|
func newTableProxy(table *models.TableInfo) ucl.Object {
|
||||||
return simpleProxy[*models.TableInfo]{value: table, fields: tableProxyFields}
|
return SimpleProxy[*models.TableInfo]{value: table, proxyInfo: tableProxyFields}
|
||||||
}
|
}
|
||||||
|
|
||||||
var tableProxyFields = proxyFields[*models.TableInfo]{
|
var tableProxyFields = &proxyInfo[*models.TableInfo]{
|
||||||
|
strFunc: func(t *models.TableInfo) string {
|
||||||
|
return fmt.Sprintf("Table(%v)", t.Name)
|
||||||
|
},
|
||||||
|
fields: map[string]func(t *models.TableInfo) ucl.Object{
|
||||||
"Name": func(t *models.TableInfo) ucl.Object { return ucl.StringObject(t.Name) },
|
"Name": func(t *models.TableInfo) ucl.Object { return ucl.StringObject(t.Name) },
|
||||||
"Keys": func(t *models.TableInfo) ucl.Object { return newKeyAttributeProxy(t.Keys) },
|
"Keys": func(t *models.TableInfo) ucl.Object { return newKeyAttributeProxy(t.Keys) },
|
||||||
"DefinedAttributes": func(t *models.TableInfo) ucl.Object { return ucl.StringListObject(t.DefinedAttributes) },
|
"DefinedAttributes": func(t *models.TableInfo) ucl.Object { return ucl.StringListObject(t.DefinedAttributes) },
|
||||||
"GSIs": func(t *models.TableInfo) ucl.Object { return newSimpleProxyList(t.GSIs, newGSIProxy) },
|
"GSIs": func(t *models.TableInfo) ucl.Object { return newSimpleProxyList(t.GSIs, newGSIProxy) },
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
func newKeyAttributeProxy(keyAttrs models.KeyAttribute) ucl.Object {
|
func newKeyAttributeProxy(keyAttrs models.KeyAttribute) ucl.Object {
|
||||||
return simpleProxy[models.KeyAttribute]{value: keyAttrs, fields: keyAttributeProxyFields}
|
return SimpleProxy[models.KeyAttribute]{value: keyAttrs, proxyInfo: keyAttributeProxyFields}
|
||||||
}
|
}
|
||||||
|
|
||||||
var keyAttributeProxyFields = proxyFields[models.KeyAttribute]{
|
var keyAttributeProxyFields = &proxyInfo[models.KeyAttribute]{
|
||||||
|
strFunc: func(t models.KeyAttribute) string {
|
||||||
|
return fmt.Sprintf("KeyAttribute(%v,%v)", t.PartitionKey, t.SortKey)
|
||||||
|
},
|
||||||
|
fields: map[string]func(t models.KeyAttribute) ucl.Object{
|
||||||
"PartitionKey": func(t models.KeyAttribute) ucl.Object { return ucl.StringObject(t.PartitionKey) },
|
"PartitionKey": func(t models.KeyAttribute) ucl.Object { return ucl.StringObject(t.PartitionKey) },
|
||||||
"SortKey": func(t models.KeyAttribute) ucl.Object { return ucl.StringObject(t.SortKey) },
|
"SortKey": func(t models.KeyAttribute) ucl.Object { return ucl.StringObject(t.SortKey) },
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
func newGSIProxy(gsi models.TableGSI) ucl.Object {
|
func newGSIProxy(gsi models.TableGSI) ucl.Object {
|
||||||
return simpleProxy[models.TableGSI]{value: gsi, fields: gsiProxyFields}
|
return SimpleProxy[models.TableGSI]{value: gsi, proxyInfo: gsiProxyFields}
|
||||||
}
|
}
|
||||||
|
|
||||||
var gsiProxyFields = proxyFields[models.TableGSI]{
|
var gsiProxyFields = &proxyInfo[models.TableGSI]{
|
||||||
|
strFunc: func(t models.TableGSI) string {
|
||||||
|
return fmt.Sprintf("TableGSI(%v,(%v,%v))", t.Name, t.Keys.PartitionKey, t.Keys.SortKey)
|
||||||
|
},
|
||||||
|
fields: map[string]func(t models.TableGSI) ucl.Object{
|
||||||
"Name": func(t models.TableGSI) ucl.Object { return ucl.StringObject(t.Name) },
|
"Name": func(t models.TableGSI) ucl.Object { return ucl.StringObject(t.Name) },
|
||||||
"Keys": func(t models.TableGSI) ucl.Object { return newKeyAttributeProxy(t.Keys) },
|
"Keys": func(t models.TableGSI) ucl.Object { return newKeyAttributeProxy(t.Keys) },
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
type resultSetItemsProxy struct {
|
type resultSetItemsProxy struct {
|
||||||
|
@ -114,7 +150,7 @@ type resultSetItemsProxy struct {
|
||||||
}
|
}
|
||||||
|
|
||||||
func (ip resultSetItemsProxy) String() string {
|
func (ip resultSetItemsProxy) String() string {
|
||||||
return "items"
|
return "RSItem()"
|
||||||
}
|
}
|
||||||
|
|
||||||
func (ip resultSetItemsProxy) Truthy() bool {
|
func (ip resultSetItemsProxy) Truthy() bool {
|
||||||
|
@ -136,7 +172,7 @@ type itemProxy struct {
|
||||||
}
|
}
|
||||||
|
|
||||||
func (ip itemProxy) String() string {
|
func (ip itemProxy) String() string {
|
||||||
return "item"
|
return fmt.Sprintf("RSItems(%v)", len(ip.item))
|
||||||
}
|
}
|
||||||
|
|
||||||
func (ip itemProxy) Truthy() bool {
|
func (ip itemProxy) Truthy() bool {
|
||||||
|
|
|
@ -26,9 +26,9 @@ func (rs resultSetPVar) Get(ctx context.Context) (any, error) {
|
||||||
}
|
}
|
||||||
|
|
||||||
func (rs resultSetPVar) Set(ctx context.Context, value any) error {
|
func (rs resultSetPVar) Set(ctx context.Context, value any) error {
|
||||||
rsVal, ok := value.(simpleProxy[*models.ResultSet])
|
rsVal, ok := value.(SimpleProxy[*models.ResultSet])
|
||||||
if !ok {
|
if !ok {
|
||||||
return errors.New("new value to @resultset is not a result set")
|
return errors.New("new value to @resultset is nil or not a result set")
|
||||||
}
|
}
|
||||||
|
|
||||||
msg := rs.readController.SetResultSet(rsVal.value)
|
msg := rs.readController.SetResultSet(rsVal.value)
|
||||||
|
|
|
@ -1,6 +1,8 @@
|
||||||
package cmdpacks_test
|
package cmdpacks_test
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"fmt"
|
||||||
|
tea "github.com/charmbracelet/bubbletea"
|
||||||
"github.com/lmika/dynamo-browse/internal/common/ui/commandctrl"
|
"github.com/lmika/dynamo-browse/internal/common/ui/commandctrl"
|
||||||
"github.com/lmika/dynamo-browse/internal/common/ui/commandctrl/cmdpacks"
|
"github.com/lmika/dynamo-browse/internal/common/ui/commandctrl/cmdpacks"
|
||||||
"github.com/lmika/dynamo-browse/internal/dynamo-browse/controllers"
|
"github.com/lmika/dynamo-browse/internal/dynamo-browse/controllers"
|
||||||
|
@ -48,14 +50,41 @@ func TestStdCmds_Mark(t *testing.T) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type testDataGenerator func() []testdynamo.TestData
|
||||||
type services struct {
|
type services struct {
|
||||||
CommandController *commandctrl.CommandController
|
CommandController *commandctrl.CommandController
|
||||||
SelItemIndex int
|
SelItemIndex int
|
||||||
|
|
||||||
State *controllers.State
|
State *controllers.State
|
||||||
|
|
||||||
|
settingStore *settingstore.SettingStore
|
||||||
|
table string
|
||||||
|
|
||||||
|
testDataGenerator testDataGenerator
|
||||||
|
testData []testdynamo.TestData
|
||||||
}
|
}
|
||||||
|
|
||||||
func newService(t *testing.T) *services {
|
type serviceOpt func(*services)
|
||||||
|
|
||||||
|
func withDataGenerator(tg testDataGenerator) serviceOpt {
|
||||||
|
return func(s *services) {
|
||||||
|
s.testDataGenerator = tg
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func withTable(table string) serviceOpt {
|
||||||
|
return func(s *services) {
|
||||||
|
s.table = table
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func withDefaultLimit(limit int) serviceOpt {
|
||||||
|
return func(s *services) {
|
||||||
|
s.settingStore.SetDefaultLimit(limit)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func newService(t *testing.T, opts ...serviceOpt) *services {
|
||||||
ws := testworkspace.New(t)
|
ws := testworkspace.New(t)
|
||||||
|
|
||||||
resultSetSnapshotStore := workspacestore.NewResultSetSnapshotStore(ws)
|
resultSetSnapshotStore := workspacestore.NewResultSetSnapshotStore(ws)
|
||||||
|
@ -66,7 +95,18 @@ func newService(t *testing.T) *services {
|
||||||
itemRendererService := itemrenderer.NewService(itemrenderer.PlainTextRenderer(), itemrenderer.PlainTextRenderer())
|
itemRendererService := itemrenderer.NewService(itemrenderer.PlainTextRenderer(), itemrenderer.PlainTextRenderer())
|
||||||
inputHistoryService := inputhistory.New(inputHistoryStore)
|
inputHistoryService := inputhistory.New(inputHistoryStore)
|
||||||
|
|
||||||
client := testdynamo.SetupTestTable(t, testData)
|
s := &services{
|
||||||
|
table: "service-test-data",
|
||||||
|
settingStore: settingStore,
|
||||||
|
testDataGenerator: normalTestData,
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, opt := range opts {
|
||||||
|
opt(s)
|
||||||
|
}
|
||||||
|
|
||||||
|
s.testData = s.testDataGenerator()
|
||||||
|
client := testdynamo.SetupTestTable(t, s.testData)
|
||||||
|
|
||||||
provider := dynamo.NewProvider(client)
|
provider := dynamo.NewProvider(client)
|
||||||
service := tables.NewService(provider, settingStore)
|
service := tables.NewService(provider, settingStore)
|
||||||
|
@ -74,6 +114,7 @@ func newService(t *testing.T) *services {
|
||||||
|
|
||||||
state := controllers.NewState()
|
state := controllers.NewState()
|
||||||
jobsController := controllers.NewJobsController(jobs.NewService(eventBus), eventBus, true)
|
jobsController := controllers.NewJobsController(jobs.NewService(eventBus), eventBus, true)
|
||||||
|
|
||||||
readController := controllers.NewTableReadController(
|
readController := controllers.NewTableReadController(
|
||||||
state,
|
state,
|
||||||
service,
|
service,
|
||||||
|
@ -84,7 +125,7 @@ func newService(t *testing.T) *services {
|
||||||
eventBus,
|
eventBus,
|
||||||
pasteboardprovider.NilProvider{},
|
pasteboardprovider.NilProvider{},
|
||||||
nil,
|
nil,
|
||||||
"service-test-data",
|
s.table,
|
||||||
)
|
)
|
||||||
writeController := controllers.NewTableWriteController(state, service, jobsController, readController, settingStore)
|
writeController := controllers.NewTableWriteController(state, service, jobsController, readController, settingStore)
|
||||||
settingsController := controllers.NewSettingsController(settingStore, eventBus)
|
settingsController := controllers.NewSettingsController(settingStore, eventBus)
|
||||||
|
@ -102,10 +143,8 @@ func newService(t *testing.T) *services {
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
s := &services{
|
s.State = state
|
||||||
State: state,
|
s.CommandController = commandController
|
||||||
CommandController: commandController,
|
|
||||||
}
|
|
||||||
|
|
||||||
commandController.SetUIStateProvider(s)
|
commandController.SetUIStateProvider(s)
|
||||||
readController.Init()
|
readController.Init()
|
||||||
|
@ -117,7 +156,13 @@ func (s *services) SelectedItemIndex() int {
|
||||||
return s.SelItemIndex
|
return s.SelItemIndex
|
||||||
}
|
}
|
||||||
|
|
||||||
var testData = []testdynamo.TestData{
|
func (s *services) SetSelectedItemIndex(newIdx int) tea.Msg {
|
||||||
|
s.SelItemIndex = newIdx
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func normalTestData() []testdynamo.TestData {
|
||||||
|
return []testdynamo.TestData{
|
||||||
{
|
{
|
||||||
TableName: "service-test-data",
|
TableName: "service-test-data",
|
||||||
Data: []map[string]interface{}{
|
Data: []map[string]interface{}{
|
||||||
|
@ -140,4 +185,28 @@ var testData = []testdynamo.TestData{
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func largeTestData() []testdynamo.TestData {
|
||||||
|
return []testdynamo.TestData{
|
||||||
|
{
|
||||||
|
TableName: "large-table",
|
||||||
|
Data: genRow(50, func(i int) map[string]interface{} {
|
||||||
|
return map[string]interface{}{
|
||||||
|
"pk": fmt.Sprint(i),
|
||||||
|
"sk": fmt.Sprint(i),
|
||||||
|
"alpha": fmt.Sprintf("row %v", i),
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func genRow(count int, mapFn func(int) map[string]interface{}) []map[string]interface{} {
|
||||||
|
result := make([]map[string]interface{}, count)
|
||||||
|
for i := 0; i < count; i++ {
|
||||||
|
result[i] = mapFn(i)
|
||||||
|
}
|
||||||
|
return result
|
||||||
}
|
}
|
||||||
|
|
|
@ -151,3 +151,37 @@ func (rs *ResultSet) Sort(criteria SortCriteria) {
|
||||||
rs.sortCriteria = criteria
|
rs.sortCriteria = criteria
|
||||||
Sort(rs.items, criteria)
|
Sort(rs.items, criteria)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (rs *ResultSet) MergeWith(otherRS *ResultSet) *ResultSet {
|
||||||
|
type pksk struct {
|
||||||
|
pk types.AttributeValue
|
||||||
|
sk types.AttributeValue
|
||||||
|
}
|
||||||
|
|
||||||
|
if !rs.TableInfo.Equal(otherRS.TableInfo) {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
itemsInI := make(map[pksk]Item)
|
||||||
|
newItems := make([]Item, 0, len(rs.Items())+len(otherRS.Items()))
|
||||||
|
for _, item := range rs.Items() {
|
||||||
|
pk, sk := item.PKSK(rs.TableInfo)
|
||||||
|
itemsInI[pksk{pk, sk}] = item
|
||||||
|
newItems = append(newItems, item)
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, item := range otherRS.Items() {
|
||||||
|
pk, sk := item.PKSK(rs.TableInfo)
|
||||||
|
if _, hasItem := itemsInI[pksk{pk, sk}]; !hasItem {
|
||||||
|
newItems = append(newItems, item)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
newResultSet := &ResultSet{
|
||||||
|
Created: time.Now(),
|
||||||
|
TableInfo: rs.TableInfo,
|
||||||
|
}
|
||||||
|
newResultSet.SetItems(newItems)
|
||||||
|
|
||||||
|
return newResultSet
|
||||||
|
}
|
||||||
|
|
Loading…
Reference in a new issue