sqs-browse: fixed assumption regarding table keys
This commit is contained in:
		
							parent
							
								
									30dbc4eefe
								
							
						
					
					
						commit
						3428bd2a8a
					
				|  | @ -3,18 +3,20 @@ package controllers | |||
| import ( | ||||
| 	"context" | ||||
| 	"github.com/lmika/awstools/internal/common/ui/uimodels" | ||||
| 	"github.com/lmika/awstools/internal/dynamo-browse/models" | ||||
| 	"github.com/lmika/awstools/internal/dynamo-browse/services/tables" | ||||
| 	"github.com/pkg/errors" | ||||
| ) | ||||
| 
 | ||||
| type TableReadController struct { | ||||
| 	tableService *tables.Service | ||||
| 	tableName string | ||||
| 	tableName    string | ||||
| } | ||||
| 
 | ||||
| func NewTableReadController(tableService *tables.Service, tableName string) *TableReadController { | ||||
| 	return &TableReadController{ | ||||
| 		tableService: tableService, | ||||
| 		tableName: tableName, | ||||
| 		tableName:    tableName, | ||||
| 	} | ||||
| } | ||||
| 
 | ||||
|  | @ -24,14 +26,19 @@ func (c *TableReadController) Scan() uimodels.Operation { | |||
| 	}) | ||||
| } | ||||
| 
 | ||||
| func (c *TableReadController) doScan(ctx context.Context, quiet bool) error { | ||||
| func (c *TableReadController) doScan(ctx context.Context, quiet bool) (err error) { | ||||
| 	uiCtx := uimodels.Ctx(ctx) | ||||
| 
 | ||||
| 	if !quiet { | ||||
| 		uiCtx.Message("Scanning...") | ||||
| 	} | ||||
| 
 | ||||
| 	resultSet, err := c.tableService.Scan(ctx, c.tableName) | ||||
| 	tableInfo, err := c.tableInfo(ctx) | ||||
| 	if err != nil { | ||||
| 		return err | ||||
| 	} | ||||
| 
 | ||||
| 	resultSet, err := c.tableService.Scan(ctx, tableInfo) | ||||
| 	if err != nil { | ||||
| 		return err | ||||
| 	} | ||||
|  | @ -41,4 +48,18 @@ func (c *TableReadController) doScan(ctx context.Context, quiet bool) error { | |||
| 	} | ||||
| 	uiCtx.Send(NewResultSet{resultSet}) | ||||
| 	return nil | ||||
| } | ||||
| } | ||||
| 
 | ||||
| // tableInfo returns the table info from the state if a result set exists.  If not, it fetches the
 | ||||
| // table information from the service.
 | ||||
| func (c *TableReadController) tableInfo(ctx context.Context) (*models.TableInfo, error) { | ||||
| 	if existingResultSet := CurrentState(ctx).ResultSet; existingResultSet != nil { | ||||
| 		return existingResultSet.TableInfo, nil | ||||
| 	} | ||||
| 
 | ||||
| 	tableInfo, err := c.tableService.Describe(ctx, c.tableName) | ||||
| 	if err != nil { | ||||
| 		return nil, errors.Wrapf(err, "cannot describe %v", c.tableName) | ||||
| 	} | ||||
| 	return tableInfo, nil | ||||
| } | ||||
|  |  | |||
|  | @ -69,8 +69,13 @@ func (c *TableWriteController) Duplicate() uimodels.Operation { | |||
| 					return errors.New("operation aborted") | ||||
| 				} | ||||
| 
 | ||||
| 				tableInfo, err := c.tableReadControllers.tableInfo(ctx) | ||||
| 				if err != nil { | ||||
| 					return err | ||||
| 				} | ||||
| 
 | ||||
| 				// Delete the item
 | ||||
