diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml new file mode 100644 index 0000000..7ca0952 --- /dev/null +++ b/.github/workflows/ci.yaml @@ -0,0 +1,37 @@ +name: ci + +on: + push: + branches: + - main + - feature/* + pull_request: + branches: + - main + +jobs: + build: + runs-on: ubuntu-latest + + services: + postgres: + image: amazon/dynamodb-local:latest + ports: + - 8000:8000 + steps: + - name: Checkout + uses: actions/checkout@v2 + - name: Setup Go + uses: actions/setup-go@v2 + with: + go-version: 1.17 + - name: Configure + run: | + git config --global url."https://${{ secrets.GO_MODULES_TOKEN }}:x-oauth-basic@github.com/lmika".insteadOf "https://github.com/lmika" + - name: Test + run: | + set -xue + go get . + go test ./... + env: + GOPRIVATE: "github:com/lmika/*" \ No newline at end of file diff --git a/go.mod b/go.mod index 9db652d..781ad70 100644 --- a/go.mod +++ b/go.mod @@ -2,33 +2,39 @@ module github.com/lmika/awstools go 1.17 +require ( + github.com/aws/aws-sdk-go-v2 v1.15.0 + github.com/aws/aws-sdk-go-v2/config v1.13.1 + github.com/aws/aws-sdk-go-v2/service/dynamodb v1.15.0 + github.com/aws/aws-sdk-go-v2/service/sqs v1.16.0 + github.com/calyptia/go-bubble-table v0.1.0 + github.com/charmbracelet/bubbles v0.10.3 + github.com/charmbracelet/bubbletea v0.20.0 + github.com/charmbracelet/lipgloss v0.5.0 + github.com/lmika/events v0.0.0-20200906102219-a2269cd4394e + github.com/lmika/gopkgs v0.0.0-20211210041137-0dc91e939890 + github.com/pkg/errors v0.9.1 +) + require ( github.com/atotto/clipboard v0.1.4 // indirect - github.com/aws/aws-sdk-go-v2 v1.15.0 // indirect - github.com/aws/aws-sdk-go-v2/config v1.13.1 // indirect github.com/aws/aws-sdk-go-v2/credentials v1.8.0 // indirect + github.com/aws/aws-sdk-go-v2/feature/dynamodb/attributevalue v1.8.0 // 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.6 // indirect github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.4.0 // indirect github.com/aws/aws-sdk-go-v2/internal/ini v1.3.5 // indirect - github.com/aws/aws-sdk-go-v2/service/dynamodb v1.15.0 // indirect + github.com/aws/aws-sdk-go-v2/service/dynamodbstreams v1.13.0 // indirect github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.9.0 // indirect github.com/aws/aws-sdk-go-v2/service/internal/endpoint-discovery v1.7.0 // indirect github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.7.0 // indirect - github.com/aws/aws-sdk-go-v2/service/sqs v1.16.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/smithy-go v1.11.1 // indirect - github.com/calyptia/go-bubble-table v0.1.0 // indirect - github.com/charmbracelet/bubbles v0.10.3 // indirect - github.com/charmbracelet/bubbletea v0.20.0 // indirect - github.com/charmbracelet/lipgloss v0.5.0 // indirect github.com/containerd/console v1.0.3 // indirect github.com/davecgh/go-spew v1.1.1 // indirect github.com/jmespath/go-jmespath v0.4.0 // indirect github.com/juju/ansiterm v0.0.0-20210929141451-8b71cc96ebdc // indirect - github.com/lmika/events v0.0.0-20200906102219-a2269cd4394e // indirect - github.com/lmika/gopkgs v0.0.0-20211210041137-0dc91e939890 // 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.14 // indirect @@ -36,8 +42,10 @@ require ( github.com/muesli/ansi v0.0.0-20211018074035-2e021307bc4b // indirect github.com/muesli/reflow v0.3.0 // indirect github.com/muesli/termenv v0.11.1-0.20220212125758-44cd13922739 // indirect - github.com/pkg/errors v0.9.1 // indirect + github.com/pmezard/go-difflib v1.0.0 // indirect github.com/rivo/uniseg v0.2.0 // indirect + github.com/stretchr/testify v1.7.1 // indirect golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c // indirect golang.org/x/term v0.0.0-20210927222741-03fcf44c2211 // indirect + gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c // indirect ) diff --git a/go.sum b/go.sum index 55f630b..c52fe47 100644 --- a/go.sum +++ b/go.sum @@ -8,6 +8,8 @@ github.com/aws/aws-sdk-go-v2/config v1.13.1 h1:yLv8bfNoT4r+UvUKQKqRtdnvuWGMK5a82 github.com/aws/aws-sdk-go-v2/config v1.13.1/go.mod h1:Ba5Z4yL/UGbjQUzsiaN378YobhFo0MLfueXGiOsYtEs= 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/feature/dynamodb/attributevalue v1.8.0 h1:XxTy21xVUkoCZOSGwf+AW22v8aK3eEbYMaGGQ3MbKKk= +github.com/aws/aws-sdk-go-v2/feature/dynamodb/attributevalue v1.8.0/go.mod h1:6WkjzWenkrj3IgLPIPBBz4Qh99jNDF8L4Wj03vfMhAA= 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/internal/configsources v1.1.4 h1:CRiQJ4E2RhfDdqbie1ZYDo8QtIo75Mk7oTdJSfwJTMQ= @@ -22,6 +24,8 @@ github.com/aws/aws-sdk-go-v2/internal/ini v1.3.5 h1:ixotxbfTCFpqbuwFv/RcZwyzhkxP github.com/aws/aws-sdk-go-v2/internal/ini v1.3.5/go.mod h1:R3sWUqPcfXSiF/LSFJhjyJmpg9uV6yP2yv3YZZjldVI= github.com/aws/aws-sdk-go-v2/service/dynamodb v1.15.0 h1:qnx+WyIH9/AD+wAxi05WCMNanO236ceqHg6hChCWs3M= github.com/aws/aws-sdk-go-v2/service/dynamodb v1.15.0/go.mod h1:+Kc1UmbE37ijaAsb3KogW6FR8z0myjX6VtdcCkQEK0k= +github.com/aws/aws-sdk-go-v2/service/dynamodbstreams v1.13.0 h1:s71pGCiLqqGRoUWtdJ2j4PazwEpZVwQc16na/4FfXdk= +github.com/aws/aws-sdk-go-v2/service/dynamodbstreams v1.13.0/go.mod h1:YGzTq/joAih4HRZZtMBWGP4bI8xVucOBQ9RvuanpclA= github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.9.0 h1:uhb7moM7VjqIEpWzTpCvceLDSwrWpaleXm39OnVjuLE= github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.9.0/go.mod h1:pA2St3Pu2Ldy6fBPY45Azoh1WBG4oS7eIKOd4XN7Meg= github.com/aws/aws-sdk-go-v2/service/internal/endpoint-discovery v1.7.0 h1:6Bc0KHhAyxGe15JUHrK+Udw7KhE5LN+5HKZjQGo4yDI= @@ -95,12 +99,15 @@ github.com/muesli/termenv v0.11.1-0.20220212125758-44cd13922739 h1:QANkGiGr39l1E github.com/muesli/termenv v0.11.1-0.20220212125758-44cd13922739/go.mod h1:Bd5NYQ7pd+SrtBSrSNoBBmXlcY8+Xj4BMJgh8qcZrvs= 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/rivo/uniseg v0.1.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= github.com/rivo/uniseg v0.2.0 h1:S1pD9weZBuJdFmowNwbpi7BJ8TNftyUImj/0WQi72jY= github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= github.com/sahilm/fuzzy v0.1.0/go.mod h1:VFvziUEIMCrT6A6tw2RFIXPXXmzXbOsSHF0DOI8ZK9Y= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/testify v1.7.1 h1:5TQK59W5E3v0r2duFAb7P95B6hEeOyEnHRa8MjYSMTY= +github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= @@ -115,3 +122,5 @@ golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8T gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c h1:dUUwHk2QECo/6vqA44rthZ8ie2QXMNeKRTHCNY2nXvo= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/internal/dynamo-browse/models/attrutils.go b/internal/dynamo-browse/models/attrutils.go new file mode 100644 index 0000000..4757814 --- /dev/null +++ b/internal/dynamo-browse/models/attrutils.go @@ -0,0 +1,43 @@ +package models + +import ( + "github.com/aws/aws-sdk-go-v2/service/dynamodb/types" + "math/big" +) + +func compareScalarAttributes(x, y types.AttributeValue) (int, bool) { + switch xVal := x.(type) { + case *types.AttributeValueMemberS: + if yVal, ok := y.(*types.AttributeValueMemberS); ok { + return comparisonValue(xVal.Value == yVal.Value, xVal.Value < yVal.Value), true + } + case *types.AttributeValueMemberN: + if yVal, ok := y.(*types.AttributeValueMemberN); ok { + xNumVal, _, err := big.ParseFloat(xVal.Value, 10, 63, big.ToNearestEven) + if err != nil { + return 0, false + } + + yNumVal, _, err := big.ParseFloat(yVal.Value, 10, 63, big.ToNearestEven) + if err != nil { + return 0, false + } + + return xNumVal.Cmp(yNumVal), true + } + case *types.AttributeValueMemberBOOL: + if yVal, ok := y.(*types.AttributeValueMemberBOOL); ok { + return comparisonValue(xVal.Value == yVal.Value, !xVal.Value), true + } + } + return 0, false +} + +func comparisonValue(isEqual bool, isLess bool) int { + if isEqual { + return 0 + } else if isLess { + return -1 + } + return 1 +} diff --git a/internal/dynamo-browse/models/sorted.go b/internal/dynamo-browse/models/sorted.go new file mode 100644 index 0000000..75a92e7 --- /dev/null +++ b/internal/dynamo-browse/models/sorted.go @@ -0,0 +1,55 @@ +package models + +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 +} + +// Sort sorts the items in place +func Sort(items []Item, pk, sk string) { + si := sortedItems{items: items, pk: pk, sk: sk} + sort.Sort(&si) +} + +func (si *sortedItems) Len() int { + return len(si.items) +} + +func (si *sortedItems) Less(i, j int) bool { + // Compare primary keys + pv1, pv2 := si.items[i][si.pk], si.items[j][si.pk] + pc, ok := 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 + sv1, sv2 := si.items[i][si.sk], si.items[j][si.sk] + sc, ok := compareScalarAttributes(sv1, sv2) + if !ok { + return i < j + } + + if sc < 0 { + return true + } else if sc > 0 { + return false + } + + // This should never happen, but just in case + return i < j +} + +func (si *sortedItems) Swap(i, j int) { + si.items[j], si.items[i] = si.items[i], si.items[j] +} diff --git a/internal/dynamo-browse/models/sorted_test.go b/internal/dynamo-browse/models/sorted_test.go new file mode 100644 index 0000000..674cb2f --- /dev/null +++ b/internal/dynamo-browse/models/sorted_test.go @@ -0,0 +1,103 @@ +package models_test + +import ( + "github.com/aws/aws-sdk-go-v2/service/dynamodb/types" + "github.com/lmika/awstools/internal/dynamo-browse/models" + "github.com/stretchr/testify/assert" + "testing" +) + +func TestSort(t *testing.T) { + t.Run("pk and sk are both strings", func(t *testing.T) { + items := make([]models.Item, len(testStringData)) + copy(items, testStringData) + + models.Sort(items, "pk", "sk") + + assert.Equal(t, items[0], testStringData[1]) + assert.Equal(t, items[1], testStringData[2]) + assert.Equal(t, items[2], testStringData[0]) + }) + + t.Run("pk and sk are both numbers", func(t *testing.T) { + items := make([]models.Item, len(testNumberData)) + copy(items, testNumberData) + + models.Sort(items, "pk", "sk") + + assert.Equal(t, items[0], testNumberData[2]) + assert.Equal(t, items[1], testNumberData[1]) + assert.Equal(t, items[2], testNumberData[0]) + }) + + t.Run("pk and sk are both bools", func(t *testing.T) { + items := make([]models.Item, len(testBoolData)) + copy(items, testBoolData) + + models.Sort(items, "pk", "sk") + + assert.Equal(t, items[0], testBoolData[2]) + assert.Equal(t, items[1], testBoolData[1]) + assert.Equal(t, items[2], testBoolData[0]) + }) +} + +var testStringData = []models.Item{ + { + "pk": &types.AttributeValueMemberS{Value: "bbb"}, + "sk": &types.AttributeValueMemberS{Value: "131"}, + "beta": &types.AttributeValueMemberN{Value: "2468"}, + "gamma": &types.AttributeValueMemberS{Value: "foobar"}, + }, + { + "pk": &types.AttributeValueMemberS{Value: "abc"}, + "sk": &types.AttributeValueMemberS{Value: "111"}, + "alpha": &types.AttributeValueMemberS{Value: "This is some value"}, + }, + { + "pk": &types.AttributeValueMemberS{Value: "abc"}, + "sk": &types.AttributeValueMemberS{Value: "222"}, + "alpha": &types.AttributeValueMemberS{Value: "This is another some value"}, + "beta": &types.AttributeValueMemberN{Value: "2468"}, + }, +} + +var testNumberData = []models.Item{ + { + "pk": &types.AttributeValueMemberN{Value: "1141"}, + "sk": &types.AttributeValueMemberN{Value: "1111"}, + "beta": &types.AttributeValueMemberN{Value: "2468"}, + "gamma": &types.AttributeValueMemberS{Value: "foobar"}, + }, + { + "pk": &types.AttributeValueMemberN{Value: "1141"}, + "sk": &types.AttributeValueMemberN{Value: "111.5"}, + "alpha": &types.AttributeValueMemberS{Value: "This is some value"}, + }, + { + "pk": &types.AttributeValueMemberN{Value: "5"}, + "sk": &types.AttributeValueMemberN{Value: "222"}, + "alpha": &types.AttributeValueMemberS{Value: "This is another some value"}, + "beta": &types.AttributeValueMemberN{Value: "2468"}, + }, +} + +var testBoolData = []models.Item{ + { + "pk": &types.AttributeValueMemberBOOL{Value: true}, + "sk": &types.AttributeValueMemberBOOL{Value: true}, + "beta": &types.AttributeValueMemberN{Value: "2468"}, + "gamma": &types.AttributeValueMemberS{Value: "foobar"}, + }, + { + "pk": &types.AttributeValueMemberBOOL{Value: true}, + "sk": &types.AttributeValueMemberBOOL{Value: false}, + "alpha": &types.AttributeValueMemberS{Value: "This is some value"}, + }, + { + "pk": &types.AttributeValueMemberBOOL{Value: false}, + "sk": &types.AttributeValueMemberBOOL{Value: false}, + "alpha": &types.AttributeValueMemberS{Value: "This is another some value"}, + "beta": &types.AttributeValueMemberN{Value: "2468"}, + }, +} diff --git a/internal/dynamo-browse/providers/dynamo/provider_test.go b/internal/dynamo-browse/providers/dynamo/provider_test.go new file mode 100644 index 0000000..d607374 --- /dev/null +++ b/internal/dynamo-browse/providers/dynamo/provider_test.go @@ -0,0 +1,113 @@ +package dynamo_test + +import ( + "context" + "github.com/aws/aws-sdk-go-v2/service/dynamodb/types" + "github.com/lmika/awstools/internal/dynamo-browse/providers/dynamo" + "github.com/lmika/awstools/test/testdynamo" + "github.com/stretchr/testify/assert" + "testing" +) + +func TestProvider_ScanItems(t *testing.T) { + tableName := "test-table" + + client := testdynamo.SetupTestTable(t, tableName, testData) + provider := dynamo.NewProvider(client) + + t.Run("should return scanned items from the table", func(t *testing.T) { + ctx := context.Background() + + items, err := provider.ScanItems(ctx, tableName) + assert.NoError(t, err) + assert.Len(t, items, 3) + + assert.Contains(t, items, testdynamo.TestRecordAsItem(t, testData[0])) + assert.Contains(t, items, testdynamo.TestRecordAsItem(t, testData[1])) + assert.Contains(t, items, testdynamo.TestRecordAsItem(t, testData[2])) + }) + + t.Run("should return error if table name does not exist", func(t *testing.T) { + ctx := context.Background() + + items, err := provider.ScanItems(ctx, "does-not-exist") + assert.Error(t, err) + assert.Nil(t, items) + }) +} + +func TestProvider_DeleteItem(t *testing.T) { + tableName := "test-table" + + t.Run("should delete item if exists in table", func(t *testing.T) { + client := testdynamo.SetupTestTable(t, tableName, testData) + provider := dynamo.NewProvider(client) + + ctx := context.Background() + + err := provider.DeleteItem(ctx, tableName, map[string]types.AttributeValue{ + "pk": &types.AttributeValueMemberS{Value: "abc"}, + "sk": &types.AttributeValueMemberS{Value: "222"}, + }) + + items, err := provider.ScanItems(ctx, tableName) + assert.NoError(t, err) + assert.Len(t, items, 2) + + assert.Contains(t, items, testdynamo.TestRecordAsItem(t, testData[0])) + assert.Contains(t, items, testdynamo.TestRecordAsItem(t, testData[2])) + assert.NotContains(t, items, testdynamo.TestRecordAsItem(t, testData[1])) + + }) + + t.Run("should do nothing if key does not exist", func(t *testing.T) { + client := testdynamo.SetupTestTable(t, tableName, testData) + provider := dynamo.NewProvider(client) + + ctx := context.Background() + + err := provider.DeleteItem(ctx, tableName, map[string]types.AttributeValue{ + "pk": &types.AttributeValueMemberS{Value: "zyx"}, + "sk": &types.AttributeValueMemberS{Value: "999"}, + }) + + items, err := provider.ScanItems(ctx, tableName) + assert.NoError(t, err) + assert.Len(t, items, 3) + + assert.Contains(t, items, testdynamo.TestRecordAsItem(t, testData[0])) + assert.Contains(t, items, testdynamo.TestRecordAsItem(t, testData[1])) + assert.Contains(t, items, testdynamo.TestRecordAsItem(t, testData[2])) + }) + + t.Run("should return error if table name does not exist", func(t *testing.T) { + client := testdynamo.SetupTestTable(t, tableName, testData) + provider := dynamo.NewProvider(client) + + ctx := context.Background() + + items, err := provider.ScanItems(ctx, "does-not-exist") + assert.Error(t, err) + assert.Nil(t, items) + }) +} + +var testData = testdynamo.TestData{ + { + "pk": "abc", + "sk": "111", + "alpha": "This is some value", + }, + { + "pk": "abc", + "sk": "222", + "alpha": "This is another some value", + "beta": 1231, + }, + { + "pk": "bbb", + "sk": "131", + "beta": 2468, + "gamma": "foobar", + }, +} diff --git a/internal/dynamo-browse/services/tables/service.go b/internal/dynamo-browse/services/tables/service.go index fde7cb0..a3068c1 100644 --- a/internal/dynamo-browse/services/tables/service.go +++ b/internal/dynamo-browse/services/tables/service.go @@ -24,16 +24,18 @@ func (s *Service) Scan(ctx context.Context, table string) (*models.ResultSet, er return nil, errors.Wrapf(err, "unable to scan table %v", table) } - // Get the columns // TODO: need to get PKs and SKs from table + pk, sk := "pk", "sk" + + // Get the columns seenColumns := make(map[string]int) - seenColumns["pk"] = 0 - seenColumns["sk"] = 1 + seenColumns[pk] = 0 + seenColumns[sk] = 1 for _, result := range results { for k := range result { if _, isSeen := seenColumns[k]; !isSeen { - seenColumns[k] = len(seenColumns) + seenColumns[k] = 2 } } } @@ -43,12 +45,17 @@ func (s *Service) Scan(ctx context.Context, table string) (*models.ResultSet, er columns = append(columns, k) } sort.Slice(columns, func(i, j int) bool { + if seenColumns[columns[i]] == seenColumns[columns[j]] { + return columns[i] < columns[j] + } return seenColumns[columns[i]] < seenColumns[columns[j]] }) + models.Sort(results, pk, sk) + return &models.ResultSet{ Columns: columns, - Items: results, + Items: results, }, nil } diff --git a/internal/dynamo-browse/services/tables/service_test.go b/internal/dynamo-browse/services/tables/service_test.go new file mode 100644 index 0000000..e7c4ab0 --- /dev/null +++ b/internal/dynamo-browse/services/tables/service_test.go @@ -0,0 +1,51 @@ +package tables_test + +import ( + "context" + "github.com/lmika/awstools/internal/dynamo-browse/providers/dynamo" + "github.com/lmika/awstools/internal/dynamo-browse/services/tables" + "github.com/lmika/awstools/test/testdynamo" + "github.com/stretchr/testify/assert" + "testing" +) + +func TestService_Scan(t *testing.T) { + tableName := "test-table" + + client := testdynamo.SetupTestTable(t, tableName, testData) + provider := dynamo.NewProvider(client) + + t.Run("return all columns and fields in sorted order", func(t *testing.T) { + ctx := context.Background() + + service := tables.NewService(provider) + rs, err := service.Scan(ctx, tableName) + assert.NoError(t, err) + + // Hash first, then range, then columns in alphabetic order + 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])) + assert.Equal(t, rs.Items[2], testdynamo.TestRecordAsItem(t, testData[2])) + }) +} + +var testData = testdynamo.TestData{ + { + "pk": "abc", + "sk": "222", + "alpha": "This is another some value", + "beta": 1231, + }, + { + "pk": "abc", + "sk": "111", + "alpha": "This is some value", + }, + { + "pk": "bbb", + "sk": "131", + "beta": 2468, + "gamma": "foobar", + }, +} diff --git a/test/testdynamo/client.go b/test/testdynamo/client.go new file mode 100644 index 0000000..901e656 --- /dev/null +++ b/test/testdynamo/client.go @@ -0,0 +1,59 @@ +package testdynamo + +import ( + "context" + "github.com/aws/aws-sdk-go-v2/aws" + "github.com/aws/aws-sdk-go-v2/config" + "github.com/aws/aws-sdk-go-v2/feature/dynamodb/attributevalue" + "github.com/aws/aws-sdk-go-v2/service/dynamodb" + "github.com/aws/aws-sdk-go-v2/service/dynamodb/types" + "github.com/stretchr/testify/assert" + "testing" +) + +type TestData []map[string]interface{} + +func SetupTestTable(t *testing.T, tableName string, testData TestData) *dynamodb.Client { + t.Helper() + ctx := context.Background() + + cfg, err := config.LoadDefaultConfig(ctx) + assert.NoError(t, err) + + dynamoClient := dynamodb.NewFromConfig(cfg, + dynamodb.WithEndpointResolver(dynamodb.EndpointResolverFromURL("http://localhost:8000"))) + + dynamoClient.DeleteTable(ctx, &dynamodb.DeleteTableInput{ + TableName: aws.String(tableName), + }) + + _, err = dynamoClient.CreateTable(ctx, &dynamodb.CreateTableInput{ + TableName: aws.String(tableName), + KeySchema: []types.KeySchemaElement{ + {AttributeName: aws.String("pk"), KeyType: types.KeyTypeHash}, + {AttributeName: aws.String("sk"), KeyType: types.KeyTypeRange}, + }, + AttributeDefinitions: []types.AttributeDefinition{ + {AttributeName: aws.String("pk"), AttributeType: types.ScalarAttributeTypeS}, + {AttributeName: aws.String("sk"), AttributeType: types.ScalarAttributeTypeS}, + }, + ProvisionedThroughput: &types.ProvisionedThroughput{ + ReadCapacityUnits: aws.Int64(100), + WriteCapacityUnits: aws.Int64(100), + }, + }) + assert.NoError(t, err) + + for _, item := range testData { + m, err := attributevalue.MarshalMap(item) + assert.NoError(t, err) + + _, err = dynamoClient.PutItem(ctx, &dynamodb.PutItemInput{ + TableName: aws.String(tableName), + Item: m, + }) + assert.NoError(t, err) + } + + return dynamoClient +} diff --git a/test/testdynamo/helpers.go b/test/testdynamo/helpers.go new file mode 100644 index 0000000..c44743a --- /dev/null +++ b/test/testdynamo/helpers.go @@ -0,0 +1,15 @@ +package testdynamo + +import ( + "github.com/aws/aws-sdk-go-v2/feature/dynamodb/attributevalue" + "github.com/lmika/awstools/internal/dynamo-browse/models" + "github.com/stretchr/testify/assert" + "testing" +) + +func TestRecordAsItem(t *testing.T, item map[string]interface{}) models.Item { + m, err := attributevalue.MarshalMap(item) + assert.NoError(t, err) + + return models.Item(m) +}