| 				if err := c.tableService.Put(ctx, c.tableName, newItem); err != nil { | ||||
| 				if err := c.tableService.Put(ctx, tableInfo, newItem); err != nil { | ||||
| 					return err | ||||
| 				} | ||||
| 
 | ||||
|  | @ -105,8 +110,13 @@ func (c *TableWriteController) Delete() uimodels.Operation { | |||
| 				return errors.New("operation aborted") | ||||
| 			} | ||||
| 
 | ||||
| 			tableInfo, err := c.tableReadControllers.tableInfo(ctx) | ||||
| 			if err != nil { | ||||
| 				return err | ||||
| 			} | ||||
| 
 | ||||
| 			// Delete the item
 | ||||
| 			if err := c.tableService.Delete(ctx, c.tableName, state.SelectedItem); err != nil { | ||||
| 			if err := c.tableService.Delete(ctx, tableInfo, state.SelectedItem); err != nil { | ||||
| 				return err | ||||
| 			} | ||||
| 
 | ||||
|  |  | |||
|  | @ -3,9 +3,9 @@ package models | |||
| import "github.com/aws/aws-sdk-go-v2/service/dynamodb/types" | ||||
| 
 | ||||
| type ResultSet struct { | ||||
| 	Table   string | ||||
| 	Columns []string | ||||
| 	Items   []Item | ||||
| 	TableInfo *TableInfo | ||||
| 	Columns   []string | ||||
| 	Items     []Item | ||||
| } | ||||
| 
 | ||||
| type Item map[string]types.AttributeValue | ||||
|  | @ -21,3 +21,12 @@ func (i Item) Clone() Item { | |||
| 
 | ||||
| 	return newItem | ||||
| } | ||||
| 
 | ||||
| func (i Item) KeyValue(info *TableInfo) map[string]types.AttributeValue { | ||||
| 	itemKey := make(map[string]types.AttributeValue) | ||||
| 	itemKey[info.Keys.PartitionKey] = i[info.Keys.PartitionKey] | ||||
| 	if info.Keys.SortKey != "" { | ||||
| 		itemKey[info.Keys.SortKey] = i[info.Keys.SortKey] | ||||
| 	} | ||||
| 	return itemKey | ||||
| } | ||||
|  | @ -5,13 +5,13 @@ import "sort" | |||
| // sortedItems is a collection of items that is sorted.
 | ||||
| // Items are sorted based on the PK, and SK in ascending order
 | ||||
| type sortedItems struct { | ||||
| 	pk, sk string | ||||
| 	items  []Item | ||||
| 	tableInfo *TableInfo | ||||
| 	items     []Item | ||||
| } | ||||
| 
 | ||||
| // Sort sorts the items in place
 | ||||
| func Sort(items []Item, pk, sk string) { | ||||
| 	si := sortedItems{items: items, pk: pk, sk: sk} | ||||
| func Sort(items []Item, tableInfo *TableInfo) { | ||||
| 	si := sortedItems{items: items, tableInfo: tableInfo} | ||||
| 	sort.Sort(&si) | ||||
| } | ||||
| 
 | ||||
|  | @ -21,7 +21,7 @@ func (si *sortedItems) Len() int { | |||
| 
 | ||||
| func (si *sortedItems) Less(i, j int) bool { | ||||
| 	// Compare primary keys
 | ||||
| 	pv1, pv2 := si.items[i][si.pk], si.items[j][si.pk] | ||||
| 	pv1, pv2 := si.items[i][si.tableInfo.Keys.PartitionKey], si.items[j][si.tableInfo.Keys.PartitionKey] | ||||
| 	pc, ok := compareScalarAttributes(pv1, pv2) | ||||
| 	if !ok { | ||||
| 		return i < j | ||||
|  | @ -34,16 +34,18 @@ func (si *sortedItems) Less(i, j int) bool { | |||
| 	} | ||||
| 
 | ||||
| 	// Partition keys are equal, compare sort key
 | ||||
| 	sv1, sv2 := si.items[i][si.sk], si.items[j][si.sk] | ||||
| 	sc, ok := compareScalarAttributes(sv1, sv2) | ||||
| 	if !ok { | ||||
| 		return i < j | ||||
| 	} | ||||
| 	if sortKey := si.tableInfo.Keys.SortKey; sortKey != "" { | ||||
| 		sv1, sv2 := si.items[i][sortKey], si.items[j][sortKey] | ||||
| 		sc, ok := compareScalarAttributes(sv1, sv2) | ||||
| 		if !ok { | ||||
| 			return i < j | ||||
| 		} | ||||
| 
 | ||||
| 	if sc < 0 { | ||||
| 		return true | ||||
| 	} else if sc > 0 { | ||||
| 		return false | ||||
| 		if sc < 0 { | ||||
| 			return true | ||||
| 		} else if sc > 0 { | ||||
| 			return false | ||||
| 		} | ||||
| 	} | ||||
| 
 | ||||
| 	// This should never happen, but just in case
 | ||||
|  |  | |||
							
								
								
									
										12
									
								
								internal/dynamo-browse/models/tableinfo.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										12
									
								
								internal/dynamo-browse/models/tableinfo.go
									
									
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,12 @@ | |||
| package models | ||||
| 
 | ||||
| type TableInfo struct { | ||||
| 	Name              string | ||||
| 	Keys              KeyAttribute | ||||
| 	DefinedAttributes []string | ||||
| } | ||||
| 
 | ||||
| type KeyAttribute struct { | ||||
| 	PartitionKey string | ||||
| 	SortKey      string | ||||
| } | ||||
|  | @ -13,6 +13,32 @@ type Provider struct { | |||
| 	client *dynamodb.Client | ||||
| } | ||||
| 
 | ||||
| func (p *Provider) DescribeTable(ctx context.Context, tableName string) (*models.TableInfo, error) { | ||||
| 	out, err := p.client.DescribeTable(ctx, &dynamodb.DescribeTableInput{ | ||||
| 		TableName: aws.String(tableName), | ||||
| 	}) | ||||
| 	if err != nil { | ||||
| 		return nil, errors.Wrapf(err, "cannot describe table %v", tableName) | ||||
| 	} | ||||
| 
 | ||||
| 	var tableInfo models.TableInfo | ||||
| 	tableInfo.Name = aws.ToString(out.Table.TableName) | ||||
| 
 | ||||
| 	for _, keySchema := range out.Table.KeySchema { | ||||
| 		if keySchema.KeyType == types.KeyTypeHash { | ||||
| 			tableInfo.Keys.PartitionKey = aws.ToString(keySchema.AttributeName) | ||||
| 		} else if keySchema.KeyType == types.KeyTypeRange { | ||||
| 			tableInfo.Keys.SortKey = aws.ToString(keySchema.AttributeName) | ||||
| 		} | ||||
| 	} | ||||
| 
 | ||||
| 	for _, definedAttribute := range out.Table.AttributeDefinitions { | ||||
| 		tableInfo.DefinedAttributes = append(tableInfo.DefinedAttributes, aws.ToString(definedAttribute.AttributeName)) | ||||
| 	} | ||||
| 
 | ||||
| 	return &tableInfo, nil | ||||
| } | ||||
| 
 | ||||
| func (p *Provider) PutItem(ctx context.Context, name string, item models.Item) error { | ||||
| 	_, err := p.client.PutItem(ctx, &dynamodb.PutItemInput{ | ||||
| 		TableName: aws.String(name), | ||||
|  |  | |||
|  | @ -7,6 +7,7 @@ import ( | |||
| ) | ||||
| 
 | ||||
| type TableProvider interface { | ||||
| 	DescribeTable(ctx context.Context, tableName string) (*models.TableInfo, error) | ||||
| 	ScanItems(ctx context.Context, tableName string) ([]models.Item, error) | ||||
| 	DeleteItem(ctx context.Context, tableName string, key map[string]types.AttributeValue) error | ||||
| 	PutItem(ctx context.Context, name string, item models.Item) error | ||||
|  |  | |||
|  | @ -2,7 +2,6 @@ package tables | |||
| 
 | ||||
| import ( | ||||
| 	"context" | ||||
| 	"github.com/aws/aws-sdk-go-v2/service/dynamodb/types" | ||||
| 	"github.com/lmika/awstools/internal/dynamo-browse/models" | ||||
| 	"github.com/pkg/errors" | ||||
| 	"sort" | ||||
|  | @ -18,24 +17,34 @@ func NewService(provider TableProvider) *Service { | |||
| 	} | ||||
| } | ||||
| 
 | ||||
| func (s *Service) Scan(ctx context.Context, table string) (*models.ResultSet, error) { | ||||
| 	results, err := s.provider.ScanItems(ctx, table) | ||||
| 	if err != nil { | ||||
| 		return nil, errors.Wrapf(err, "unable to scan table %v", table) | ||||
| 	} | ||||
| func (s *Service) Describe(ctx context.Context, table string) (*models.TableInfo, error) { | ||||
| 	return s.provider.DescribeTable(ctx, table) | ||||
| } | ||||
| 
 | ||||
| 	// TODO: need to get PKs and SKs from table
 | ||||
| 	pk, sk := "pk", "sk" | ||||
| func (s *Service) Scan(ctx context.Context, tableInfo *models.TableInfo) (*models.ResultSet, error) { | ||||
| 	results, err := s.provider.ScanItems(ctx, tableInfo.Name) | ||||
| 	if err != nil { | ||||
| 		return nil, errors.Wrapf(err, "unable to scan table %v", tableInfo.Name) | ||||
| 	} | ||||
| 
 | ||||
| 	// Get the columns
 | ||||
| 	seenColumns := make(map[string]int) | ||||
| 	seenColumns[pk] = 0 | ||||
| 	seenColumns[sk] = 1 | ||||
| 	seenColumns[tableInfo.Keys.PartitionKey] = 0 | ||||
| 	if tableInfo.Keys.SortKey != "" { | ||||
| 		seenColumns[tableInfo.Keys.SortKey] = 1 | ||||
| 	} | ||||
| 
 | ||||
| 	for _, definedAttribute := range tableInfo.DefinedAttributes { | ||||
| 		if _, seen := seenColumns[definedAttribute]; !seen { | ||||
| 			seenColumns[definedAttribute] = len(seenColumns) | ||||
| 		} | ||||
| 	} | ||||
| 
 | ||||
| 	otherColsRank := len(seenColumns) | ||||
| 	for _, result := range results { | ||||
| 		for k := range result { | ||||
| 			if _, isSeen := seenColumns[k]; !isSeen { | ||||
| 				seenColumns[k] = 2 | ||||
| 				seenColumns[k] = otherColsRank | ||||
| 			} | ||||
| 		} | ||||
| 	} | ||||
|  | @ -51,23 +60,19 @@ func (s *Service) Scan(ctx context.Context, table string) (*models.ResultSet, er | |||
| 		return seenColumns[columns[i]] < seenColumns[columns[j]] | ||||
| 	}) | ||||
| 
 | ||||
| 	models.Sort(results, pk, sk) | ||||
| 	models.Sort(results, tableInfo) | ||||
| 
 | ||||
| 	return &models.ResultSet{ | ||||
| 		Table:   table, | ||||
| 		TableInfo:   tableInfo, | ||||
| 		Columns: columns, | ||||
| 		Items:   results, | ||||
| 	}, nil | ||||
| } | ||||
| 
 | ||||
| func (s *Service) Put(ctx context.Context, tableName string, item models.Item) error { | ||||
| 	return s.provider.PutItem(ctx, tableName, item) | ||||
| func (s *Service) Put(ctx context.Context, tableInfo *models.TableInfo, item models.Item) error { | ||||
| 	return s.provider.PutItem(ctx, tableInfo.Name, item) | ||||
| } | ||||
| 
 | ||||
| func (s *Service) Delete(ctx context.Context, name string, item models.Item) error { | ||||
| 	// TODO: do not hardcode keys
 | ||||
| 	return s.provider.DeleteItem(ctx, name, map[string]types.AttributeValue{ | ||||
| 		"pk": item["pk"], | ||||
| 		"sk": item["sk"], | ||||
| 	}) | ||||
| func (s *Service) Delete(ctx context.Context, tableInfo *models.TableInfo, item models.Item) error { | ||||
| 	return s.provider.DeleteItem(ctx, tableInfo.Name, item.KeyValue(tableInfo)) | ||||
| } | ||||
|  |  | |||
|  | @ -9,6 +9,28 @@ import ( | |||
| 	"testing" | ||||
| ) | ||||
| 
 | ||||
| func TestService_Describe(t *testing.T) { | ||||
| 	tableName := "service-describe-table" | ||||
| 
 | ||||
| 	client, cleanupFn := testdynamo.SetupTestTable(t, tableName, testData) | ||||
| 	defer cleanupFn() | ||||
| 	provider := dynamo.NewProvider(client) | ||||
| 
 | ||||
| 	t.Run("return details of the table", func(t *testing.T) { | ||||
| 		ctx := context.Background() | ||||
| 
 | ||||
| 		service := tables.NewService(provider) | ||||
| 		ti, err := service.Describe(ctx, tableName) | ||||
| 		assert.NoError(t, err) | ||||
| 
 | ||||
| 		// Hash first, then range, then columns in alphabetic order
 | ||||
| 		assert.Equal(t, ti.Name, tableName) | ||||
| 		assert.Equal(t, "pk", ti.Keys.PartitionKey, "pk") | ||||
| 		assert.Equal(t, "sk", ti.Keys.SortKey, "sk") | ||||
| 		assert.Equal(t, []string{"pk", "sk"}, ti.DefinedAttributes) | ||||
| 	}) | ||||
| } | ||||
| 
 | ||||
| func TestService_Scan(t *testing.T) { | ||||
| 	tableName := "service-scan-test-table" | ||||
| 
 | ||||
|  | @ -20,11 +42,14 @@ func TestService_Scan(t *testing.T) { | |||
| 		ctx := context.Background() | ||||
| 
 | ||||
| 		service := tables.NewService(provider) | ||||
| 		rs, err := service.Scan(ctx, tableName) | ||||
| 		ti, err := service.Describe(ctx, tableName) | ||||
| 		assert.NoError(t, err) | ||||
| 
 | ||||
| 		rs, err := service.Scan(ctx, ti) | ||||
| 		assert.NoError(t, err) | ||||
| 
 | ||||
| 		// Hash first, then range, then columns in alphabetic order
 | ||||
| 		assert.Equal(t, rs.Table, tableName) | ||||
| 		assert.Equal(t, rs.TableInfo, ti) | ||||
| 		assert.Equal(t, rs.Columns, []string{"pk", "sk", "alpha", "beta", "gamma"}) | ||||
| 		assert.Equal(t, rs.Items[0], testdynamo.TestRecordAsItem(t, testData[1])) | ||||
| 		assert.Equal(t, rs.Items[1], testdynamo.TestRecordAsItem(t, testData[0])) | ||||
|  |  | |||
|  | @ -71,6 +71,7 @@ func NewModel(dispatcher *dispatcher.Dispatcher, commandController *commandctrl. | |||
| } | ||||
| 
 | ||||
| func (m uiModel) Init() tea.Cmd { | ||||
| 	m.invokeOperation(context.Background(), m.tableReadController.Scan()) | ||||
| 	return nil | ||||
| } | ||||
| 
 | ||||
|  | @ -257,7 +258,7 @@ func (m uiModel) View() string { | |||
| func (m uiModel) headerView() string { | ||||
| 	var titleText string | ||||
| 	if m.state.ResultSet != nil { | ||||
| 		titleText = "Table: " + m.state.ResultSet.Table | ||||
| 		titleText = "Table: " + m.state.ResultSet.TableInfo.Name | ||||
| 	} else { | ||||
| 		titleText = "No table" | ||||
| 	} | ||||
|  |  | |||
		Loading…
	
		Reference in a new issue