Compare commits

...

10 commits

Author SHA1 Message Date
Leon Mika e37b8099a3 Fixed a glaring error where the user cannot close the column selector
Some checks failed
ci / build (push) Has been cancelled
Cause of this was that the close event type was also being used by the related overlay, and the event was being caught by that even though the overlay was hidden.

Also started working on changing the sort order within the column selector by pressing S.
2024-04-02 23:00:19 +11:00
Leon Mika f5bf31a903 Fixed release CI/CD and added 'C' as copy to table binding 2024-03-03 09:48:03 +11:00
Leon Mika 5d95d44a97
Added the rel-picker which can quickly goto related tables
* New rel-picker that can be opened using Shift+O and allows for quickly going to related tables.
2024-03-03 09:20:28 +11:00
Leon Mika 12909c89ee Removed internal os module
Module "os" is no longer needed since Risor comes with an "os" and "exec" module out of the box now.
2024-03-03 08:54:57 +11:00
Leon Mika ceb064a346 Upgraded to Risor 1.4.0 2024-03-03 08:34:41 +11:00
Leon Mika 7ca0cf6982
Converted scripting language Tamarin to Risor (#55)
- Converted Tamarin script language to Risor
- Added a "find" and "merge" method to the result set script type.
- Added the ability to copy the table of results to the pasteboard by pressing C
- Added the -q flag, which will run a query and display the results as a CSV file on the command line
- Upgraded Go to 1.21 in Github actions
- Fix issue with missing limits
- Added the '-where' switch to the mark
- Added the 'marked' function to the query expression.
- Added a sampled time and count on the right-side of the mode line
- Added the 'M' key binding to toggle the marked items
- Started working on tab completion for 'sa' and 'da' commands
- Added count and sample time to the right-side of the mode line
- Added Ctrl+V to the prompt to paste the text of the pasteboard with all whitespace characters trimmed
- Fixed failing unit tests
2023-10-06 15:27:06 +11:00
Leon Mika ed53173a1d
Added the "export -all" switch (#54)
Extended the "export" command with an "-all" flag. When included, all rows of the table matching the query will be exported to CSV.
2023-07-31 20:59:05 +10:00
Leon Mika 20a9a8c758 fix: Added a small timeout to the runNow() script scheduler
This is to avoid a small race conditions in the tests, where a script has signalled that it's finished loading but the schedular has not started waiting for the next task.
2023-07-03 11:24:16 +10:00
Leon Mika f65c5778a9
issue-50: fixed package name (#52)
Changed package name from github.com/lmika/audax to github.com/lmika/dynamo-browse
2023-04-17 08:31:03 +10:00
Leon Mika 4b4d515ade
Added a few changes to query expressions (#51)
- Added the between operator to query expressions.
- Added the using query expression suffix to specify which index to query (or force a scan). This is required if query planning has found multiple indices that can potentially be used.
- Rewrote the types of the query expressions to allow for functions to be defined once, and be useful in queries that result in DynamoDB queries, and evaluation.
- Added some test functions around time and summing numbers.
- Fixed a bug in the del-attr which was not honouring marked rows in a similar way to set-attr: it was only deleting attributes from the first row.
- Added the -to type flag to set-attr which will set the attribute to the value of a query expression.
2023-04-14 15:35:43 +10:00
146 changed files with 3815 additions and 1190 deletions

View file

@ -24,7 +24,7 @@ jobs:
- name: Setup Go
uses: actions/setup-go@v3
with:
go-version: 1.18
go-version: 1.22
- name: Configure
run: |
git config --global url."https://${{ secrets.GO_MODULES_TOKEN }}:x-oauth-basic@github.com/lmika".insteadOf "https://github.com/lmika"

View file

@ -20,7 +20,7 @@ jobs:
- name: Setup Go
uses: actions/setup-go@v3
with:
go-version: 1.18
go-version: 1.22
- name: Configure
run: |
git config --global url."https://${{ secrets.GO_MODULES_TOKEN }}:x-oauth-basic@github.com/lmika".insteadOf "https://github.com/lmika"
@ -41,7 +41,7 @@ jobs:
- name: Setup Go
uses: actions/setup-go@v3
with:
go-version: 1.18
go-version: 1.22
- name: Configure
run: |
git config --global url."https://${{ secrets.GO_MODULES_TOKEN }}:x-oauth-basic@github.com/lmika".insteadOf "https://github.com/lmika"
@ -52,7 +52,7 @@ jobs:
- name: Release
if: startsWith(github.ref, 'refs/tags/')
run: |
goreleaser release -f macos.goreleaser.yml --skip-validate --rm-dist
goreleaser release -f macos.goreleaser.yml --skip=validate --clean
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
HOMEBREW_GITHUB_TOKEN: ${{ secrets.HOMEBREW_GITHUB_TOKEN }}
@ -66,7 +66,7 @@ jobs:
- name: Setup Go
uses: actions/setup-go@v3
with:
go-version: 1.18
go-version: 1.22
- name: Configure
run: |
git config --global url."https://${{ secrets.GO_MODULES_TOKEN }}:x-oauth-basic@github.com/lmika".insteadOf "https://github.com/lmika"
@ -75,7 +75,7 @@ jobs:
if: startsWith(github.ref, 'refs/tags/')
with:
version: latest
args: release -f linux.goreleaser.yml --skip-validate --rm-dist
args: release -f linux.goreleaser.yml --skip=validate --clean
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
HOMEBREW_GITHUB_TOKEN: ${{ secrets.HOMEBREW_GITHUB_TOKEN }}

View file

@ -4,33 +4,36 @@ import (
"context"
"flag"
"fmt"
"github.com/aws/aws-sdk-go-v2/config"
"github.com/aws/aws-sdk-go-v2/service/dynamodb"
tea "github.com/charmbracelet/bubbletea"
"github.com/lmika/audax/internal/common/ui/commandctrl"
"github.com/lmika/audax/internal/common/ui/logging"
"github.com/lmika/audax/internal/common/ui/osstyle"
"github.com/lmika/audax/internal/common/workspaces"
"github.com/lmika/audax/internal/dynamo-browse/controllers"
"github.com/lmika/audax/internal/dynamo-browse/providers/dynamo"
"github.com/lmika/audax/internal/dynamo-browse/providers/inputhistorystore"
"github.com/lmika/audax/internal/dynamo-browse/providers/settingstore"
"github.com/lmika/audax/internal/dynamo-browse/providers/workspacestore"
"github.com/lmika/audax/internal/dynamo-browse/services/inputhistory"
"github.com/lmika/audax/internal/dynamo-browse/services/itemrenderer"
"github.com/lmika/audax/internal/dynamo-browse/services/jobs"
keybindings_service "github.com/lmika/audax/internal/dynamo-browse/services/keybindings"
"github.com/lmika/audax/internal/dynamo-browse/services/scriptmanager"
"github.com/lmika/audax/internal/dynamo-browse/services/tables"
"github.com/lmika/audax/internal/dynamo-browse/services/viewsnapshot"
"github.com/lmika/audax/internal/dynamo-browse/ui"
"github.com/lmika/audax/internal/dynamo-browse/ui/keybindings"
"github.com/lmika/audax/internal/dynamo-browse/ui/teamodels/styles"
bus "github.com/lmika/events"
"github.com/lmika/gopkgs/cli"
"log"
"net"
"os"
"github.com/aws/aws-sdk-go-v2/config"
"github.com/aws/aws-sdk-go-v2/service/dynamodb"
tea "github.com/charmbracelet/bubbletea"
"github.com/lmika/dynamo-browse/internal/common/ui/commandctrl"
"github.com/lmika/dynamo-browse/internal/common/ui/logging"
"github.com/lmika/dynamo-browse/internal/common/ui/osstyle"
"github.com/lmika/dynamo-browse/internal/common/workspaces"
"github.com/lmika/dynamo-browse/internal/dynamo-browse/controllers"
"github.com/lmika/dynamo-browse/internal/dynamo-browse/models/queryexpr"
"github.com/lmika/dynamo-browse/internal/dynamo-browse/providers/dynamo"
"github.com/lmika/dynamo-browse/internal/dynamo-browse/providers/inputhistorystore"
"github.com/lmika/dynamo-browse/internal/dynamo-browse/providers/pasteboardprovider"
"github.com/lmika/dynamo-browse/internal/dynamo-browse/providers/settingstore"
"github.com/lmika/dynamo-browse/internal/dynamo-browse/providers/workspacestore"
"github.com/lmika/dynamo-browse/internal/dynamo-browse/services/inputhistory"
"github.com/lmika/dynamo-browse/internal/dynamo-browse/services/itemrenderer"
"github.com/lmika/dynamo-browse/internal/dynamo-browse/services/jobs"
keybindings_service "github.com/lmika/dynamo-browse/internal/dynamo-browse/services/keybindings"
"github.com/lmika/dynamo-browse/internal/dynamo-browse/services/scriptmanager"
"github.com/lmika/dynamo-browse/internal/dynamo-browse/services/tables"
"github.com/lmika/dynamo-browse/internal/dynamo-browse/services/viewsnapshot"
"github.com/lmika/dynamo-browse/internal/dynamo-browse/ui"
"github.com/lmika/dynamo-browse/internal/dynamo-browse/ui/keybindings"
"github.com/lmika/dynamo-browse/internal/dynamo-browse/ui/teamodels/styles"
bus "github.com/lmika/events"
"github.com/lmika/gopkgs/cli"
)
func main() {
@ -40,6 +43,7 @@ func main() {
var flagRO = flag.Bool("ro", false, "enable readonly mode")
var flagDefaultLimit = flag.Int("default-limit", 0, "default limit for queries and scans")
var flagWorkspace = flag.String("w", "", "workspace file")
var flagQuery = flag.String("q", "", "run query")
flag.Parse()
ctx := context.Background()
@ -84,6 +88,7 @@ func main() {
resultSetSnapshotStore := workspacestore.NewResultSetSnapshotStore(ws)
settingStore := settingstore.New(ws)
inputHistoryStore := inputhistorystore.NewInputHistoryStore(ws)
pasteboardProvider := pasteboardprovider.New()
if *flagRO {
if err := settingStore.SetReadOnly(*flagRO); err != nil {
@ -105,19 +110,58 @@ func main() {
state := controllers.NewState()
jobsController := controllers.NewJobsController(jobsService, eventBus, false)
tableReadController := controllers.NewTableReadController(state, tableService, workspaceService, itemRendererService, jobsController, inputHistoryService, eventBus, *flagTable)
tableReadController := controllers.NewTableReadController(
state,
tableService,
workspaceService,
itemRendererService,
jobsController,
inputHistoryService,
eventBus,
pasteboardProvider,
scriptManagerService,
*flagTable,
)
tableWriteController := controllers.NewTableWriteController(state, tableService, jobsController, tableReadController, settingStore)
columnsController := controllers.NewColumnsController(eventBus)
exportController := controllers.NewExportController(state, columnsController)
columnsController := controllers.NewColumnsController(tableReadController, eventBus)
exportController := controllers.NewExportController(state, tableService, jobsController, columnsController, pasteboardProvider)
settingsController := controllers.NewSettingsController(settingStore, eventBus)
keyBindings := keybindings.Default()
scriptController := controllers.NewScriptController(scriptManagerService, tableReadController, settingsController, eventBus)
scriptController := controllers.NewScriptController(scriptManagerService, tableReadController, jobsController, settingsController, eventBus)
if *flagQuery != "" {
if *flagTable == "" {
cli.Fatalf("-t will need to be set for -q")
}
ctx := context.Background()
query, err := queryexpr.Parse(*flagQuery)
if err != nil {
cli.Fatalf("query: %v", err)
}
ti, err := tableService.Describe(ctx, *flagTable)
if err != nil {
cli.Fatalf("cannot describe table: %v", err)
}
rs, err := tableService.ScanOrQuery(ctx, ti, query, nil)
if err != nil {
cli.Fatalf("cannot execute query: %v", err)
}
if err := exportController.ExportToWriter(os.Stdout, rs); err != nil {
cli.Fatalf("cannot export results of query: %v", err)
}
return
}
keyBindingService := keybindings_service.NewService(keyBindings)
keyBindingController := controllers.NewKeyBindingController(keyBindingService, scriptController)
commandController := commandctrl.NewCommandController(inputHistoryService)
commandController.AddCommandLookupExtension(scriptController)
commandController.SetCommandCompletionProvider(columnsController)
model := ui.NewModel(
tableReadController,
@ -131,6 +175,7 @@ func main() {
scriptController,
eventBus,
keyBindingController,
pasteboardProvider,
keyBindings,
)

82
go.mod
View file

@ -1,17 +1,19 @@
module github.com/lmika/audax
module github.com/lmika/dynamo-browse
go 1.18
go 1.22
toolchain go1.22.0
require (
github.com/alecthomas/participle/v2 v2.0.0-beta.5
github.com/asdine/storm v2.1.2+incompatible
github.com/aws/aws-sdk-go-v2 v1.17.4
github.com/aws/aws-sdk-go-v2/config v1.13.1
github.com/aws/aws-sdk-go-v2/credentials v1.8.0
github.com/aws/aws-sdk-go-v2 v1.18.1
github.com/aws/aws-sdk-go-v2/config v1.18.27
github.com/aws/aws-sdk-go-v2/credentials v1.13.26
github.com/aws/aws-sdk-go-v2/feature/dynamodb/attributevalue v1.10.12
github.com/aws/aws-sdk-go-v2/feature/dynamodb/expression v1.4.39
github.com/aws/aws-sdk-go-v2/service/dynamodb v1.18.3
github.com/aws/aws-sdk-go-v2/service/sqs v1.16.0
github.com/aws/aws-sdk-go-v2/service/dynamodb v1.19.11
github.com/aws/aws-sdk-go-v2/service/sqs v1.23.2
github.com/aws/aws-sdk-go-v2/service/ssm v1.24.0
github.com/brianvoe/gofakeit/v6 v6.15.0
github.com/calyptia/go-bubble-table v0.2.1
@ -27,7 +29,7 @@ require (
github.com/muesli/ansi v0.0.0-20211031195517-c9f0611b6c70
github.com/muesli/reflow v0.3.0
github.com/pkg/errors v0.9.1
github.com/stretchr/testify v1.8.1
github.com/stretchr/testify v1.8.4
golang.design/x/clipboard v0.6.2
golang.org/x/exp v0.0.0-20230108222341-4b8118a2686a
)
@ -35,38 +37,68 @@ require (
require (
github.com/DataDog/zstd v1.5.2 // indirect
github.com/Sereal/Sereal v0.0.0-20220220040404-e0d1e550e879 // indirect
github.com/anthonynsimon/bild v0.13.0 // indirect
github.com/atotto/clipboard v0.1.4 // 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.28 // indirect
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.4.22 // indirect
github.com/aws/aws-sdk-go-v2/internal/ini v1.3.5 // indirect
github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.4.10 // indirect
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.13.4 // indirect
github.com/aws/aws-sdk-go-v2/internal/configsources v1.1.34 // indirect
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.4.28 // indirect
github.com/aws/aws-sdk-go-v2/internal/ini v1.3.35 // indirect
github.com/aws/aws-sdk-go-v2/internal/v4a v1.0.26 // indirect
github.com/aws/aws-sdk-go-v2/service/cloudformation v1.30.0 // indirect
github.com/aws/aws-sdk-go-v2/service/cloudfront v1.26.8 // indirect
github.com/aws/aws-sdk-go-v2/service/cloudtrail v1.27.1 // indirect
github.com/aws/aws-sdk-go-v2/service/cloudwatch v1.26.2 // indirect
github.com/aws/aws-sdk-go-v2/service/dynamodbstreams v1.14.3 // indirect
github.com/aws/aws-sdk-go-v2/service/ebs v1.16.14 // indirect
github.com/aws/aws-sdk-go-v2/service/ec2 v1.102.0 // indirect
github.com/aws/aws-sdk-go-v2/service/ecr v1.18.13 // indirect
github.com/aws/aws-sdk-go-v2/service/ecs v1.27.4 // indirect
github.com/aws/aws-sdk-go-v2/service/eks v1.27.14 // indirect
github.com/aws/aws-sdk-go-v2/service/elasticache v1.27.2 // indirect
github.com/aws/aws-sdk-go-v2/service/elasticsearchservice v1.19.2 // indirect
github.com/aws/aws-sdk-go-v2/service/glue v1.52.0 // indirect
github.com/aws/aws-sdk-go-v2/service/iam v1.21.0 // indirect
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.9.11 // indirect
github.com/aws/aws-sdk-go-v2/service/internal/endpoint-discovery v1.7.22 // indirect
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.7.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/aws-sdk-go-v2/service/internal/checksum v1.1.29 // indirect
github.com/aws/aws-sdk-go-v2/service/internal/endpoint-discovery v1.7.28 // indirect
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.9.28 // indirect
github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.14.3 // indirect
github.com/aws/aws-sdk-go-v2/service/kinesis v1.17.14 // indirect
github.com/aws/aws-sdk-go-v2/service/kms v1.22.2 // indirect
github.com/aws/aws-sdk-go-v2/service/lambda v1.37.0 // indirect
github.com/aws/aws-sdk-go-v2/service/rds v1.46.0 // indirect
github.com/aws/aws-sdk-go-v2/service/redshift v1.28.0 // indirect
github.com/aws/aws-sdk-go-v2/service/route53 v1.28.3 // indirect
github.com/aws/aws-sdk-go-v2/service/s3 v1.36.0 // indirect
github.com/aws/aws-sdk-go-v2/service/sfn v1.18.0 // indirect
github.com/aws/aws-sdk-go-v2/service/sns v1.20.13 // indirect
github.com/aws/aws-sdk-go-v2/service/sso v1.12.12 // indirect
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.14.12 // indirect
github.com/aws/aws-sdk-go-v2/service/sts v1.19.2 // indirect
github.com/aws/aws-sdk-go-v2/service/wafv2 v1.35.1 // indirect
github.com/aws/smithy-go v1.13.5 // indirect
github.com/aymanbagabas/go-osc52 v1.0.3 // indirect
github.com/containerd/console v1.0.3 // indirect
github.com/davecgh/go-spew v1.1.1 // indirect
github.com/gofrs/uuid v4.3.1+incompatible // indirect
github.com/gofrs/uuid v4.4.0+incompatible // indirect
github.com/golang/protobuf v1.5.2 // indirect
github.com/golang/snappy v0.0.4 // indirect
github.com/hashicorp/errwrap v1.0.0 // indirect
github.com/hashicorp/go-multierror v1.1.1 // indirect
github.com/jackc/pgpassfile v1.0.0 // indirect
github.com/jackc/pgservicefile v0.0.0-20200714003250-2b9c44734f2b // indirect
github.com/jackc/pgx/v5 v5.0.4 // indirect
github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a // indirect
github.com/jackc/pgx/v5 v5.4.1 // indirect
github.com/jmespath/go-jmespath v0.4.0 // indirect
github.com/juju/ansiterm v0.0.0-20210929141451-8b71cc96ebdc // 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.16 // indirect
github.com/mattn/go-isatty v0.0.17 // indirect
github.com/mattn/go-localereader v0.0.1 // indirect
github.com/muesli/cancelreader v0.2.2 // indirect
github.com/muesli/termenv v0.13.0 // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect
github.com/risor-io/risor v1.4.0 // indirect
github.com/rivo/uniseg v0.4.2 // indirect
github.com/sahilm/fuzzy v0.1.0 // indirect
github.com/stretchr/objx v0.5.0 // indirect
@ -76,13 +108,13 @@ require (
github.com/vmihailenco/msgpack v4.0.4+incompatible // indirect
github.com/wI2L/jsondiff v0.3.0 // indirect
go.etcd.io/bbolt v1.3.6 // indirect
golang.org/x/crypto v0.0.0-20220829220503-c86fa9a7ed90 // indirect
golang.org/x/crypto v0.9.0 // indirect
golang.org/x/exp/shiny v0.0.0-20230213192124-5e25df0256eb // indirect
golang.org/x/image v0.0.0-20211028202545-6944b10bf410 // indirect
golang.org/x/image v0.5.0 // indirect
golang.org/x/mobile v0.0.0-20210716004757-34ab1303b554 // indirect
golang.org/x/sys v0.1.0 // indirect
golang.org/x/term v0.0.0-20220919170432-7a66f970e087 // indirect
golang.org/x/text v0.3.8 // indirect
golang.org/x/sys v0.8.0 // indirect
golang.org/x/term v0.8.0 // indirect
golang.org/x/text v0.9.0 // indirect
google.golang.org/appengine v1.6.7 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
)

151
go.sum
View file

@ -1,3 +1,4 @@
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo=
github.com/DataDog/zstd v1.5.2 h1:vUG4lAyuPCXO0TLbXvPv7EB7cNK1QV/luu55UHLrrn8=
github.com/DataDog/zstd v1.5.2/go.mod h1:g4AWEaM3yOg3HYfnJ3YIawPnVdXJh9QME85blwSAmyw=
@ -7,6 +8,9 @@ github.com/alecthomas/assert/v2 v2.0.3 h1:WKqJODfOiQG0nEJKFKzDIG3E29CN2/4zR9XGJz
github.com/alecthomas/participle/v2 v2.0.0-beta.5 h1:y6dsSYVb1G5eK6mgmy+BgI3Mw35a3WghArZ/Hbebrjo=
github.com/alecthomas/participle/v2 v2.0.0-beta.5/go.mod h1:RC764t6n4L8D8ITAJv0qdokritYSNR3wV5cVwmIEaMM=
github.com/alecthomas/repr v0.1.0 h1:ENn2e1+J3k09gyj2shc0dHr/yjaWSHRlrJ4DPMevDqE=
github.com/anthonynsimon/bild v0.13.0 h1:mN3tMaNds1wBWi1BrJq0ipDBhpkooYfu7ZFSMhXt1C8=
github.com/anthonynsimon/bild v0.13.0/go.mod h1:tpzzp0aYkAsMi1zmfhimaDyX1xjn2OUc1AJZK/TF0AE=
github.com/armon/consul-api v0.0.0-20180202201655-eb2c6b5be1b6/go.mod h1:grANhF5doyWs3UAsr3K4I6qtAmlQcZDesFNEHPZAzj8=
github.com/asdine/storm v2.1.2+incompatible h1:dczuIkyqwY2LrtXPz8ixMrU/OFgZp71kbKTHGrXYt/Q=
github.com/asdine/storm v2.1.2+incompatible/go.mod h1:RarYDc9hq1UPLImuiXK3BIWPJLdIygvV3PsInK0FbVQ=
github.com/atotto/clipboard v0.1.4 h1:EH0zSVneZPSuFR11BlR9YppQTVDbh5+16AmcJi4g1z4=
@ -15,44 +19,126 @@ github.com/aws/aws-sdk-go-v2 v1.13.0/go.mod h1:L6+ZpqHaLbAaxsqV0L4cvxZY7QupWJB4f
github.com/aws/aws-sdk-go-v2 v1.16.1/go.mod h1:ytwTPBG6fXTZLxxeeCCWj2/EMYp/xDUgX+OET6TLNNU=
github.com/aws/aws-sdk-go-v2 v1.17.4 h1:wyC6p9Yfq6V2y98wfDsj6OnNQa4w2BLGCLIxzNhwOGY=
github.com/aws/aws-sdk-go-v2 v1.17.4/go.mod h1:uzbQtefpm44goOPmdKyAlXSNcwlRgF3ePWVW6EtJvvw=
github.com/aws/aws-sdk-go-v2 v1.18.1 h1:+tefE750oAb7ZQGzla6bLkOwfcQCEtC5y2RqoqCeqKo=
github.com/aws/aws-sdk-go-v2 v1.18.1/go.mod h1:uzbQtefpm44goOPmdKyAlXSNcwlRgF3ePWVW6EtJvvw=
github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.4.10 h1:dK82zF6kkPeCo8J1e+tGx4JdvDIQzj7ygIoLg8WMuGs=
github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.4.10/go.mod h1:VeTZetY5KRJLuD/7fkQXMU6Mw7H5m/KP2J5Iy9osMno=
github.com/aws/aws-sdk-go-v2/config v1.13.1 h1:yLv8bfNoT4r+UvUKQKqRtdnvuWGMK5a82l4ru9Jvnuo=
github.com/aws/aws-sdk-go-v2/config v1.13.1/go.mod h1:Ba5Z4yL/UGbjQUzsiaN378YobhFo0MLfueXGiOsYtEs=
github.com/aws/aws-sdk-go-v2/config v1.18.27 h1:Az9uLwmssTE6OGTpsFqOnaGpLnKDqNYOJzWuC6UAYzA=
github.com/aws/aws-sdk-go-v2/config v1.18.27/go.mod h1:0My+YgmkGxeqjXZb5BYme5pc4drjTnM+x1GJ3zv42Nw=
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/credentials v1.13.26 h1:qmU+yhKmOCyujmuPY7tf5MxR/RKyZrOPO3V4DobiTUk=
github.com/aws/aws-sdk-go-v2/credentials v1.13.26/go.mod h1:GoXt2YC8jHUBbA4jr+W3JiemnIbkXOfxSXcisUsZ3os=
github.com/aws/aws-sdk-go-v2/feature/dynamodb/attributevalue v1.10.12 h1:ama2cD4WaH6+8Gq/M/g+ZumPmmqCyanr+6Sm+iJVxfA=
github.com/aws/aws-sdk-go-v2/feature/dynamodb/attributevalue v1.10.12/go.mod h1:tPnUO5mS3JThpwfq4Q8iPd745s7yh6fGPqDUEBw+Wv4=
github.com/aws/aws-sdk-go-v2/feature/dynamodb/expression v1.4.39 h1:PhgfvgqwMFQKwOcxLV7V3lNDVnR3ZUWzoB6T9oCFpR4=
github.com/aws/aws-sdk-go-v2/feature/dynamodb/expression v1.4.39/go.mod h1:/GkvC7uHpK50ilKkKx9I2gZiI/ieZbKjS2aah1rT9uE=
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/feature/ec2/imds v1.13.4 h1:LxK/bitrAr4lnh9LnIS6i7zWbCOdMsfzKFBI6LUCS0I=
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.13.4/go.mod h1:E1hLXN/BL2e6YizK1zFlYd8vsfi2GTjbjBazinMmeaM=
github.com/aws/aws-sdk-go-v2/internal/configsources v1.1.4/go.mod h1:XHgQ7Hz2WY2GAn//UXHofLfPXWh+s62MbMOijrg12Lw=
github.com/aws/aws-sdk-go-v2/internal/configsources v1.1.8/go.mod h1:LnTQMTqbKsbtt+UI5+wPsB7jedW+2ZgozoPG8k6cMxg=
github.com/aws/aws-sdk-go-v2/internal/configsources v1.1.28 h1:r+XwaCLpIvCKjBIYy/HVZujQS9tsz5ohHG3ZIe0wKoE=
github.com/aws/aws-sdk-go-v2/internal/configsources v1.1.28/go.mod h1:3lwChorpIM/BhImY/hy+Z6jekmN92cXGPI1QJasVPYY=
github.com/aws/aws-sdk-go-v2/internal/configsources v1.1.34 h1:A5UqQEmPaCFpedKouS4v+dHCTUo2sKqhoKO9U5kxyWo=
github.com/aws/aws-sdk-go-v2/internal/configsources v1.1.34/go.mod h1:wZpTEecJe0Btj3IYnDx/VlUzor9wm3fJHyvLpQF0VwY=
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.2.0/go.mod h1:BsCSJHx5DnDXIrOcqB8KN1/B+hXLG/bi4Y6Vjcx/x9E=
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.4.2/go.mod h1:1x4ZP3Z8odssdhuLI+/1Tqw6Pt/VAaP4Tr8EUxHvPXE=
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.4.22 h1:7AwGYXDdqRQYsluvKFmWoqpcOQJ4bH634SkYf3FNj/A=
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.4.22/go.mod h1:EqK7gVrIGAHyZItrD1D8B0ilgwMD1GiWAmbU4u/JHNk=
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.4.28 h1:srIVS45eQuewqz6fKKu6ZGXaq6FuFg5NzgQBAM6g8Y4=
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.4.28/go.mod h1:7VRpKQQedkfIEXb4k52I7swUnZP0wohVajJMRn3vsUw=
github.com/aws/aws-sdk-go-v2/internal/ini v1.3.5 h1:ixotxbfTCFpqbuwFv/RcZwyzhkxPSYDYEMcj4niB5Uk=
github.com/aws/aws-sdk-go-v2/internal/ini v1.3.5/go.mod h1:R3sWUqPcfXSiF/LSFJhjyJmpg9uV6yP2yv3YZZjldVI=
github.com/aws/aws-sdk-go-v2/internal/ini v1.3.35 h1:LWA+3kDM8ly001vJ1X1waCuLJdtTl48gwkPKWy9sosI=
github.com/aws/aws-sdk-go-v2/internal/ini v1.3.35/go.mod h1:0Eg1YjxE0Bhn56lx+SHJwCzhW+2JGtizsrx+lCqrfm0=
github.com/aws/aws-sdk-go-v2/internal/v4a v1.0.26 h1:wscW+pnn3J1OYnanMnza5ZVYXLX4cKk5rAvUAl4Qu+c=
github.com/aws/aws-sdk-go-v2/internal/v4a v1.0.26/go.mod h1:MtYiox5gvyB+OyP0Mr0Sm/yzbEAIPL9eijj/ouHAPw0=
github.com/aws/aws-sdk-go-v2/service/cloudformation v1.30.0 h1:XbDkc4FLeg1RfnqeblfbJvaEabqq9ByZl4zqyPFkfSc=
github.com/aws/aws-sdk-go-v2/service/cloudformation v1.30.0/go.mod h1:SwQFcCs9Rog8hSHm+81KBkAK+UKLXErA/1ChaEI8mLE=
github.com/aws/aws-sdk-go-v2/service/cloudfront v1.26.8 h1:loRDtQ0vT0+JCB0hQBCfv95tttEzJ1rqSaTDy5cpy0A=
github.com/aws/aws-sdk-go-v2/service/cloudfront v1.26.8/go.mod h1:YTd4wGn2beCF9wkSTpEcupk79zDFYJk2Ca76B8YyvJg=
github.com/aws/aws-sdk-go-v2/service/cloudtrail v1.27.1 h1:Qw1G/M7eanpm6s/URkG1UuRLKEnRnpUvkUb7NMVvWb8=
github.com/aws/aws-sdk-go-v2/service/cloudtrail v1.27.1/go.mod h1:oKRYqorIUkfAVmX03+lpv3tW5WelDpaliqzTwmCj/k8=
github.com/aws/aws-sdk-go-v2/service/cloudwatch v1.26.2 h1:PWGu2JhCb/XJlJ7SSFJq76pxk4xWsN76nZxh7TzMHx0=
github.com/aws/aws-sdk-go-v2/service/cloudwatch v1.26.2/go.mod h1:2KOZkkzMDZCo/aLzPhys06mHNkiU74u85aMJA3PLRvg=
github.com/aws/aws-sdk-go-v2/service/dynamodb v1.18.3 h1:MxOpCZ+o9+AIeQHi2ocW7H4D7p0LhEkmetETVvDnkvg=
github.com/aws/aws-sdk-go-v2/service/dynamodb v1.18.3/go.mod h1:nkpC9xkh+3vdxmhqN8Ac10pgV14DsJDLzUsV2CcS+44=
github.com/aws/aws-sdk-go-v2/service/dynamodb v1.19.11 h1:tLTGNAsazbfjfjW1k/i43kyCcyTTTTFaD93H7JbSbbs=
github.com/aws/aws-sdk-go-v2/service/dynamodb v1.19.11/go.mod h1:W1oiFegjVosgjIwb2Vv45jiCQT1ee8x85u8EyZRYLes=
github.com/aws/aws-sdk-go-v2/service/dynamodbstreams v1.14.3 h1:B+bkmCnNJi194pu9aTtYUe8f4EPXafC+xfU+zciVxdg=
github.com/aws/aws-sdk-go-v2/service/dynamodbstreams v1.14.3/go.mod h1:bRphLmXQD9Ux4jLcFEwyrWdmuPTj2Lh8VGl9wILuJII=
github.com/aws/aws-sdk-go-v2/service/ebs v1.16.14 h1:DosI4CvEUo6/V21pDspzYkOa2X3Zwy5XS/cbPFiqDv0=
github.com/aws/aws-sdk-go-v2/service/ebs v1.16.14/go.mod h1:yVTqVHjnrbAj6FvhTQfjNgwQbjPbDUUvA1x4IpXFmrE=
github.com/aws/aws-sdk-go-v2/service/ec2 v1.102.0 h1:P4dyjm49F2kKws0FpouBC6fjVImACXKt752+CWa01lM=
github.com/aws/aws-sdk-go-v2/service/ec2 v1.102.0/go.mod h1:tIctCeX9IbzsUTKHt53SVEcgyfxV2ElxJeEB+QUbc4M=
github.com/aws/aws-sdk-go-v2/service/ecr v1.18.13 h1:hF7MUVNjubetjggZDtn3AmqCJzD7EUi//tSdxMYPm7U=
github.com/aws/aws-sdk-go-v2/service/ecr v1.18.13/go.mod h1:XwEFO35g0uN/SftK0asWxh8Rk6DOx37R83TmWe2tzEE=
github.com/aws/aws-sdk-go-v2/service/ecs v1.27.4 h1:F1N0Eh5EGRRY9QpF+tMTkx8Wb59DkQWE91Xza/9dk1c=
github.com/aws/aws-sdk-go-v2/service/ecs v1.27.4/go.mod h1:0irnFofeEZwT7uTjSkNVcSQJbWRqZ9BRoxhKjt1BObM=
github.com/aws/aws-sdk-go-v2/service/eks v1.27.14 h1:47HQVuJXgwvuoc4AT3rVdm77H0qGFbFnsuE4PRT+xX0=
github.com/aws/aws-sdk-go-v2/service/eks v1.27.14/go.mod h1:QxuWcm9rlLkW3aEV8tiDzqZewnNSNUZfnqJvo1Nv9A0=
github.com/aws/aws-sdk-go-v2/service/elasticache v1.27.2 h1:IC9XLGcT3yEkziTlX7PX54km7cHJnltlV7Ppwq2+7ik=
github.com/aws/aws-sdk-go-v2/service/elasticache v1.27.2/go.mod h1:+oJhn/SIud310/2LLSVmlNZmExmlYPaGCLmUsnq5JZc=
github.com/aws/aws-sdk-go-v2/service/elasticsearchservice v1.19.2 h1:Zam6yofBgdtP13laNoeA+DA9wlKJNooU8p3CWw6xLaI=
github.com/aws/aws-sdk-go-v2/service/elasticsearchservice v1.19.2/go.mod h1:dehjpZ00q0RJcBUOUEysaj7zHK2rHSS4ePp89MsFiaI=
github.com/aws/aws-sdk-go-v2/service/glue v1.52.0 h1:ukSf8ZdoZ6AygsUWIjj177wLOXljxBspBaNMgvx6fRA=
github.com/aws/aws-sdk-go-v2/service/glue v1.52.0/go.mod h1:wMCE0B6l8eHb57l2DMYCGxt0rHIfcu3RvIY7SAfc+Fs=
github.com/aws/aws-sdk-go-v2/service/iam v1.21.0 h1:8hEpu60CWlrp7iEBUFRZhgPoX6+gadaGL1sD4LoRYS0=
github.com/aws/aws-sdk-go-v2/service/iam v1.21.0/go.mod h1:aQZ8BI+reeaY7RI/QQp7TKCSUHOesTdrzzylp3CW85c=
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.9.11 h1:y2+VQzC6Zh2ojtV2LoC0MNwHWc6qXv/j2vrQtlftkdA=
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.9.11/go.mod h1:iV4q2hsqtNECrfmlXyord9u4zyuFEJX9eLgLpSPzWA8=
github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.1.29 h1:zZSLP3v3riMOP14H7b4XP0uyfREDQOYv2cqIrvTXDNQ=
github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.1.29/go.mod h1:z7EjRjVwZ6pWcWdI2H64dKttvzaP99jRIj5hphW0M5U=
github.com/aws/aws-sdk-go-v2/service/internal/endpoint-discovery v1.7.22 h1:6zEryIiJOSk5/OcVHzkPDwzNBQ2atYCTShyA7TqkuxA=
github.com/aws/aws-sdk-go-v2/service/internal/endpoint-discovery v1.7.22/go.mod h1:moeOz5SKfY0p6pNIChdPIQdfaUfWI67+OVe0/r6+aGY=
github.com/aws/aws-sdk-go-v2/service/internal/endpoint-discovery v1.7.28 h1:/D994rtMQd1jQ2OY+7tvUlMlrv1L1c7Xtma/FhkbVtY=
github.com/aws/aws-sdk-go-v2/service/internal/endpoint-discovery v1.7.28/go.mod h1:3bJI2pLY3ilrqO5EclusI1GbjFJh1iXYrhOItf2sjKw=
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.7.0 h1:4QAOB3KrvI1ApJK14sliGr3Ie2pjyvNypn/lfzDHfUw=
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.7.0/go.mod h1:K/qPe6AP2TGYv4l6n7c88zh9jWBDf6nHhvg1fx/EWfU=
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.9.28 h1:bkRyG4a929RCnpVSTvLM2j/T4ls015ZhhYApbmYs15s=
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.9.28/go.mod h1:jj7znCIg05jXlaGBlFMGP8+7UN3VtCkRBG2spnmRQkU=
github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.14.3 h1:dBL3StFxHtpBzJJ/mNEsjXVgfO+7jR0dAIEwLqMapEA=
github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.14.3/go.mod h1:f1QyiAsvIv4B49DmCqrhlXqyaR+0IxMmyX+1P+AnzOM=
github.com/aws/aws-sdk-go-v2/service/kinesis v1.17.14 h1:oSw0SQN9cKeYvCUYfPul7bH11b8E9I9BnoVUme3iSaU=
github.com/aws/aws-sdk-go-v2/service/kinesis v1.17.14/go.mod h1:omXkSCk1T1difhE8wVaecXNeerY6jmpFFu49ngjEDQk=
github.com/aws/aws-sdk-go-v2/service/kms v1.22.2 h1:jwmtdM1/l1DRNy5jQrrYpsQm8zwetkgeqhAqefDr1yI=
github.com/aws/aws-sdk-go-v2/service/kms v1.22.2/go.mod h1:aNfh11Smy55o65PB3MyKbkM8BFyFUcZmj1k+4g8eNfg=
github.com/aws/aws-sdk-go-v2/service/lambda v1.37.0 h1:xzyM5ZR9kZW0/Bkw5EiihOy6B+BYclp5K+yb6OHjc7s=
github.com/aws/aws-sdk-go-v2/service/lambda v1.37.0/go.mod h1:Q8zQi5nZpjUF/H55dKEpKfEvFWJkgZzjjqvDb2AR5b4=
github.com/aws/aws-sdk-go-v2/service/rds v1.46.0 h1:uv2LAciZRd5lEXzJo2u92tdZh/JxcVL7YLC51D4NLG4=
github.com/aws/aws-sdk-go-v2/service/rds v1.46.0/go.mod h1:goBDR4OPrsnKpYyU0GHGcEnlTmL8O+eKGsWeyOAFJ5M=
github.com/aws/aws-sdk-go-v2/service/redshift v1.28.0 h1:tmhg03t7nNVSFqhxb8YpHqq8H1wwwrfEQv/rL7NkTAE=
github.com/aws/aws-sdk-go-v2/service/redshift v1.28.0/go.mod h1:x9am33DT5lVKUb0DH1UVbX+iFfpIqAKx6DAqB5Qu6jU=
github.com/aws/aws-sdk-go-v2/service/route53 v1.28.3 h1:nJbE4+tHd8xpM1RB1ZF0/xTJnFd/ATz42ZC35lwXx0w=
github.com/aws/aws-sdk-go-v2/service/route53 v1.28.3/go.mod h1:Cd4MnFoV+6fELBrgWXJ4Y09FrSkn/VjNPkOr1Yr1muU=
github.com/aws/aws-sdk-go-v2/service/s3 v1.36.0 h1:lEmQ1XSD9qLk+NZXbgvLJI/IiTz7OIR2TYUTFH25EI4=
github.com/aws/aws-sdk-go-v2/service/s3 v1.36.0/go.mod h1:aVbf0sko/TsLWHx30c/uVu7c62+0EAJ3vbxaJga0xCw=
github.com/aws/aws-sdk-go-v2/service/sfn v1.18.0 h1:1AIwJvCywFO4nGtHj7ZtKb9mhLpB5hToyjtE5OO6o/I=
github.com/aws/aws-sdk-go-v2/service/sfn v1.18.0/go.mod h1:41VgIwo6R/QE8DnFZ4RrP+f2w9xTzB77h3NRu/BzXyE=
github.com/aws/aws-sdk-go-v2/service/sns v1.20.13 h1:+ADGcDhddHTKyu6Qp3oZKootryteS7D3ODo2ZPDBgjQ=
github.com/aws/aws-sdk-go-v2/service/sns v1.20.13/go.mod h1:rWrvp9i8y/lX94lS7Kn/0iu9RY6vXzeKRqS/knVX8/c=
github.com/aws/aws-sdk-go-v2/service/sqs v1.16.0 h1:dzWS4r8E9bA0TesHM40FSAtedwpTVCuTsLI8EziSqyk=
github.com/aws/aws-sdk-go-v2/service/sqs v1.16.0/go.mod h1:IBTQMG8mtyj37OWg7vIXcg714Ntcb/LlYou/rZpvV1k=
github.com/aws/aws-sdk-go-v2/service/sqs v1.23.2 h1:Y2vfLiY3HmaMisuwx6fS2kMRYbajRXXB+9vesGVPseY=
github.com/aws/aws-sdk-go-v2/service/sqs v1.23.2/go.mod h1:TaV67b6JMD1988x/uMDop/JnMFK6v5d4Ru+sDmFg+ww=
github.com/aws/aws-sdk-go-v2/service/ssm v1.24.0 h1:p22U2yL/AeRToERGcZv1R26Yci5VQnWIrpzcZdG54cg=
github.com/aws/aws-sdk-go-v2/service/ssm v1.24.0/go.mod h1:chcyLYBEVRac/7rWJsD6cUHUR2osROwavvNqCplfwog=
github.com/aws/aws-sdk-go-v2/service/sso v1.9.0 h1:1qLJeQGBmNQW3mBNzK2CFmrQNmoXWrscPqsrAaU1aTA=
github.com/aws/aws-sdk-go-v2/service/sso v1.9.0/go.mod h1:vCV4glupK3tR7pw7ks7Y4jYRL86VvxS+g5qk04YeWrU=
github.com/aws/aws-sdk-go-v2/service/sso v1.12.12 h1:nneMBM2p79PGWBQovYO/6Xnc2ryRMw3InnDJq1FHkSY=
github.com/aws/aws-sdk-go-v2/service/sso v1.12.12/go.mod h1:HuCOxYsF21eKrerARYO6HapNeh9GBNq7fius2AcwodY=
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.14.12 h1:2qTR7IFk7/0IN/adSFhYu9Xthr0zVFTgBrmPldILn80=
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.14.12/go.mod h1:E4VrHCPzmVB/KFXtqBGKb3c8zpbNBgKe3fisDNLAW5w=
github.com/aws/aws-sdk-go-v2/service/sts v1.14.0 h1:ksiDXhvNYg0D2/UFkLejsaz3LqpW5yjNQ8Nx9Sn2c0E=
github.com/aws/aws-sdk-go-v2/service/sts v1.14.0/go.mod h1:u0xMJKDvvfocRjiozsoZglVNXRG19043xzp3r2ivLIk=
github.com/aws/aws-sdk-go-v2/service/sts v1.19.2 h1:XFJ2Z6sNUUcAz9poj+245DMkrHE4h2j5I9/xD50RHfE=
github.com/aws/aws-sdk-go-v2/service/sts v1.19.2/go.mod h1:dp0yLPsLBOi++WTxzCjA/oZqi6NPIhoR+uF7GeMU9eg=
github.com/aws/aws-sdk-go-v2/service/wafv2 v1.35.1 h1:FtzLuTf9HPECIcKdBMtA16ZwZWOIj/r57Z3QuWuYfqc=
github.com/aws/aws-sdk-go-v2/service/wafv2 v1.35.1/go.mod h1:RBpb9oTsEgAUfyaTAT2hFC83DxtLxj+SQpcbhaXiHnU=
github.com/aws/smithy-go v1.10.0/go.mod h1:SObp3lf9smib00L/v3U2eAKG8FyQ7iLrJnQiAmR5n+E=
github.com/aws/smithy-go v1.11.2/go.mod h1:3xHYmszWVx2c0kIwQeEVf9uSm4fYZt67FBJnwub1bgM=
github.com/aws/smithy-go v1.13.5 h1:hgz0X/DX0dGqTYpGALqXJoRKRj5oQ7150i5FdTePzO8=
@ -76,11 +162,18 @@ github.com/cloudcmds/tamarin v1.0.0 h1:PhrJ74FCUJo24/nIPXnQe9E3WVEIYo4aG58pICOMD
github.com/cloudcmds/tamarin v1.0.0/go.mod h1:U1aHBoAFtJbI9jzgaj8TUo9C6vfzUKzn1OhWKIdigVM=
github.com/containerd/console v1.0.3 h1:lIr7SlA5PxZyMV30bDW0MGbiOPXwc63yRuCP0ARubLw=
github.com/containerd/console v1.0.3/go.mod h1:7LqA/THxQ86k76b8c/EMSiaJ3h1eZkMkXar0TQ1gf3U=
github.com/coreos/etcd v3.3.10+incompatible/go.mod h1:uF7uidLiAD3TWHmW31ZFd/JWoc32PjwdhPthX9715RE=
github.com/coreos/go-etcd v2.0.0+incompatible/go.mod h1:Jez6KQU2B/sWsbdaef3ED8NzMklzPG4d5KIOhIy30Tk=
github.com/coreos/go-semver v0.2.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk=
github.com/cpuguy83/go-md2man v1.0.10/go.mod h1:SmD6nW6nTyfqj6ABTjUi3V3JVMnlJmwcJI5acqYI6dE=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=
github.com/gofrs/uuid v4.3.1+incompatible h1:0/KbAdpx3UXAx1kEOWHJeOkpbgRFGHVgv+CFIY7dBJI=
github.com/gofrs/uuid v4.3.1+incompatible/go.mod h1:b2aQJv3Z4Fp6yNu3cdSllBxTCLRxnplIgP/c0N/04lM=
github.com/gofrs/uuid v4.4.0+incompatible h1:3qXRTX8/NbyulANqlc0lchS1gqAVxRgsuW1YrTJupqA=
github.com/gofrs/uuid v4.4.0+incompatible/go.mod h1:b2aQJv3Z4Fp6yNu3cdSllBxTCLRxnplIgP/c0N/04lM=
github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk=
github.com/golang/protobuf v1.5.2 h1:ROPKBNFfQgOUMifHyP+KYbvpjbdoFNs+aK7DXlji0Tw=
@ -97,13 +190,19 @@ github.com/hashicorp/errwrap v1.0.0 h1:hLrqtEDnRye3+sgx6z4qVLNuviH3MR5aQ0ykNJa/U
github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4=
github.com/hashicorp/go-multierror v1.1.1 h1:H5DkEtf6CXdFp0N0Em5UCwQpXMWke8IA0+lD48awMYo=
github.com/hashicorp/go-multierror v1.1.1/go.mod h1:iw975J/qwKPdAO1clOe2L8331t/9/fmwbPZ6JB6eMoM=
github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ=
github.com/hexops/gotextdiff v1.0.3 h1:gitA9+qJrrTCsiCl7+kh75nPqQt1cx4ZkudSTLoUqJM=
github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8=
github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsIM=
github.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg=
github.com/jackc/pgservicefile v0.0.0-20200714003250-2b9c44734f2b h1:C8S2+VttkHFdOOCXJe+YGfa4vHYwlt4Zx+IVXQ97jYg=
github.com/jackc/pgservicefile v0.0.0-20200714003250-2b9c44734f2b/go.mod h1:vsD4gTJCa9TptPL8sPkXrLZ+hDuNrZCnj29CQpr4X1E=
github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a h1:bbPeKD0xmW/Y25WS6cokEszi5g+S0QxI/d45PkRi7Nk=
github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a/go.mod h1:5TJZWKEWniPve33vlWYSoGYefn3gLQRzjfDlhSJ9ZKM=
github.com/jackc/pgx/v5 v5.0.4 h1:r5O6y84qHX/z/HZV40JBdx2obsHz7/uRj5b+CcYEdeY=
github.com/jackc/pgx/v5 v5.0.4/go.mod h1:U0ynklHtgg43fue9Ly30w3OCSTDPlXjig9ghrNGaguQ=
github.com/jackc/pgx/v5 v5.4.1 h1:oKfB/FhuVtit1bBM3zNRRsZ925ZkMN3HXL+LgLUM9lE=
github.com/jackc/pgx/v5 v5.4.1/go.mod h1:q6iHT8uDNXWiFNOlRqJzBTaSH3+2xCXkokxHZC5qWFY=
github.com/jmespath/go-jmespath v0.4.0 h1:BEgLn5cpjn8UN1mAw4NjwDrS35OdebyEtFe+9YPoQUg=
github.com/jmespath/go-jmespath v0.4.0/go.mod h1:T8mJZnbsbmF+m6zOOFylbeCJqk5+pHWvzYPziyZiYoo=
github.com/jmespath/go-jmespath/internal/testify v1.5.1 h1:shLQSRRSCCPj3f2gpwzGwWFoC7ycTf1rcQZHOlsJ6N8=
@ -112,6 +211,7 @@ github.com/juju/ansiterm v0.0.0-20210929141451-8b71cc96ebdc h1:ZQrgZFsLzkw7o3CoD
github.com/juju/ansiterm v0.0.0-20210929141451-8b71cc96ebdc/go.mod h1:PyXUpnI3olx3bsPcHt98FGPX/KCFZ1Fi+hw1XLI6384=
github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI=
github.com/kr/pretty v0.3.0 h1:WgNl7dwNpEZ6jJ9k1snq4pZsg7DOEN8hP9Xw0Tsjwk0=
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
@ -129,11 +229,14 @@ github.com/lucasb-eyer/go-colorful v1.2.0 h1:1nnpGOrhyZZuNyfu1QjKiUICQ74+3FNCN69
github.com/lucasb-eyer/go-colorful v1.2.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0=
github.com/lunixbochs/vtclean v1.0.0 h1:xu2sLAri4lGiovBDQKxl5mrXyESr3gUr5m5SM5+LVb8=
github.com/lunixbochs/vtclean v1.0.0/go.mod h1:pHhQNgMf3btfWnGBVipUOjRYhoOsdGqdm/+2c2E2WMI=
github.com/magiconair/properties v1.8.0/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ=
github.com/mattn/go-colorable v0.1.10/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc=
github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU=
github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94=
github.com/mattn/go-isatty v0.0.16 h1:bq3VjFmv/sOjHtdEhmkEV4x1AJtvUvOJ2PFAZ5+peKQ=
github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
github.com/mattn/go-isatty v0.0.17 h1:BTarxUcIeDqL27Mc+vyvdWYSL28zpIhv3RoTdsLMPng=
github.com/mattn/go-isatty v0.0.17/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
github.com/mattn/go-localereader v0.0.1 h1:ygSAOl7ZXTx4RdPYinUpg6W99U8jWvWi9Ye2JC/oIi4=
github.com/mattn/go-localereader v0.0.1/go.mod h1:8fBrzywKY7BI3czFoHkuzRoWE9C+EiG4R1k4Cjx5p88=
github.com/mattn/go-runewidth v0.0.10/go.mod h1:RAqKPSqVFrSLVXbA8x7dzmKdmGzieGRCM46jaSJTDAk=
@ -141,6 +244,8 @@ github.com/mattn/go-runewidth v0.0.12/go.mod h1:RAqKPSqVFrSLVXbA8x7dzmKdmGzieGRC
github.com/mattn/go-runewidth v0.0.13/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w=
github.com/mattn/go-runewidth v0.0.14 h1:+xnbZSEeDbOIg5/mE6JF0w6n9duR1l3/WmbinWVwUuU=
github.com/mattn/go-runewidth v0.0.14/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w=
github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0=
github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y=
github.com/muesli/ansi v0.0.0-20211018074035-2e021307bc4b/go.mod h1:fQuZ0gauxyBcmsdE3ZT4NasjaRdxmbCS0jRHsrWu3Ho=
github.com/muesli/ansi v0.0.0-20211031195517-c9f0611b6c70 h1:kMlmsLSbjkikxQJ1IPwaM+7LJ9ltFu/fi8CRzvSnQmA=
github.com/muesli/ansi v0.0.0-20211031195517-c9f0611b6c70/go.mod h1:fQuZ0gauxyBcmsdE3ZT4NasjaRdxmbCS0jRHsrWu3Ho=
@ -154,27 +259,47 @@ github.com/muesli/termenv v0.11.1-0.20220204035834-5ac8409525e0/go.mod h1:Bd5NYQ
github.com/muesli/termenv v0.11.1-0.20220212125758-44cd13922739/go.mod h1:Bd5NYQ7pd+SrtBSrSNoBBmXlcY8+Xj4BMJgh8qcZrvs=
github.com/muesli/termenv v0.13.0 h1:wK20DRpJdDX8b7Ek2QfhvqhRQFZ237RGRO0RQ/Iqdy0=
github.com/muesli/termenv v0.13.0/go.mod h1:sP1+uffeLaEYpyOTb8pLCUctGcGLnoFjSn4YJK5e2bc=
github.com/pelletier/go-toml v1.2.0/go.mod h1:5z9KED0ma1S8pY6P1sdut58dfprrGBbd/94hg7ilaic=
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/risor-io/risor v0.8.0 h1:G0fpHMGztvocKVu8egkKNbvLy4Rsjkuk+0zReu2JSn8=
github.com/risor-io/risor v0.8.0/go.mod h1:lvatEIYxs6HL+X/Bm0R+Mq4Z9a5Y036mniw6DwUnqs0=
github.com/risor-io/risor v1.1.1 h1:J8rIZX/0HXhg/t2+QygksvP65XCWhg5QxRZrwZabhxE=
github.com/risor-io/risor v1.1.1/go.mod h1:0UMw7ZMbUKSPFgQyuHCFe7UuBUewBKX4K3By4ba1CBA=
github.com/risor-io/risor v1.4.0 h1:G17pWgq+N06jWvnaJVwos89tC5C4VMjqwGYRrTWleRM=
github.com/risor-io/risor v1.4.0/go.mod h1:+s/FeK0CdsTCCNZsHSp8EJa3u3mMrhqtNGLCv/GcW8Y=
github.com/rivo/uniseg v0.1.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
github.com/rivo/uniseg v0.4.2 h1:YwD0ulJSJytLpiaWua0sBDusfsCZohxjxzVTYjwxfV8=
github.com/rivo/uniseg v0.4.2/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88=
github.com/rogpeppe/go-internal v1.9.0 h1:73kH8U+JUqXU8lRuOHeVHaa/SZPifC7BkcraZVejAe8=
github.com/russross/blackfriday v1.5.2/go.mod h1:JO/DiYxRf+HjHt06OyowR9PTA263kcR/rfWxYHBV53g=
github.com/sahilm/fuzzy v0.1.0 h1:FzWGaw2Opqyu+794ZQ9SYifWv2EIXpwP4q8dY1kDAwI=
github.com/sahilm/fuzzy v0.1.0/go.mod h1:VFvziUEIMCrT6A6tw2RFIXPXXmzXbOsSHF0DOI8ZK9Y=
github.com/spf13/afero v1.1.2/go.mod h1:j4pytiNVoe2o6bmDsKpLACNPDBIoEAkihy7loJ1B0CQ=
github.com/spf13/cast v1.3.0/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE=
github.com/spf13/cobra v0.0.5/go.mod h1:3K3wKZymM7VvHMDS9+Akkh4K60UwM26emMESw8tLCHU=
github.com/spf13/jwalterweatherman v1.0.0/go.mod h1:cQK4TGJAtQXfYWX+Ddv3mKDzgVb68N+wFjFa4jdeBTo=
github.com/spf13/pflag v1.0.3/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4=
github.com/spf13/viper v1.3.2/go.mod h1:ZiWeW+zYFKm7srdB9IoDzzZXaJaI5eL9QjNiN/DMA2s=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
github.com/stretchr/objx v0.5.0 h1:1zr/of2m5FGMsad5YfcqgdqdWrIhu+EBEJRhR1U7z/c=
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA=
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
github.com/stretchr/testify v1.8.1 h1:w7B6lhMri9wdJUVmEZPGGhZzrYTPvgJArz7wNPgYKsk=
github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
github.com/stretchr/testify v1.8.3 h1:RP3t2pwF7cMEbC1dqtB6poj3niw/9gnV4Cjg5oW5gtY=
github.com/stretchr/testify v1.8.3/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk=
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
github.com/tidwall/gjson v1.14.3 h1:9jvXn7olKEHU1S9vwoMGliaT8jq1vJ7IH/n9zD9Dnlw=
github.com/tidwall/gjson v1.14.3/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk=
github.com/tidwall/match v1.1.1 h1:+Ho715JplO36QYgwN9PGYNhgZvoUSc9X2c80KVTi+GA=
@ -182,42 +307,58 @@ github.com/tidwall/match v1.1.1/go.mod h1:eRSPERbgtNPcGhD8UCthc6PmLEQXEWd3PRB5JT
github.com/tidwall/pretty v1.2.0/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU=
github.com/tidwall/pretty v1.2.1 h1:qjsOFOWWQl+N3RsoF5/ssm1pHmJJwhjlSbZ51I6wMl4=
github.com/tidwall/pretty v1.2.1/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU=
github.com/ugorji/go/codec v0.0.0-20181204163529-d75b2dcb6bc8/go.mod h1:VFNgLljTbGfSG7qAOspJ7OScBnGdDN/yBr0sguwnwf0=
github.com/vmihailenco/msgpack v4.0.4+incompatible h1:dSLoQfGFAo3F6OoNhwUmLwVgaUXK79GlxNBwueZn0xI=
github.com/vmihailenco/msgpack v4.0.4+incompatible/go.mod h1:fy3FlTQTDXWkZ7Bh6AcGMlsjHatGryHQYUTf1ShIgkk=
github.com/wI2L/jsondiff v0.3.0 h1:iTzQ9u/d86GE9RsBzVHX88f2EA1vQUboHwLhSQFc1s4=
github.com/wI2L/jsondiff v0.3.0/go.mod h1:y1IMzNNjlSsk3IUoJdRJO7VRBtzMvRgyo4Vu0LdHpTc=
github.com/xordataexchange/crypt v0.0.3-0.20170626215501-b2862e3d0a77/go.mod h1:aYKd//L2LvnjZzWKhF00oedf4jCCReLcmhLdhm1A27Q=
github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k=
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
go.etcd.io/bbolt v1.3.6 h1:/ecaJf0sk1l4l6V4awd65v2C3ILy7MSj+s/x1ADCIMU=
go.etcd.io/bbolt v1.3.6/go.mod h1:qXsaaIqmgQH0T+OPdb99Bf+PKfBBQVAdyD6TY9G8XM4=
golang.design/x/clipboard v0.6.2 h1:a3Np4qfKnLWwfFJQhUWU3IDeRfmVuqWl+QPtP4CSYGw=
golang.design/x/clipboard v0.6.2/go.mod h1:kqBSweBP0/im4SZGGjLrppH0D400Hnfo5WbFKSNK8N4=
golang.org/x/crypto v0.0.0-20181203042331-505ab145d0a9/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
golang.org/x/crypto v0.0.0-20220829220503-c86fa9a7ed90 h1:Y/gsMcFOcR+6S6f3YeMKl5g+dZMEWqcz5Czj/GWYbkM=
golang.org/x/crypto v0.0.0-20220829220503-c86fa9a7ed90/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
golang.org/x/crypto v0.9.0 h1:LF6fAI+IutBocDJ2OT0Q1g8plpYljMZ4+lty+dsqw3g=
golang.org/x/crypto v0.9.0/go.mod h1:yrmDGqONDYtNj3tH8X9dzUun2m2lzPa9ngI6/RUPGR0=
golang.org/x/exp v0.0.0-20190731235908-ec7cb31e5a56/go.mod h1:JhuoJpWY28nO4Vef9tZUw9qufEGTyX1+7lmHxV5q5G4=
golang.org/x/exp v0.0.0-20230108222341-4b8118a2686a h1:tlXy25amD5A7gOfbXdqCGN5k8ESEed/Ee1E5RcrYnqU=
golang.org/x/exp v0.0.0-20230108222341-4b8118a2686a/go.mod h1:CxIveKay+FTh1D0yPZemJVgC/95VzuuOLq5Qi4xnoYc=
golang.org/x/exp/shiny v0.0.0-20230213192124-5e25df0256eb h1:gdeQX7xJSkTNF+Sw7++XNIOo4pGL0CjQv3N2Vm1Erxk=
golang.org/x/exp/shiny v0.0.0-20230213192124-5e25df0256eb/go.mod h1:UH99kUObWAZkDnWqppdQe5ZhPYESUw8I0zVV1uWBR+0=
golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js=
golang.org/x/image v0.0.0-20190703141733-d6a02ce849c9/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0=
golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0=
golang.org/x/image v0.0.0-20211028202545-6944b10bf410 h1:hTftEOvwiOq2+O8k2D5/Q7COC7k5Qcrgc2TFURJYnvQ=
golang.org/x/image v0.0.0-20211028202545-6944b10bf410/go.mod h1:023OzeP/+EPmXeapQh35lcL3II3LrY8Ic+EFFKVhULM=
golang.org/x/image v0.5.0 h1:5JMiNunQeQw++mMOz48/ISeNu3Iweh/JaZU8ZLqHRrI=
golang.org/x/image v0.5.0/go.mod h1:FVC7BI/5Ym8R25iw5OLsgshdUBbT1h5jZTpA+mvAdZ4=
golang.org/x/mobile v0.0.0-20190312151609-d3739f865fa6/go.mod h1:z+o9i4GpDbdi3rU15maQ/Ox0txvL9dWGYEHz965HBQE=
golang.org/x/mobile v0.0.0-20210716004757-34ab1303b554 h1:3In5TnfvnuXTF/uflgpYxSCEGP2NdYT37KsPh3VjZYU=
golang.org/x/mobile v0.0.0-20210716004757-34ab1303b554/go.mod h1:jFTmtFYCV0MFtXBU+J5V/+5AUeVS0ON/0WkE/KSrl6E=
golang.org/x/mod v0.1.0/go.mod h1:0QHyrYULN0/3qlju5TqG8bIK38QM8yzMo5ekMj3DlcY=
golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks=
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM=
golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2 h1:CIJ76btIcR3eFI5EgSo6k1qKw9KJexJuRLI9G7Hp5wE=
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
golang.org/x/net v0.10.0 h1:X2//UzNDwYmtCLn7To6G58Wr6f5ahEAQgKNzv9Y951M=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sys v0.0.0-20181205085412-a5c9d58dba9a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
@ -231,13 +372,19 @@ golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBc
golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220204135822-1c1b9b1eba6a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220209214540-3681064d5158/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.1.0 h1:kunALQeHf1/185U1i0GOB/fy1IPRDDpuoOOqRReG57U=
golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.8.0 h1:EBmGv8NaZBZTWvrbjNoL6HVt+IVy3QDQpJs7VRIw3tU=
golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
golang.org/x/term v0.0.0-20220919170432-7a66f970e087 h1:tPwmk4vmvVCMdr98VgL4JH+qZxPL8fqlUOHnyOM8N3w=
golang.org/x/term v0.0.0-20220919170432-7a66f970e087/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
golang.org/x/term v0.8.0 h1:n5xxQn2i3PC0yLAbjTpNT85q/Kgzcr2gIoX9OrJUols=
golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
@ -245,10 +392,14 @@ golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
golang.org/x/text v0.3.8 h1:nAL+RVCQ9uMn3vJZbV+MRnydTJFPf8qqY42YiA6MrqY=
golang.org/x/text v0.3.8/go.mod h1:E6s5w1FMmriuDzIBO73fBruAKo1PCIq6d2Q6DHfQ8WQ=
golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
golang.org/x/text v0.9.0 h1:2sjJmO8cDvYveuX97RDLsxlyUxLl+GHoLxBiRdHllBE=
golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20190312151545-0bb0c0a6e846/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.1.2/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk=
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=

View file

@ -8,6 +8,16 @@ func Values[K comparable, T any](ts map[K]T) []T {
return values
}
func MapValues[K comparable, T, U any](ts map[K]T, fn func(t T) U) map[K]U {
us := make(map[K]U)
for k, t := range ts {
us[k] = fn(t)
}
return us
}
func MapValuesWithError[K comparable, T, U any](ts map[K]T, fn func(t T) (U, error)) (map[K]U, error) {
us := make(map[K]U)

View file

@ -9,6 +9,14 @@ func All[T any](ts []T, predicate func(t T) bool) bool {
return true
}
func Generate[U any](from, to int, fn func(t int) U) []U {
us := make([]U, to-from+1)
for i := from; i <= to; i++ {
us[i-from] = fn(i)
}
return us
}
func Map[T, U any](ts []T, fn func(t T) U) []U {
us := make([]U, len(ts))
for i, t := range ts {
@ -39,3 +47,22 @@ func Filter[T any](ts []T, fn func(t T) bool) []T {
}
return us
}
func FindFirst[T any](ts []T, fn func(t T) bool) (returnedT T, found bool) {
for _, t := range ts {
if fn(t) {
return t, true
}
}
return returnedT, false
}
func FindLast[T any](ts []T, fn func(t T) bool) (returnedT T, found bool) {
for i := len(ts) - 1; i >= 0; i-- {
t := ts[i]
if fn(t) {
return t, true
}
}
return returnedT, false
}

View file

@ -11,16 +11,17 @@ import (
"path/filepath"
"strings"
"github.com/lmika/audax/internal/common/ui/events"
"github.com/lmika/dynamo-browse/internal/common/ui/events"
"github.com/lmika/shellwords"
)
const commandsCategory = "commands"
type CommandController struct {
historyProvider IterProvider
commandList *CommandList
lookupExtensions []CommandLookupExtension
historyProvider IterProvider
commandList *CommandList
lookupExtensions []CommandLookupExtension
completionProvider CommandCompletionProvider
}
func NewCommandController(historyProvider IterProvider) *CommandController {
@ -40,6 +41,10 @@ func (c *CommandController) AddCommandLookupExtension(ext CommandLookupExtension
c.lookupExtensions = append(c.lookupExtensions, ext)
}
func (c *CommandController) SetCommandCompletionProvider(provider CommandCompletionProvider) {
c.completionProvider = provider
}
func (c *CommandController) Prompt() tea.Msg {
return events.PromptForInputMsg{
Prompt: ":",
@ -47,6 +52,24 @@ func (c *CommandController) Prompt() tea.Msg {
OnDone: func(value string) tea.Msg {
return c.Execute(value)
},
// TEMP
OnTabComplete: func(value string) (string, bool) {
if c.completionProvider == nil {
return "", false
}
if strings.HasPrefix(value, "sa ") || strings.HasPrefix(value, "da ") {
tokens := shellwords.Split(strings.TrimSpace(value))
lastToken := tokens[len(tokens)-1]
options := c.completionProvider.AttributesWithPrefix(lastToken)
if len(options) == 1 {
return value[:len(value)-len(lastToken)] + options[0], true
}
}
return "", false
},
// END TEMP
}
}

View file

@ -2,11 +2,11 @@ package commandctrl_test
import (
"context"
"github.com/lmika/audax/internal/common/ui/events"
"github.com/lmika/audax/internal/dynamo-browse/services"
"github.com/lmika/dynamo-browse/internal/common/ui/events"
"github.com/lmika/dynamo-browse/internal/dynamo-browse/services"
"testing"
"github.com/lmika/audax/internal/common/ui/commandctrl"
"github.com/lmika/dynamo-browse/internal/common/ui/commandctrl"
"github.com/stretchr/testify/assert"
)

View file

@ -2,7 +2,7 @@ package commandctrl
import (
"context"
"github.com/lmika/audax/internal/dynamo-browse/services"
"github.com/lmika/dynamo-browse/internal/dynamo-browse/services"
)
type IterProvider interface {

View file

@ -19,3 +19,7 @@ type CommandList struct {
type CommandLookupExtension interface {
LookupCommand(name string) Command
}
type CommandCompletionProvider interface {
AttributesWithPrefix(prefix string) []string
}

View file

@ -2,7 +2,7 @@ package dispatcher
import (
tea "github.com/charmbracelet/bubbletea"
"github.com/lmika/audax/internal/common/ui/uimodels"
"github.com/lmika/dynamo-browse/internal/common/ui/uimodels"
)
type DispatcherContext struct {

View file

@ -4,8 +4,8 @@ import (
"context"
"sync"
"github.com/lmika/audax/internal/common/ui/events"
"github.com/lmika/audax/internal/common/ui/uimodels"
"github.com/lmika/dynamo-browse/internal/common/ui/events"
"github.com/lmika/dynamo-browse/internal/common/ui/uimodels"
"github.com/pkg/errors"
)

View file

@ -2,7 +2,7 @@ package events
import (
tea "github.com/charmbracelet/bubbletea"
"github.com/lmika/audax/internal/dynamo-browse/services"
"github.com/lmika/dynamo-browse/internal/dynamo-browse/services"
"log"
)
@ -54,3 +54,8 @@ type MessageWithMode interface {
MessageWithStatus
ModeMessage() string
}
type MessageWithRightMode interface {
MessageWithStatus
RightModeMessage() string
}

View file

@ -2,7 +2,7 @@ package events
import (
tea "github.com/charmbracelet/bubbletea"
"github.com/lmika/audax/internal/dynamo-browse/services"
"github.com/lmika/dynamo-browse/internal/dynamo-browse/services"
)
// Error indicates that an error occurred
@ -21,8 +21,9 @@ type ModeMessage string
// PromptForInput indicates that the context is requesting a line of input
type PromptForInputMsg struct {
Prompt string
History services.HistoryProvider
OnDone func(value string) tea.Msg
OnCancel func() tea.Msg
Prompt string
History services.HistoryProvider
OnDone func(value string) tea.Msg
OnCancel func() tea.Msg
OnTabComplete func(value string) (string, bool)
}

View file

@ -2,21 +2,25 @@ package controllers
import (
tea "github.com/charmbracelet/bubbletea"
"github.com/lmika/audax/internal/common/ui/events"
"github.com/lmika/audax/internal/dynamo-browse/models"
"github.com/lmika/audax/internal/dynamo-browse/models/columns"
"github.com/lmika/audax/internal/dynamo-browse/models/queryexpr"
"github.com/lmika/dynamo-browse/internal/common/ui/events"
"github.com/lmika/dynamo-browse/internal/dynamo-browse/models"
"github.com/lmika/dynamo-browse/internal/dynamo-browse/models/columns"
"github.com/lmika/dynamo-browse/internal/dynamo-browse/models/evaluators"
"github.com/lmika/dynamo-browse/internal/dynamo-browse/models/queryexpr"
bus "github.com/lmika/events"
"strings"
)
type ColumnsController struct {
tr *TableReadController
// State
colModel *columns.Columns
resultSet *models.ResultSet
}
func NewColumnsController(eventBus *bus.Bus) *ColumnsController {
cc := &ColumnsController{}
func NewColumnsController(tr *TableReadController, eventBus *bus.Bus) *ColumnsController {
cc := &ColumnsController{tr: tr}
eventBus.On(newResultSetEvent, cc.onNewResultSet)
return cc
@ -79,7 +83,7 @@ func (cc *ColumnsController) AddColumn(afterIndex int) tea.Msg {
newCol := columns.Column{
Name: colExpr.String(),
Evaluator: columns.ExprFieldValueEvaluator{Expr: colExpr},
Evaluator: queryexpr.ExprFieldValueEvaluator{Expr: colExpr},
}
if afterIndex >= len(cc.colModel.Columns)-1 {
@ -115,3 +119,44 @@ func (cc *ColumnsController) DeleteColumn(afterIndex int) tea.Msg {
return ColumnsUpdated{}
}
func (cc *ColumnsController) SortByColumn(index int) tea.Msg {
if index >= len(cc.colModel.Columns) {
return nil
}
column := cc.colModel.Columns[index]
newCriteria := models.SortCriteria{
Fields: []models.SortField{
{Field: column.Evaluator, Asc: true},
},
}
if ff := cc.SortCriteria().FirstField(); evaluators.Equals(ff.Field, column.Evaluator) {
newCriteria.Fields[0].Asc = !ff.Asc
}
cc.SetSortCriteria(newCriteria)
return ColumnsUpdated{}
}
func (c *ColumnsController) AttributesWithPrefix(prefix string) []string {
options := make([]string, 0)
for _, col := range c.resultSet.Columns() {
if strings.HasPrefix(col, prefix) {
options = append(options, col)
}
}
return options
}
func (cc *ColumnsController) SortCriteria() models.SortCriteria {
if cc.resultSet == nil {
return models.SortCriteria{}
}
return cc.resultSet.SortCriteria()
}
func (cc *ColumnsController) SetSortCriteria(criteria models.SortCriteria) {
cc.tr.SortResultSet(criteria)
}

View file

@ -2,7 +2,7 @@ package controllers
import (
tea "github.com/charmbracelet/bubbletea"
"github.com/lmika/audax/internal/common/ui/events"
"github.com/lmika/dynamo-browse/internal/common/ui/events"
)
type promptSequence struct {

View file

@ -2,8 +2,12 @@ package controllers
import (
"fmt"
"strings"
"time"
tea "github.com/charmbracelet/bubbletea"
"github.com/lmika/audax/internal/dynamo-browse/models"
"github.com/lmika/dynamo-browse/internal/dynamo-browse/models"
"github.com/lmika/dynamo-browse/internal/dynamo-browse/models/relitems"
)
type SetTableItemView struct {
@ -42,6 +46,24 @@ func (rs NewResultSet) ModeMessage() string {
return modeLine
}
func (rs NewResultSet) RightModeMessage() string {
var sb strings.Builder
itemCountStr := applyToN("", len(rs.ResultSet.Items()), "item", "items", "")
if rs.currentFilter != "" {
sb.WriteString(fmt.Sprintf("%d of %v", rs.filteredCount, itemCountStr))
} else {
sb.WriteString(itemCountStr)
}
if !rs.ResultSet.Created.IsZero() {
sb.WriteString(" • ")
sb.WriteString(rs.ResultSet.Created.Format(time.Kitchen))
}
return sb.String()
}
func (rs NewResultSet) StatusMessage() string {
if rs.statusMessage != "" {
return rs.statusMessage
@ -69,3 +91,9 @@ func (rs ResultSetUpdated) StatusMessage() string {
type ShowColumnOverlay struct{}
type HideColumnOverlay struct{}
type ShowRelatedItemsOverlay struct {
Items []relitems.RelatedItem
OnSelected func(item relitems.RelatedItem) tea.Msg
}
type HideRelatedItemsOverlay struct{}

View file

@ -1,57 +1,146 @@
package controllers
import (
"bytes"
"context"
"encoding/csv"
tea "github.com/charmbracelet/bubbletea"
"github.com/lmika/audax/internal/common/ui/events"
"github.com/lmika/audax/internal/dynamo-browse/models/attrutils"
"github.com/pkg/errors"
"fmt"
"io"
"os"
tea "github.com/charmbracelet/bubbletea"
"github.com/lmika/dynamo-browse/internal/common/ui/events"
"github.com/lmika/dynamo-browse/internal/dynamo-browse/models"
"github.com/lmika/dynamo-browse/internal/dynamo-browse/models/attrutils"
"github.com/lmika/dynamo-browse/internal/dynamo-browse/models/columns"
"github.com/lmika/dynamo-browse/internal/dynamo-browse/services"
"github.com/lmika/dynamo-browse/internal/dynamo-browse/services/jobs"
"github.com/pkg/errors"
)
type ExportController struct {
state *State
columns *ColumnsController
state *State
tableService TableReadService
jobController *JobsController
columns *ColumnsController
pasteboardProvider services.PasteboardProvider
}
func NewExportController(state *State, columns *ColumnsController) *ExportController {
return &ExportController{state, columns}
func NewExportController(
state *State,
tableService TableReadService,
jobsController *JobsController,
columns *ColumnsController,
pasteboardProvider services.PasteboardProvider,
) *ExportController {
return &ExportController{state, tableService, jobsController, columns, pasteboardProvider}
}
func (c *ExportController) ExportCSV(filename string) tea.Msg {
func (c *ExportController) ExportCSV(filename string, opts ExportOptions) tea.Msg {
resultSet := c.state.ResultSet()
if resultSet == nil {
return events.Error(errors.New("no result set"))
}
f, err := os.Create(filename)
if err != nil {
return events.Error(errors.Wrapf(err, "cannot export to '%v'", filename))
}
defer f.Close()
return NewJob(c.jobController, fmt.Sprintf("Exporting to %v…", filename), func(ctx context.Context) (int, error) {
f, err := os.Create(filename)
if err != nil {
return 0, errors.Wrapf(err, "cannot export to '%v'", filename)
}
defer f.Close()
cw := csv.NewWriter(f)
cw := csv.NewWriter(f)
defer cw.Flush()
columns := c.columns.Columns().VisibleColumns()
colNames := make([]string, len(columns))
for i, c := range columns {
colNames[i] = c.Name
}
if err := cw.Write(colNames); err != nil {
return 0, errors.Wrapf(err, "cannot export to '%v'", filename)
}
totalRows := 0
row := make([]string, len(columns))
for {
for _, item := range resultSet.Items() {
for i, col := range columns {
row[i], _ = attrutils.AttributeToString(col.Evaluator.EvaluateForItem(item))
}
if err := cw.Write(row); err != nil {
return 0, errors.Wrapf(err, "cannot export to '%v'", filename)
}
}
totalRows += len(resultSet.Items())
if !opts.AllResults || !resultSet.HasNextPage() {
break
}
jobs.PostUpdate(ctx, fmt.Sprintf("exported %d items", totalRows))
resultSet, err = c.tableService.NextPage(ctx, resultSet)
if err != nil {
return 0, errors.Wrapf(err, "cannot get next page while exporting to '%v'", filename)
}
}
return totalRows, nil
}).OnDone(func(rows int) tea.Msg {
return events.StatusMsg(applyToN("Exported ", rows, "item", "items", " to "+filename))
}).Submit()
}
func (c *ExportController) ExportCSVToClipboard() tea.Msg {
var bts bytes.Buffer
resultSet := c.state.ResultSet()
if resultSet == nil {
return errors.New("no result set")
}
if err := c.exportCSV(&bts, c.columns.Columns().VisibleColumns(), resultSet); err != nil {
return events.Error(err)
}
if err := c.pasteboardProvider.WriteText(bts.Bytes()); err != nil {
return events.Error(err)
}
return nil
}
// TODO: this really needs to be a service!
func (c *ExportController) ExportToWriter(w io.Writer, resultSet *models.ResultSet) error {
return c.exportCSV(w, columns.NewColumnsFromResultSet(resultSet).Columns, resultSet)
}
func (c *ExportController) exportCSV(w io.Writer, cols []columns.Column, resultSet *models.ResultSet) error {
cw := csv.NewWriter(w)
defer cw.Flush()
columns := c.columns.Columns().VisibleColumns()
colNames := make([]string, len(columns))
for i, c := range columns {
colNames := make([]string, len(cols))
for i, c := range cols {
colNames[i] = c.Name
}
if err := cw.Write(colNames); err != nil {
return events.Error(errors.Wrapf(err, "cannot export to '%v'", filename))
return errors.Wrap(err, "cannot export to clipboard")
}
row := make([]string, len(columns))
row := make([]string, len(cols))
for _, item := range resultSet.Items() {
for i, col := range columns {
for i, col := range cols {
row[i], _ = attrutils.AttributeToString(col.Evaluator.EvaluateForItem(item))
}
if err := cw.Write(row); err != nil {
return events.Error(errors.Wrapf(err, "cannot export to '%v'", filename))
return errors.Wrap(err, "cannot export to clipboard")
}
}
return nil
}
type ExportOptions struct {
// AllResults returns all results from the table
AllResults bool
}

View file

@ -1,6 +1,9 @@
package controllers_test
import (
"fmt"
"github.com/lmika/dynamo-browse/internal/common/sliceutils"
"github.com/lmika/dynamo-browse/internal/dynamo-browse/controllers"
"github.com/stretchr/testify/assert"
"os"
"strings"
@ -14,7 +17,7 @@ func TestExportController_ExportCSV(t *testing.T) {
tempFile := tempFile(t)
invokeCommand(t, srv.readController.Init())
invokeCommand(t, srv.exportController.ExportCSV(tempFile))
invokeCommand(t, srv.exportController.ExportCSV(tempFile, controllers.ExportOptions{}))
bts, err := os.ReadFile(tempFile)
assert.NoError(t, err)
@ -27,13 +30,66 @@ func TestExportController_ExportCSV(t *testing.T) {
}, ""))
})
t.Run("should export all pages of the results", func(t *testing.T) {
pageLimits := []int{5, 10, 50}
for _, pageLimit := range pageLimits {
t.Run(fmt.Sprintf("page size %d", pageLimit), func(t *testing.T) {
t.Run("all results", func(t *testing.T) {
srv := newService(t, serviceConfig{tableName: "count-to-30", defaultLimit: 5})
tempFile := tempFile(t)
expected := append([]string{
"pk,sk,num\n",
}, sliceutils.Generate(1, 30, func(i int) string {
return fmt.Sprintf("NUM,NUM#%02d,%d\n", i, i)
})...)
invokeCommand(t, srv.readController.Init())
invokeCommand(t, srv.exportController.ExportCSV(tempFile, controllers.ExportOptions{
AllResults: true,
}))
bts, err := os.ReadFile(tempFile)
assert.NoError(t, err)
assert.Equal(t, strings.Join(expected, ""), string(bts))
})
t.Run("with query", func(t *testing.T) {
srv := newService(t, serviceConfig{tableName: "count-to-30", defaultLimit: 5})
tempFile := tempFile(t)
expected := append([]string{
"pk,sk,num\n",
}, sliceutils.Generate(1, 15, func(i int) string {
return fmt.Sprintf("NUM,NUM#%02d,%d\n", i, i)
})...)
invokeCommand(t, srv.readController.Init())
invokeCommandWithPrompt(t, srv.readController.PromptForQuery(), "num<=15")
invokeCommand(t, srv.exportController.ExportCSV(tempFile, controllers.ExportOptions{
AllResults: true,
}))
bts, err := os.ReadFile(tempFile)
assert.NoError(t, err)
assert.Equal(t, strings.Join(expected, ""), string(bts))
})
})
}
})
t.Run("should return error if result set is not set", func(t *testing.T) {
srv := newService(t, serviceConfig{tableName: "non-existant-table"})
tempFile := tempFile(t)
invokeCommandExpectingError(t, srv.readController.Init())
invokeCommandExpectingError(t, srv.exportController.ExportCSV(tempFile))
invokeCommandExpectingError(t, srv.exportController.ExportCSV(tempFile, controllers.ExportOptions{}))
})
t.Run("should honour new columns in CSV file", func(t *testing.T) {
@ -48,7 +104,7 @@ func TestExportController_ExportCSV(t *testing.T) {
invokeCommandWithPrompt(t, srv.columnsController.AddColumn(1), "address.street")
invokeCommand(t, srv.columnsController.ShiftColumnLeft(1))
invokeCommand(t, srv.exportController.ExportCSV(tempFile))
invokeCommand(t, srv.exportController.ExportCSV(tempFile, controllers.ExportOptions{}))
bts, err := os.ReadFile(tempFile)
assert.NoError(t, err)
@ -71,7 +127,7 @@ func TestExportController_ExportCSV(t *testing.T) {
invokeCommand(t, srv.columnsController.ToggleVisible(1))
invokeCommand(t, srv.columnsController.ToggleVisible(2))
invokeCommand(t, srv.exportController.ExportCSV(tempFile))
invokeCommand(t, srv.exportController.ExportCSV(tempFile, controllers.ExportOptions{}))
bts, err := os.ReadFile(tempFile)
assert.NoError(t, err)

View file

@ -2,10 +2,12 @@ package controllers
import (
"context"
"io/fs"
"github.com/aws/aws-sdk-go-v2/service/dynamodb/types"
tea "github.com/charmbracelet/bubbletea"
"github.com/lmika/audax/internal/dynamo-browse/models"
"io/fs"
"github.com/lmika/dynamo-browse/internal/dynamo-browse/models"
"github.com/lmika/dynamo-browse/internal/dynamo-browse/models/relitems"
)
type TableReadService interface {
@ -33,3 +35,7 @@ type CustomKeyBindingSource interface {
UnbindKey(key string)
Rebind(bindingName string, newKey string) error
}
type RelatedItemSupplier interface {
RelatedItemOfItem(context.Context, *models.ResultSet, int) ([]relitems.RelatedItem, error)
}

View file

@ -3,8 +3,8 @@ package controllers
import (
"context"
tea "github.com/charmbracelet/bubbletea"
"github.com/lmika/audax/internal/common/ui/events"
"github.com/lmika/audax/internal/dynamo-browse/services/jobs"
"github.com/lmika/dynamo-browse/internal/common/ui/events"
"github.com/lmika/dynamo-browse/internal/dynamo-browse/services/jobs"
)
func NewJob[T any](jc *JobsController, description string, job func(ctx context.Context) (T, error)) JobBuilder[T] {
@ -51,6 +51,9 @@ func (jb JobBuilder[T]) executeJob(ctx context.Context) tea.Msg {
if jb.onEither != nil {
return jb.onEither(res, err)
} else if err == nil {
if jb.onDone == nil {
return nil
}
return jb.onDone(res)
} else {
if jb.onErr != nil {

View file

@ -2,8 +2,8 @@ package controllers
import (
tea "github.com/charmbracelet/bubbletea"
"github.com/lmika/audax/internal/common/ui/events"
"github.com/lmika/audax/internal/dynamo-browse/services/jobs"
"github.com/lmika/dynamo-browse/internal/common/ui/events"
"github.com/lmika/dynamo-browse/internal/dynamo-browse/services/jobs"
bus "github.com/lmika/events"
"log"
)

View file

@ -3,8 +3,8 @@ package controllers
import (
"fmt"
tea "github.com/charmbracelet/bubbletea"
"github.com/lmika/audax/internal/common/ui/events"
"github.com/lmika/audax/internal/dynamo-browse/services/keybindings"
"github.com/lmika/dynamo-browse/internal/common/ui/events"
"github.com/lmika/dynamo-browse/internal/dynamo-browse/services/keybindings"
"github.com/pkg/errors"
)

View file

@ -3,21 +3,24 @@ package controllers
import (
"context"
"fmt"
tea "github.com/charmbracelet/bubbletea"
"github.com/lmika/audax/internal/common/ui/commandctrl"
"github.com/lmika/audax/internal/common/ui/events"
"github.com/lmika/audax/internal/dynamo-browse/models"
"github.com/lmika/audax/internal/dynamo-browse/models/queryexpr"
"github.com/lmika/audax/internal/dynamo-browse/services/scriptmanager"
bus "github.com/lmika/events"
"github.com/pkg/errors"
"log"
"strings"
tea "github.com/charmbracelet/bubbletea"
"github.com/lmika/dynamo-browse/internal/common/ui/commandctrl"
"github.com/lmika/dynamo-browse/internal/common/ui/events"
"github.com/lmika/dynamo-browse/internal/dynamo-browse/models"
"github.com/lmika/dynamo-browse/internal/dynamo-browse/models/queryexpr"
"github.com/lmika/dynamo-browse/internal/dynamo-browse/models/relitems"
"github.com/lmika/dynamo-browse/internal/dynamo-browse/services/scriptmanager"
bus "github.com/lmika/events"
"github.com/pkg/errors"
)
type ScriptController struct {
scriptManager *scriptmanager.Service
tableReadController *TableReadController
jobController *JobsController
settingsController *SettingsController
eventBus *bus.Bus
sendMsg func(msg tea.Msg)
@ -26,12 +29,14 @@ type ScriptController struct {
func NewScriptController(
scriptManager *scriptmanager.Service,
tableReadController *TableReadController,
jobController *JobsController,
settingsController *SettingsController,
eventBus *bus.Bus,
) *ScriptController {
sc := &ScriptController{
scriptManager: scriptManager,
tableReadController: tableReadController,
jobController: jobController,
settingsController: settingsController,
eventBus: eventBus,
}
@ -61,13 +66,6 @@ func (sc *ScriptController) Init() {
} else {
log.Printf("warn: script lookup paths are invalid: %v", err)
}
sc.scriptManager.SetDefaultOptions(scriptmanager.Options{
OSExecShell: "/bin/bash",
Permissions: scriptmanager.Permissions{
AllowShellCommands: true,
AllowEnv: true,
},
})
}
func (sc *ScriptController) SetMessageSender(sendMsg func(msg tea.Msg)) {
@ -169,7 +167,6 @@ func (s *sessionImpl) SetResultSet(ctx context.Context, newResultSet *models.Res
}
func (s *sessionImpl) Query(ctx context.Context, query string, opts scriptmanager.QueryOptions) (*models.ResultSet, error) {
// Parse the query
expr, err := queryexpr.Parse(query)
if err != nil {
@ -182,12 +179,22 @@ func (s *sessionImpl) Query(ctx context.Context, query string, opts scriptmanage
if opts.ValuePlaceholders != nil {
expr = expr.WithValueParams(opts.ValuePlaceholders)
}
if opts.IndexName != "" {
expr = expr.WithIndex(opts.IndexName)
}
return s.sc.doQuery(ctx, expr, opts)
}
func (s *ScriptController) doQuery(ctx context.Context, expr *queryexpr.QueryExpr, opts scriptmanager.QueryOptions) (*models.ResultSet, error) {
// Get the table info
var tableInfo *models.TableInfo
var (
tableInfo *models.TableInfo
err error
)
tableName := opts.TableName
currentResultSet := s.sc.tableReadController.state.ResultSet()
currentResultSet := s.tableReadController.state.ResultSet()
if tableName != "" {
// Table specified. If it's the same as the current table, then use the existing table info
@ -196,7 +203,7 @@ func (s *sessionImpl) Query(ctx context.Context, query string, opts scriptmanage
}
// Otherwise, describe the table
tableInfo, err = s.sc.tableReadController.tableService.Describe(ctx, tableName)
tableInfo, err = s.tableReadController.tableService.Describe(ctx, tableName)
if err != nil {
return nil, errors.Wrapf(err, "cannot describe table '%v'", tableName)
}
@ -208,7 +215,7 @@ func (s *sessionImpl) Query(ctx context.Context, query string, opts scriptmanage
tableInfo = currentResultSet.TableInfo
}
newResultSet, err := s.sc.tableReadController.tableService.ScanOrQuery(ctx, tableInfo, expr, nil)
newResultSet, err := s.tableReadController.tableService.ScanOrQuery(ctx, tableInfo, expr, nil)
if err != nil {
return nil, err
}
@ -244,3 +251,31 @@ func (sc *ScriptController) LookupBinding(theKey string) string {
func (sc *ScriptController) UnbindKey(key string) {
sc.scriptManager.UnbindKey(key)
}
func (c *ScriptController) LookupRelatedItems(idx int) (res tea.Msg) {
rs := c.tableReadController.state.ResultSet()
relItems, err := c.scriptManager.RelatedItemOfItem(context.Background(), rs, idx)
if err != nil {
return events.Error(err)
} else if len(relItems) == 0 {
return events.StatusMsg("No related items available")
}
return ShowRelatedItemsOverlay{
Items: relItems,
OnSelected: func(item relitems.RelatedItem) tea.Msg {
if item.OnSelect != nil {
return item.OnSelect()
}
return NewJob(c.jobController, "Running query…", func(ctx context.Context) (*models.ResultSet, error) {
return c.doQuery(ctx, item.Query, scriptmanager.QueryOptions{
TableName: item.Table,
})
}).OnDone(func(rs *models.ResultSet) tea.Msg {
return c.tableReadController.setResultSetAndFilter(rs, "", true, resultSetUpdateQuery)
}).Submit()
},
}
}

View file

@ -1,12 +1,13 @@
package controllers_test
import (
"github.com/aws/aws-sdk-go-v2/service/dynamodb/types"
"github.com/lmika/audax/internal/common/ui/events"
"github.com/lmika/audax/internal/dynamo-browse/controllers"
"github.com/stretchr/testify/assert"
"testing"
"time"
"github.com/aws/aws-sdk-go-v2/service/dynamodb/types"
"github.com/lmika/dynamo-browse/internal/common/ui/events"
"github.com/lmika/dynamo-browse/internal/dynamo-browse/controllers"
"github.com/stretchr/testify/assert"
)
func TestScriptController_RunScript(t *testing.T) {
@ -53,7 +54,7 @@ func TestScriptController_RunScript(t *testing.T) {
srv := newService(t, serviceConfig{
tableName: "alpha-table",
scriptFS: testScriptFile(t, "test.tm", `
rs := session.query('pk="abc"').unwrap()
rs := session.query('pk="abc"')
ui.print(rs.length)
`),
})
@ -72,7 +73,7 @@ func TestScriptController_RunScript(t *testing.T) {
srv := newService(t, serviceConfig{
tableName: "alpha-table",
scriptFS: testScriptFile(t, "test.tm", `
rs := session.query('pk!="abc"', { table: "count-to-30" }).unwrap()
rs := session.query('pk!="abc"', { table: "count-to-30" })
ui.print(rs.length)
`),
})
@ -93,7 +94,7 @@ func TestScriptController_RunScript(t *testing.T) {
srv := newService(t, serviceConfig{
tableName: "alpha-table",
scriptFS: testScriptFile(t, "test.tm", `
rs := session.query('pk="abc"').unwrap()
rs := session.query('pk="abc"')
session.set_result_set(rs)
`),
})
@ -112,7 +113,7 @@ func TestScriptController_RunScript(t *testing.T) {
srv := newService(t, serviceConfig{
tableName: "alpha-table",
scriptFS: testScriptFile(t, "test.tm", `
rs := session.query('pk="abc"').unwrap()
rs := session.query('pk="abc"')
rs[0].set_attr("pk", "131")
session.set_result_set(rs)
`),
@ -135,22 +136,35 @@ func TestScriptController_RunScript(t *testing.T) {
func TestScriptController_LookupCommand(t *testing.T) {
t.Run("should schedule the script on a separate go-routine", func(t *testing.T) {
srv := newService(t, serviceConfig{
tableName: "alpha-table",
scriptFS: testScriptFile(t, "test.tm", `
ext.command("mycommand", func(name) {
ui.print("Hello, ", name)
scenarios := []struct {
descr string
command string
expectedOutput string
}{
{descr: "command with arg", command: "mycommand \"test name\"", expectedOutput: "Hello, test name"},
{descr: "command no arg", command: "mycommand", expectedOutput: "Hello, nil value"},
}
for _, scenario := range scenarios {
t.Run(scenario.descr, func(t *testing.T) {
srv := newService(t, serviceConfig{
tableName: "alpha-table",
scriptFS: testScriptFile(t, "test.tm", `
ext.command("mycommand", func(name = "nil value") {
ui.print(sprintf("Hello, %v", name))
})
`),
})
`),
})
invokeCommand(t, srv.scriptController.LoadScript("test.tm"))
invokeCommand(t, srv.commandController.Execute(`mycommand "test name"`))
invokeCommand(t, srv.scriptController.LoadScript("test.tm"))
invokeCommand(t, srv.commandController.Execute(scenario.command))
srv.msgSender.waitForAtLeastOneMessages(t, 5*time.Second)
srv.msgSender.waitForAtLeastOneMessages(t, 5*time.Second)
assert.Len(t, srv.msgSender.msgs, 1)
assert.Equal(t, events.StatusMsg("Hello, test name"), srv.msgSender.msgs[0])
assert.Len(t, srv.msgSender.msgs, 1)
assert.Equal(t, events.StatusMsg(scenario.expectedOutput), srv.msgSender.msgs[0])
})
}
})
t.Run("should only allow one script to run at a time", func(t *testing.T) {

View file

@ -3,7 +3,7 @@ package controllers
import (
"fmt"
tea "github.com/charmbracelet/bubbletea"
"github.com/lmika/audax/internal/common/ui/events"
"github.com/lmika/dynamo-browse/internal/common/ui/events"
bus "github.com/lmika/events"
"github.com/pkg/errors"
"log"

View file

@ -1,8 +1,8 @@
package controllers_test
import (
"github.com/lmika/audax/internal/common/ui/events"
"github.com/lmika/audax/internal/dynamo-browse/controllers"
"github.com/lmika/dynamo-browse/internal/common/ui/events"
"github.com/lmika/dynamo-browse/internal/dynamo-browse/controllers"
"github.com/stretchr/testify/assert"
"testing"
)

View file

@ -1,9 +1,8 @@
package controllers
import (
"github.com/lmika/dynamo-browse/internal/dynamo-browse/models"
"sync"
"github.com/lmika/audax/internal/dynamo-browse/models"
)
type State struct {

View file

@ -4,22 +4,24 @@ import (
"bytes"
"context"
"fmt"
"github.com/aws/aws-sdk-go-v2/service/dynamodb/types"
tea "github.com/charmbracelet/bubbletea"
"github.com/lmika/audax/internal/common/ui/events"
"github.com/lmika/audax/internal/dynamo-browse/models"
"github.com/lmika/audax/internal/dynamo-browse/models/attrcodec"
"github.com/lmika/audax/internal/dynamo-browse/models/queryexpr"
"github.com/lmika/audax/internal/dynamo-browse/models/serialisable"
"github.com/lmika/audax/internal/dynamo-browse/services/inputhistory"
"github.com/lmika/audax/internal/dynamo-browse/services/itemrenderer"
"github.com/lmika/audax/internal/dynamo-browse/services/viewsnapshot"
bus "github.com/lmika/events"
"github.com/pkg/errors"
"golang.design/x/clipboard"
"log"
"strings"
"sync"
"github.com/aws/aws-sdk-go-v2/service/dynamodb/types"
tea "github.com/charmbracelet/bubbletea"
"github.com/lmika/dynamo-browse/internal/common/ui/events"
"github.com/lmika/dynamo-browse/internal/dynamo-browse/models"
"github.com/lmika/dynamo-browse/internal/dynamo-browse/models/attrcodec"
"github.com/lmika/dynamo-browse/internal/dynamo-browse/models/attrutils"
"github.com/lmika/dynamo-browse/internal/dynamo-browse/models/queryexpr"
"github.com/lmika/dynamo-browse/internal/dynamo-browse/models/serialisable"
"github.com/lmika/dynamo-browse/internal/dynamo-browse/services"
"github.com/lmika/dynamo-browse/internal/dynamo-browse/services/inputhistory"
"github.com/lmika/dynamo-browse/internal/dynamo-browse/services/itemrenderer"
"github.com/lmika/dynamo-browse/internal/dynamo-browse/services/viewsnapshot"
bus "github.com/lmika/events"
"github.com/pkg/errors"
)
type resultSetUpdateOp int
@ -33,6 +35,7 @@ const (
resultSetUpdateTouch
resultSetUpdateNextPage
resultSetUpdateScript
resultSetUpdateResort
)
type MarkOp int
@ -57,11 +60,12 @@ type TableReadController struct {
eventBus *bus.Bus
tableName string
loadFromLastView bool
pasteboardProvider services.PasteboardProvider
relatedItemSupplier RelatedItemSupplier
// state
mutex *sync.Mutex
state *State
clipboardInit bool
mutex *sync.Mutex
state *State
}
func NewTableReadController(
@ -72,6 +76,8 @@ func NewTableReadController(
jobController *JobsController,
inputHistoryService *inputhistory.Service,
eventBus *bus.Bus,
pasteboardProvider services.PasteboardProvider,
relatedItemSupplier RelatedItemSupplier,
tableName string,
) *TableReadController {
return &TableReadController{
@ -83,6 +89,8 @@ func NewTableReadController(
inputHistoryService: inputHistoryService,
eventBus: eventBus,
tableName: tableName,
pasteboardProvider: pasteboardProvider,
relatedItemSupplier: relatedItemSupplier,
mutex: new(sync.Mutex),
}
}
@ -143,6 +151,13 @@ func (c *TableReadController) ScanTable(name string) tea.Msg {
}).OnEither(c.handleResultSetFromJobResult(c.state.Filter(), true, false, resultSetUpdateInit)).Submit()
}
func (c *TableReadController) SortResultSet(newCriteria models.SortCriteria) {
c.state.withResultSet(func(rs *models.ResultSet) {
rs.Sort(newCriteria.Append(models.PKSKSortFilter(rs.TableInfo)))
})
c.eventBus.Fire(newResultSetEvent, c.state.resultSet, resultSetUpdateResort)
}
func (c *TableReadController) PromptForQuery() tea.Msg {
return events.PromptForInputMsg{
Prompt: "query: ",
@ -276,13 +291,35 @@ func (c *TableReadController) setResultSetAndFilter(resultSet *models.ResultSet,
return c.state.buildNewResultSetMessage("")
}
func (c *TableReadController) Mark(op MarkOp) tea.Msg {
c.state.withResultSet(func(resultSet *models.ResultSet) {
for i := range resultSet.Items() {
func (c *TableReadController) Mark(op MarkOp, where string) tea.Msg {
var (
whereExpr *queryexpr.QueryExpr
err error
)
if where != "" {
whereExpr, err = queryexpr.Parse(where)
if err != nil {
return events.Error(err)
}
}
if err := c.state.withResultSetReturningError(func(resultSet *models.ResultSet) error {
for i, item := range resultSet.Items() {
if resultSet.Hidden(i) {
continue
}
if whereExpr != nil {
res, err := whereExpr.EvalItem(item)
if err != nil {
return errors.Wrapf(err, "item %d", i)
}
if !attrutils.Truthy(res) {
continue
}
}
switch op {
case MarkOpMark:
resultSet.SetMark(i, true)
@ -292,7 +329,10 @@ func (c *TableReadController) Mark(op MarkOp) tea.Msg {
resultSet.SetMark(i, !resultSet.Marked(i))
}
}
})
return nil
}); err != nil {
return events.Error(err)
}
return ResultSetUpdated{}
}
@ -377,7 +417,7 @@ func (c *TableReadController) NextPage() tea.Msg {
resultSet := c.state.ResultSet()
if resultSet == nil {
return events.StatusMsg("Result-set is nil")
} else if resultSet.LastEvaluatedKey == nil {
} else if !resultSet.HasNextPage() {
return events.StatusMsg("No more results")
}
currentFilter := c.state.filter
@ -446,12 +486,8 @@ func (c *TableReadController) updateViewToSnapshot(viewSnapshot *serialisable.Vi
}
func (c *TableReadController) CopyItemToClipboard(idx int) tea.Msg {
if err := c.initClipboard(); err != nil {
return events.Error(err)
}
itemCount := 0
c.state.withResultSet(func(resultSet *models.ResultSet) {
if err := c.state.withResultSetReturningError(func(resultSet *models.ResultSet) error {
sb := new(strings.Builder)
_ = applyToMarkedItems(resultSet, idx, func(idx int, item models.Item) error {
if sb.Len() > 0 {
@ -461,23 +497,14 @@ func (c *TableReadController) CopyItemToClipboard(idx int) tea.Msg {
itemCount += 1
return nil
})
clipboard.Write(clipboard.FmtText, []byte(sb.String()))
})
if err := c.pasteboardProvider.WriteText([]byte(sb.String())); err != nil {
return err
}
return nil
}); err != nil {
return events.Error(err)
}
return events.StatusMsg(applyToN("", itemCount, "item", "items", " copied to clipboard"))
}
func (c *TableReadController) initClipboard() error {
c.mutex.Lock()
defer c.mutex.Unlock()
if c.clipboardInit {
return nil
}
if err := clipboard.Init(); err != nil {
return errors.Wrap(err, "unable to enable clipboard")
}
c.clipboardInit = true
return nil
}

View file

@ -4,9 +4,9 @@ import (
"fmt"
"github.com/aws/aws-sdk-go-v2/service/dynamodb/types"
tea "github.com/charmbracelet/bubbletea"
"github.com/lmika/audax/internal/common/ui/events"
"github.com/lmika/audax/internal/dynamo-browse/controllers"
"github.com/lmika/audax/test/testdynamo"
"github.com/lmika/dynamo-browse/internal/common/ui/events"
"github.com/lmika/dynamo-browse/internal/dynamo-browse/controllers"
"github.com/lmika/dynamo-browse/test/testdynamo"
"github.com/stretchr/testify/assert"
"os"
"strings"
@ -89,7 +89,7 @@ func TestTableReadController_Query(t *testing.T) {
invokeCommand(t, srv.readController.Init())
invokeCommandWithPrompts(t, srv.readController.PromptForQuery(), `pk ^= "abc"`)
invokeCommand(t, srv.exportController.ExportCSV(tempFile))
invokeCommand(t, srv.exportController.ExportCSV(tempFile, controllers.ExportOptions{}))
bts, err := os.ReadFile(tempFile)
assert.NoError(t, err)
@ -107,7 +107,7 @@ func TestTableReadController_Query(t *testing.T) {
invokeCommand(t, srv.readController.Init())
invokeCommandWithPrompts(t, srv.readController.PromptForQuery(), `alpha = "This is some value"`)
invokeCommand(t, srv.exportController.ExportCSV(tempFile))
invokeCommand(t, srv.exportController.ExportCSV(tempFile, controllers.ExportOptions{}))
bts, err := os.ReadFile(tempFile)
assert.NoError(t, err)
@ -124,7 +124,7 @@ func TestTableReadController_Query(t *testing.T) {
tempFile := tempFile(t)
invokeCommandExpectingError(t, srv.readController.Init())
invokeCommandExpectingError(t, srv.exportController.ExportCSV(tempFile))
invokeCommandExpectingError(t, srv.exportController.ExportCSV(tempFile, controllers.ExportOptions{}))
})
}

View file

@ -5,11 +5,11 @@ import (
"fmt"
"github.com/aws/aws-sdk-go-v2/service/dynamodb/types"
tea "github.com/charmbracelet/bubbletea"
"github.com/lmika/audax/internal/common/sliceutils"
"github.com/lmika/audax/internal/common/ui/events"
"github.com/lmika/audax/internal/dynamo-browse/models"
"github.com/lmika/audax/internal/dynamo-browse/models/queryexpr"
"github.com/lmika/audax/internal/dynamo-browse/services/tables"
"github.com/lmika/dynamo-browse/internal/common/sliceutils"
"github.com/lmika/dynamo-browse/internal/common/ui/events"
"github.com/lmika/dynamo-browse/internal/dynamo-browse/models"
"github.com/lmika/dynamo-browse/internal/dynamo-browse/models/queryexpr"
"github.com/lmika/dynamo-browse/internal/dynamo-browse/services/tables"
"github.com/pkg/errors"
"log"
"strconv"
@ -122,6 +122,8 @@ func (twc *TableWriteController) SetAttributeValue(idx int, itemType models.Item
return twc.setBoolValue(idx, path)
case models.NullItemType:
return twc.setNullValue(idx, path)
case models.ExprValueItemType:
return twc.setToExpressionValue(idx, path)
default:
return events.Error(errors.New("unsupported attribute type"))
}
@ -151,6 +153,39 @@ func (twc *TableWriteController) setStringValue(idx int, attr *queryexpr.QueryEx
}
}
func (twc *TableWriteController) setToExpressionValue(idx int, attr *queryexpr.QueryExpr) tea.Msg {
return events.PromptForInputMsg{
Prompt: "expr value: ",
OnDone: func(value string) tea.Msg {
valueExpr, err := queryexpr.Parse(value)
if err != nil {
return events.Error(err)
}
if err := twc.state.withResultSetReturningError(func(set *models.ResultSet) error {
if err := applyToMarkedItems(set, idx, func(idx int, item models.Item) error {
newValue, err := valueExpr.EvalItem(item)
if err != nil {
return err
}
if err := attr.SetEvalItem(item, newValue); err != nil {
return err
}
set.SetDirty(idx, true)
return nil
}); err != nil {
return err
}
set.RefreshColumns()
return nil
}); err != nil {
return events.Error(err)
}
return ResultSetUpdated{}
},
}
}
func (twc *TableWriteController) setNumberValue(idx int, attr *queryexpr.QueryExpr) tea.Msg {
return events.PromptForInputMsg{
Prompt: "number value: ",
@ -239,12 +274,17 @@ func (twc *TableWriteController) DeleteAttribute(idx int, key string) tea.Msg {
}
if err := twc.state.withResultSetReturningError(func(set *models.ResultSet) error {
err := path.DeleteAttribute(set.Items()[idx])
if err != nil {
if err := applyToMarkedItems(set, idx, func(idx int, item models.Item) error {
if err := path.DeleteAttribute(set.Items()[idx]); err != nil {
return err
}
set.SetDirty(idx, true)
return nil
}); err != nil {
return err
}
set.SetDirty(idx, true)
set.RefreshColumns()
return nil
}); err != nil {
@ -418,6 +458,44 @@ func (twc *TableWriteController) assertReadWrite() error {
return nil
}
func (twc *TableWriteController) CloneItem(idx int) tea.Msg {
if err := twc.assertReadWrite(); err != nil {
return events.Error(err)
}
// Work out which keys we need to prompt for
rs := twc.state.ResultSet()
keyPrompts := &promptSequence{
prompts: []string{rs.TableInfo.Keys.PartitionKey + ": "},
}
if rs.TableInfo.Keys.SortKey != "" {
keyPrompts.prompts = append(keyPrompts.prompts, rs.TableInfo.Keys.SortKey+": ")
}
keyPrompts.onAllDone = func(values []string) tea.Msg {
twc.state.withResultSet(func(set *models.ResultSet) {
applyToMarkedItems(set, idx, func(idx int, item models.Item) error {
// TODO: should be a deep clone
clonedItem := item.Clone()
clonedItem[rs.TableInfo.Keys.PartitionKey] = &types.AttributeValueMemberS{Value: values[0]}
if len(values) == 2 {
clonedItem[rs.TableInfo.Keys.SortKey] = &types.AttributeValueMemberS{Value: values[1]}
}
set.AddNewItem(clonedItem, models.ItemAttribute{
New: true,
Dirty: true,
})
return nil
})
})
return twc.state.buildNewResultSetMessage("New item cloned")
}
return keyPrompts.next()
}
func applyToN(prefix string, n int, singular, plural, suffix string) string {
if n == 1 {
return fmt.Sprintf("%v%v %v%v", prefix, n, singular, suffix)

View file

@ -4,21 +4,22 @@ import (
"fmt"
"github.com/aws/aws-sdk-go-v2/service/dynamodb/types"
tea "github.com/charmbracelet/bubbletea"
"github.com/lmika/audax/internal/common/ui/commandctrl"
"github.com/lmika/audax/internal/dynamo-browse/controllers"
"github.com/lmika/audax/internal/dynamo-browse/models"
"github.com/lmika/audax/internal/dynamo-browse/providers/dynamo"
"github.com/lmika/audax/internal/dynamo-browse/providers/inputhistorystore"
"github.com/lmika/audax/internal/dynamo-browse/providers/settingstore"
"github.com/lmika/audax/internal/dynamo-browse/providers/workspacestore"
"github.com/lmika/audax/internal/dynamo-browse/services/inputhistory"
"github.com/lmika/audax/internal/dynamo-browse/services/itemrenderer"
"github.com/lmika/audax/internal/dynamo-browse/services/jobs"
"github.com/lmika/audax/internal/dynamo-browse/services/scriptmanager"
"github.com/lmika/audax/internal/dynamo-browse/services/tables"
"github.com/lmika/audax/internal/dynamo-browse/services/viewsnapshot"
"github.com/lmika/audax/test/testdynamo"
"github.com/lmika/audax/test/testworkspace"
"github.com/lmika/dynamo-browse/internal/common/ui/commandctrl"
"github.com/lmika/dynamo-browse/internal/dynamo-browse/controllers"
"github.com/lmika/dynamo-browse/internal/dynamo-browse/models"
"github.com/lmika/dynamo-browse/internal/dynamo-browse/providers/dynamo"
"github.com/lmika/dynamo-browse/internal/dynamo-browse/providers/inputhistorystore"
"github.com/lmika/dynamo-browse/internal/dynamo-browse/providers/pasteboardprovider"
"github.com/lmika/dynamo-browse/internal/dynamo-browse/providers/settingstore"
"github.com/lmika/dynamo-browse/internal/dynamo-browse/providers/workspacestore"
"github.com/lmika/dynamo-browse/internal/dynamo-browse/services/inputhistory"
"github.com/lmika/dynamo-browse/internal/dynamo-browse/services/itemrenderer"
"github.com/lmika/dynamo-browse/internal/dynamo-browse/services/jobs"
"github.com/lmika/dynamo-browse/internal/dynamo-browse/services/scriptmanager"
"github.com/lmika/dynamo-browse/internal/dynamo-browse/services/tables"
"github.com/lmika/dynamo-browse/internal/dynamo-browse/services/viewsnapshot"
"github.com/lmika/dynamo-browse/test/testdynamo"
"github.com/lmika/dynamo-browse/test/testworkspace"
bus "github.com/lmika/events"
"github.com/stretchr/testify/assert"
"io/fs"
@ -617,12 +618,23 @@ func newService(t *testing.T, cfg serviceConfig) *services {
state := controllers.NewState()
jobsController := controllers.NewJobsController(jobs.NewService(eventBus), eventBus, true)
readController := controllers.NewTableReadController(state, service, workspaceService, itemRendererService, jobsController, inputHistoryService, eventBus, cfg.tableName)
readController := controllers.NewTableReadController(
state,
service,
workspaceService,
itemRendererService,
jobsController,
inputHistoryService,
eventBus,
pasteboardprovider.NilProvider{},
nil,
cfg.tableName,
)
writeController := controllers.NewTableWriteController(state, service, jobsController, readController, settingStore)
settingsController := controllers.NewSettingsController(settingStore, eventBus)
columnsController := controllers.NewColumnsController(eventBus)
exportController := controllers.NewExportController(state, columnsController)
scriptController := controllers.NewScriptController(scriptService, readController, settingsController, eventBus)
columnsController := controllers.NewColumnsController(readController, eventBus)
exportController := controllers.NewExportController(state, service, jobsController, columnsController, pasteboardprovider.NilProvider{})
scriptController := controllers.NewScriptController(scriptService, readController, jobsController, settingsController, eventBus)
commandController := commandctrl.NewCommandController(inputHistoryService)
commandController.AddCommandLookupExtension(scriptController)

View file

@ -1,6 +1,6 @@
package controllers
import "github.com/lmika/audax/internal/dynamo-browse/models"
import "github.com/lmika/dynamo-browse/internal/dynamo-browse/models"
func applyToMarkedItems(rs *models.ResultSet, selectedIndex int, applyFn func(idx int, item models.Item) error) error {
if markedItems := rs.MarkedItems(); len(markedItems) > 0 {

View file

@ -3,7 +3,7 @@ package attrcodec_test
import (
"bytes"
"github.com/aws/aws-sdk-go-v2/service/dynamodb/types"
"github.com/lmika/audax/internal/dynamo-browse/models/attrcodec"
"github.com/lmika/dynamo-browse/internal/dynamo-browse/models/attrcodec"
"github.com/stretchr/testify/assert"
"strings"
"testing"

View file

@ -0,0 +1,32 @@
package attrutils
import (
"github.com/aws/aws-sdk-go-v2/service/dynamodb/types"
)
func Truthy(x types.AttributeValue) bool {
switch xVal := x.(type) {
case *types.AttributeValueMemberS:
return len(xVal.Value) > 0
case *types.AttributeValueMemberN:
return len(xVal.Value) > 0 && xVal.Value != "0"
case *types.AttributeValueMemberBOOL:
return xVal.Value
case *types.AttributeValueMemberB:
return len(xVal.Value) > 0
case *types.AttributeValueMemberNULL:
return !xVal.Value
case *types.AttributeValueMemberL:
return len(xVal.Value) > 0
case *types.AttributeValueMemberM:
return len(xVal.Value) > 0
case *types.AttributeValueMemberBS:
return len(xVal.Value) > 0
case *types.AttributeValueMemberNS:
return len(xVal.Value) > 0
case *types.AttributeValueMemberSS:
return len(xVal.Value) > 0
}
return false
}

View file

@ -1,9 +1,7 @@
package columns
import (
"github.com/aws/aws-sdk-go-v2/service/dynamodb/types"
"github.com/lmika/audax/internal/dynamo-browse/models"
"github.com/lmika/audax/internal/dynamo-browse/models/queryexpr"
"github.com/lmika/dynamo-browse/internal/dynamo-browse/models"
)
type Columns struct {
@ -19,7 +17,7 @@ func NewColumnsFromResultSet(rs *models.ResultSet) *Columns {
for i, c := range rsCols {
cols[i] = Column{
Name: c,
Evaluator: SimpleFieldValueEvaluator(c),
Evaluator: models.SimpleFieldValueEvaluator(c),
}
}
@ -44,7 +42,7 @@ func (cols *Columns) AddMissingColumns(rs *models.ResultSet) {
if _, hasCol := existingColumns[c]; !hasCol {
newCols = append(newCols, Column{
Name: c,
Evaluator: SimpleFieldValueEvaluator(c),
Evaluator: models.SimpleFieldValueEvaluator(c),
})
}
}
@ -56,7 +54,7 @@ func (cols *Columns) AddMissingColumns(rs *models.ResultSet) {
} else {
newCols[i] = Column{
Name: c,
Evaluator: SimpleFieldValueEvaluator(c),
Evaluator: models.SimpleFieldValueEvaluator(c),
}
}
}
@ -82,25 +80,6 @@ func (cols *Columns) VisibleColumns() []Column {
type Column struct {
Name string
Evaluator FieldValueEvaluator
Evaluator models.FieldValueEvaluator
Hidden bool
}
type FieldValueEvaluator interface {
EvaluateForItem(item models.Item) types.AttributeValue
}
type SimpleFieldValueEvaluator string
func (sfve SimpleFieldValueEvaluator) EvaluateForItem(item models.Item) types.AttributeValue {
return item[string(sfve)]
}
type ExprFieldValueEvaluator struct {
Expr *queryexpr.QueryExpr
}
func (sfve ExprFieldValueEvaluator) EvaluateForItem(item models.Item) types.AttributeValue {
val, _ := sfve.Expr.EvalItem(item)
return val
}

View file

@ -0,0 +1,15 @@
package models
import (
"github.com/aws/aws-sdk-go-v2/service/dynamodb/types"
)
type FieldValueEvaluator interface {
EvaluateForItem(item Item) types.AttributeValue
}
type SimpleFieldValueEvaluator string
func (sfve SimpleFieldValueEvaluator) EvaluateForItem(item Item) types.AttributeValue {
return item[string(sfve)]
}

View file

@ -0,0 +1,25 @@
package evaluators
import (
"github.com/lmika/dynamo-browse/internal/dynamo-browse/models"
"github.com/lmika/dynamo-browse/internal/dynamo-browse/models/queryexpr"
)
func Equals(x, y models.FieldValueEvaluator) bool {
if x == nil {
return y == nil
}
switch xt := x.(type) {
case models.SimpleFieldValueEvaluator:
if yt, ok := y.(models.SimpleFieldValueEvaluator); ok {
return xt == yt
}
case queryexpr.ExprFieldValueEvaluator:
if yt, ok := y.(queryexpr.ExprFieldValueEvaluator); ok {
return xt.Expr.Equal(yt.Expr)
}
}
return false
}

View file

@ -2,7 +2,7 @@ package models
import (
"github.com/aws/aws-sdk-go-v2/service/dynamodb/types"
"github.com/lmika/audax/internal/dynamo-browse/models/attrutils"
"github.com/lmika/dynamo-browse/internal/dynamo-browse/models/attrutils"
)
type ItemIndex struct {
@ -16,7 +16,7 @@ type Item map[string]types.AttributeValue
func (i Item) Clone() Item {
newItem := Item{}
// TODO: should be a deep clone?
// TODO: should be a deep clone? YES!!
for k, v := range i {
newItem[k] = v
}
@ -33,6 +33,14 @@ func (i Item) KeyValue(info *TableInfo) map[string]types.AttributeValue {
return itemKey
}
func (i Item) PKSK(info *TableInfo) (pk types.AttributeValue, sk types.AttributeValue) {
pk = i[info.Keys.PartitionKey]
if info.Keys.SortKey != "" {
sk = i[info.Keys.SortKey]
}
return pk, sk
}
func (i Item) AttributeValueAsString(key string) (string, bool) {
return attrutils.AttributeToString(i[key])
}

View file

@ -8,4 +8,6 @@ const (
NumberItemType ItemType = "N"
BoolItemType ItemType = "BOOL"
NullItemType ItemType = "NULL"
ExprValueItemType ItemType = "exprvalue"
)

View file

@ -3,12 +3,14 @@ package models
import (
"github.com/aws/aws-sdk-go-v2/service/dynamodb/types"
"sort"
"time"
)
type ResultSet struct {
// Query information
TableInfo *TableInfo
Query Queryable
Created time.Time
ExclusiveStartKey map[string]types.AttributeValue
// Result information
@ -16,7 +18,8 @@ type ResultSet struct {
items []Item
attributes []ItemAttribute
columns []string
columns []string
sortCriteria SortCriteria
}
type Queryable interface {
@ -46,6 +49,10 @@ func (rs *ResultSet) SetItems(items []Item) {
rs.attributes = make([]ItemAttribute, len(items))
}
func (rs *ResultSet) SortCriteria() SortCriteria {
return rs.sortCriteria
}
func (rs *ResultSet) AddNewItem(item Item, attrs ItemAttribute) {
rs.items = append(rs.items, item)
rs.attributes = append(rs.attributes, attrs)
@ -135,3 +142,12 @@ func (rs *ResultSet) RefreshColumns() {
rs.columns = columns
}
func (rs *ResultSet) HasNextPage() bool {
return rs.LastEvaluatedKey != nil
}
func (rs *ResultSet) Sort(criteria SortCriteria) {
rs.sortCriteria = criteria
Sort(rs.items, criteria)
}

View file

@ -1,6 +1,6 @@
package modexpr
import "github.com/lmika/audax/internal/dynamo-browse/models"
import "github.com/lmika/dynamo-browse/internal/dynamo-browse/models"
func (a *astExpr) calcPatchMods(item models.Item) ([]patchMod, error) {
patchMods := make([]patchMod, 0)

View file

@ -1,6 +1,6 @@
package modexpr
import "github.com/lmika/audax/internal/dynamo-browse/models"
import "github.com/lmika/dynamo-browse/internal/dynamo-browse/models"
type ModExpr struct {
ast *astExpr

View file

@ -4,8 +4,8 @@ import (
"testing"
"github.com/aws/aws-sdk-go-v2/service/dynamodb/types"
"github.com/lmika/audax/internal/dynamo-browse/models"
"github.com/lmika/audax/internal/dynamo-browse/models/modexpr"
"github.com/lmika/dynamo-browse/internal/dynamo-browse/models"
"github.com/lmika/dynamo-browse/internal/dynamo-browse/models/modexpr"
"github.com/stretchr/testify/assert"
)

View file

@ -2,7 +2,7 @@ package modexpr
import (
"github.com/aws/aws-sdk-go-v2/service/dynamodb/types"
"github.com/lmika/audax/internal/dynamo-browse/models"
"github.com/lmika/dynamo-browse/internal/dynamo-browse/models"
)
type patchMod interface {

View file

@ -3,7 +3,7 @@ package models
import (
"github.com/aws/aws-sdk-go-v2/aws"
"github.com/aws/aws-sdk-go-v2/feature/dynamodb/expression"
"github.com/lmika/audax/internal/dynamo-browse/models/itemrender"
"github.com/lmika/dynamo-browse/internal/dynamo-browse/models/itemrender"
)
type QueryExecutionPlan struct {

View file

@ -4,17 +4,23 @@ import (
"github.com/alecthomas/participle/v2"
"github.com/alecthomas/participle/v2/lexer"
"github.com/aws/aws-sdk-go-v2/feature/dynamodb/expression"
"github.com/aws/aws-sdk-go-v2/service/dynamodb/types"
"github.com/lmika/audax/internal/common/sliceutils"
"github.com/lmika/audax/internal/dynamo-browse/models"
"github.com/lmika/dynamo-browse/internal/common/sliceutils"
"github.com/lmika/dynamo-browse/internal/dynamo-browse/models"
"github.com/pkg/errors"
"strconv"
)
// Modelled on the expression language here
// https://docs.aws.amazon.com/amazondynamodb/latest/developerguide/Expressions.OperatorsAndFunctions.html
type astExpr struct {
Root *astDisjunction `parser:"@@"`
Root *astDisjunction `parser:"@@"`
Options *astOptions `parser:"( 'using' @@ )?"`
}
type astOptions struct {
Scan bool `parser:"@'scan'"`
Index string `parser:" | 'index' '(' @String ')'"`
}
type astDisjunction struct {
@ -38,9 +44,15 @@ type astIn struct {
}
type astComparisonOp struct {
Ref *astEqualityOp `parser:"@@"`
Op string `parser:"( @('<' | '<=' | '>' | '>=')"`
Value *astEqualityOp `parser:"@@ )?"`
Ref *astBetweenOp `parser:"@@"`
Op string `parser:"( @('<' | '<=' | '>' | '>=')"`
Value *astBetweenOp `parser:"@@ )?"`
}
type astBetweenOp struct {
Ref *astEqualityOp `parser:"@@"`
From *astEqualityOp `parser:"( 'between' @@ "`
To *astEqualityOp `parser:" 'and' @@ )?"`
}
type astEqualityOp struct {
@ -58,7 +70,6 @@ type astIsOp struct {
type astSubRef struct {
Ref *astFunctionCall `parser:"@@"`
SubRefs []*astSubRefType `parser:"@@*"`
//Quals []string `parser:"('.' @Ident)*"`
}
type astSubRefType struct {
@ -121,7 +132,58 @@ func Parse(expr string) (*QueryExpr, error) {
return &QueryExpr{ast: ast}, nil
}
func (a *astExpr) calcQuery(ctx *evalContext, info *models.TableInfo) (*models.QueryExecutionPlan, error) {
func (a *astExpr) calcQuery(ctx *evalContext, info *models.TableInfo, preferredIndex string) (*models.QueryExecutionPlan, error) {
plans, err := a.determinePlausibleExecutionPlans(ctx, info)
if err != nil {
return nil, err
}
scanPlan, _ := sliceutils.FindLast(plans, func(p *models.QueryExecutionPlan) bool {
return !p.CanQuery
})
queryPlans := sliceutils.Filter(plans, func(p *models.QueryExecutionPlan) bool {
if !p.CanQuery {
return false
}
return true
})
if len(queryPlans) == 0 || (a.Options != nil && a.Options.Scan) {
if preferredIndex != "" {
return nil, NoPlausiblePlanWithIndexError{
PreferredIndex: preferredIndex,
PossibleIndices: sliceutils.Map(queryPlans, func(p *models.QueryExecutionPlan) string { return p.IndexName }),
}
}
return scanPlan, nil
}
if preferredIndex == "" && (a.Options != nil && a.Options.Index != "") {
preferredIndex, err = strconv.Unquote(a.Options.Index)
if err != nil {
return nil, err
}
}
if preferredIndex != "" {
queryPlans = sliceutils.Filter(queryPlans, func(p *models.QueryExecutionPlan) bool { return p.IndexName == preferredIndex })
}
if len(queryPlans) == 1 {
return queryPlans[0], nil
} else if len(queryPlans) == 0 {
return nil, NoPlausiblePlanWithIndexError{
PreferredIndex: preferredIndex,
}
}
return nil, MultiplePlansWithIndexError{
PossibleIndices: sliceutils.Map(queryPlans, func(p *models.QueryExecutionPlan) string { return p.IndexName }),
}
}
func (a *astExpr) determinePlausibleExecutionPlans(ctx *evalContext, info *models.TableInfo) ([]*models.QueryExecutionPlan, error) {
plans := make([]*models.QueryExecutionPlan, 0)
type queryTestAttempt struct {
index string
keysUnderTest models.KeyAttribute
@ -153,11 +215,11 @@ func (a *astExpr) calcQuery(ctx *evalContext, info *models.TableInfo) (*models.Q
return nil, err
}
return &models.QueryExecutionPlan{
plans = append(plans, &models.QueryExecutionPlan{
CanQuery: true,
IndexName: attempt.index,
Expression: expr,
}, nil
})
}
}
@ -174,21 +236,22 @@ func (a *astExpr) calcQuery(ctx *evalContext, info *models.TableInfo) (*models.Q
return nil, err
}
return &models.QueryExecutionPlan{
plans = append(plans, &models.QueryExecutionPlan{
CanQuery: false,
Expression: expr,
}, nil
})
return plans, nil
}
func (a *astExpr) evalToIR(ctx *evalContext, tableInfo *models.TableInfo) (irAtom, error) {
return a.Root.evalToIR(ctx, tableInfo)
}
func (a *astExpr) evalItem(ctx *evalContext, item models.Item) (types.AttributeValue, error) {
func (a *astExpr) evalItem(ctx *evalContext, item models.Item) (exprValue, error) {
return a.Root.evalItem(ctx, item)
}
func (a *astExpr) setEvalItem(ctx *evalContext, item models.Item, value types.AttributeValue) error {
func (a *astExpr) setEvalItem(ctx *evalContext, item models.Item, value exprValue) error {
return a.Root.setEvalItem(ctx, item, value)
}

View file

@ -1,8 +1,7 @@
package queryexpr
import (
"github.com/aws/aws-sdk-go-v2/service/dynamodb/types"
"github.com/lmika/audax/internal/dynamo-browse/models"
"github.com/lmika/dynamo-browse/internal/dynamo-browse/models"
"github.com/pkg/errors"
)
@ -21,15 +20,6 @@ func (a *astAtom) evalToIR(ctx *evalContext, info *models.TableInfo) (irAtom, er
return nil, errors.New("unhandled atom case")
}
func (a *astAtom) rightOperandDynamoValue() (types.AttributeValue, error) {
switch {
case a.Literal != nil:
return a.Literal.dynamoValue()
}
return nil, errors.New("unhandled atom case")
}
func (a *astAtom) unqualifiedName() (string, bool) {
switch {
case a.Ref != nil:
@ -39,12 +29,12 @@ func (a *astAtom) unqualifiedName() (string, bool) {
return "", false
}
func (a *astAtom) evalItem(ctx *evalContext, item models.Item) (types.AttributeValue, error) {
func (a *astAtom) evalItem(ctx *evalContext, item models.Item) (exprValue, error) {
switch {
case a.Ref != nil:
return a.Ref.evalItem(ctx, item)
case a.Literal != nil:
return a.Literal.dynamoValue()
return a.Literal.exprValue()
case a.Placeholder != nil:
return a.Placeholder.evalItem(ctx, item)
case a.Paren != nil:
@ -66,7 +56,7 @@ func (a *astAtom) canModifyItem(ctx *evalContext, item models.Item) bool {
return false
}
func (a *astAtom) setEvalItem(ctx *evalContext, item models.Item, value types.AttributeValue) error {
func (a *astAtom) setEvalItem(ctx *evalContext, item models.Item, value exprValue) error {
switch {
case a.Ref != nil:
return a.Ref.setEvalItem(ctx, item, value)

View file

@ -0,0 +1,155 @@
package queryexpr
import (
"fmt"
"github.com/aws/aws-sdk-go-v2/feature/dynamodb/expression"
"github.com/lmika/dynamo-browse/internal/dynamo-browse/models"
)
func (a *astBetweenOp) evalToIR(ctx *evalContext, info *models.TableInfo) (irAtom, error) {
leftIR, err := a.Ref.evalToIR(ctx, info)
if err != nil {
return nil, err
}
if a.From == nil {
return leftIR, nil
}
nameIR, isNameIR := leftIR.(nameIRAtom)
if !isNameIR {
return nil, OperandNotANameError(a.Ref.String())
}
fromIR, err := a.From.evalToIR(ctx, info)
if err != nil {
return nil, err
}
toIR, err := a.To.evalToIR(ctx, info)
if err != nil {
return nil, err
}
fromOprIR, isFromOprIR := fromIR.(valueIRAtom)
if !isFromOprIR {
return nil, OperandNotAnOperandError{}
}
toOprIR, isToOprIR := toIR.(valueIRAtom)
if !isToOprIR {
return nil, OperandNotAnOperandError{}
}
return irBetween{name: nameIR, from: fromOprIR, to: toOprIR}, nil
}
func (a *astBetweenOp) evalItem(ctx *evalContext, item models.Item) (exprValue, error) {
val, err := a.Ref.evalItem(ctx, item)
if a.From == nil {
return val, err
}
fromIR, err := a.From.evalItem(ctx, item)
if err != nil {
return nil, err
}
toIR, err := a.To.evalItem(ctx, item)
if err != nil {
return nil, err
}
switch v := val.(type) {
case stringableExprValue:
fromNumVal, isFromNumVal := fromIR.(stringableExprValue)
if !isFromNumVal {
return nil, ValuesNotComparable{Left: val.asAttributeValue(), Right: fromIR.asAttributeValue()}
}
toNumVal, isToNumVal := toIR.(stringableExprValue)
if !isToNumVal {
return nil, ValuesNotComparable{Left: val.asAttributeValue(), Right: toNumVal.asAttributeValue()}
}
return boolExprValue(v.asString() >= fromNumVal.asString() && v.asString() <= toNumVal.asString()), nil
case numberableExprValue:
fromNumVal, isFromNumVal := fromIR.(numberableExprValue)
if !isFromNumVal {
return nil, ValuesNotComparable{Left: val.asAttributeValue(), Right: fromIR.asAttributeValue()}
}
toNumVal, isToNumVal := toIR.(numberableExprValue)
if !isToNumVal {
return nil, ValuesNotComparable{Left: val.asAttributeValue(), Right: toNumVal.asAttributeValue()}
}
fromCmp := v.asBigFloat().Cmp(fromNumVal.asBigFloat())
toCmp := v.asBigFloat().Cmp(toNumVal.asBigFloat())
return boolExprValue(fromCmp >= 0 && toCmp <= 0), nil
}
return nil, InvalidTypeForBetweenError{TypeName: val.typeName()}
}
func (a *astBetweenOp) canModifyItem(ctx *evalContext, item models.Item) bool {
if a.From != nil {
return false
}
return a.Ref.canModifyItem(ctx, item)
}
func (a *astBetweenOp) setEvalItem(ctx *evalContext, item models.Item, value exprValue) error {
if a.From != nil {
return PathNotSettableError{}
}
return a.Ref.setEvalItem(ctx, item, value)
}
func (a *astBetweenOp) deleteAttribute(ctx *evalContext, item models.Item) error {
if a.From != nil {
return PathNotSettableError{}
}
return a.Ref.deleteAttribute(ctx, item)
}
func (a *astBetweenOp) String() string {
name := a.Ref.String()
if a.From != nil {
return fmt.Sprintf("%v between %v and %v", name, a.From.String(), a.To.String())
}
return name
}
type irBetween struct {
name nameIRAtom
from valueIRAtom
to valueIRAtom
}
func (i irBetween) calcQueryForScan(info *models.TableInfo) (expression.ConditionBuilder, error) {
nb := i.name.calcName(info)
fb := i.from.calcOperand(info)
tb := i.to.calcOperand(info)
return nb.Between(fb, tb), nil
}
func (i irBetween) canBeExecutedAsQuery(qci *queryCalcInfo) bool {
keyName := i.name.keyName()
if keyName == "" {
return false
}
if keyName == qci.keysUnderTest.SortKey {
return qci.addKey(keyName)
}
return false
}
func (i irBetween) calcQueryForQuery() (expression.KeyConditionBuilder, error) {
nb := i.name.keyName()
fb := i.from.exprValue()
tb := i.to.exprValue()
return expression.Key(nb).Between(buildExpressionFromValue(fb), buildExpressionFromValue(tb)), nil
}

View file

@ -2,8 +2,7 @@ package queryexpr
import (
"github.com/aws/aws-sdk-go-v2/feature/dynamodb/expression"
"github.com/aws/aws-sdk-go-v2/service/dynamodb/types"
"github.com/lmika/audax/internal/dynamo-browse/models"
"github.com/lmika/dynamo-browse/internal/dynamo-browse/models"
"strings"
)
@ -20,7 +19,7 @@ func (a *astBooleanNot) evalToIR(ctx *evalContext, tableInfo *models.TableInfo)
return &irBoolNot{atom: irNode}, nil
}
func (a *astBooleanNot) evalItem(ctx *evalContext, item models.Item) (types.AttributeValue, error) {
func (a *astBooleanNot) evalItem(ctx *evalContext, item models.Item) (exprValue, error) {
val, err := a.Operand.evalItem(ctx, item)
if err != nil {
return nil, err
@ -30,7 +29,7 @@ func (a *astBooleanNot) evalItem(ctx *evalContext, item models.Item) (types.Attr
return val, nil
}
return &types.AttributeValueMemberBOOL{Value: !isAttributeTrue(val)}, nil
return boolExprValue(!isAttributeTrue(val)), nil
}
func (a *astBooleanNot) canModifyItem(ctx *evalContext, item models.Item) bool {
@ -40,7 +39,7 @@ func (a *astBooleanNot) canModifyItem(ctx *evalContext, item models.Item) bool {
return a.Operand.canModifyItem(ctx, item)
}
func (a *astBooleanNot) setEvalItem(ctx *evalContext, item models.Item, value types.AttributeValue) error {
func (a *astBooleanNot) setEvalItem(ctx *evalContext, item models.Item, value exprValue) error {
if a.HasNot {
return PathNotSettableError{}
}

View file

@ -2,38 +2,127 @@ package queryexpr
import (
"context"
"github.com/aws/aws-sdk-go-v2/service/dynamodb/types"
"github.com/pkg/errors"
"strconv"
)
type nativeFunc func(ctx context.Context, args []types.AttributeValue) (types.AttributeValue, error)
type nativeFunc func(ctx context.Context, args []exprValue) (exprValue, error)
var nativeFuncs = map[string]nativeFunc{
"size": func(ctx context.Context, args []types.AttributeValue) (types.AttributeValue, error) {
"size": func(ctx context.Context, args []exprValue) (exprValue, error) {
if len(args) != 1 {
return nil, InvalidArgumentNumberError{Name: "size", Expected: 1, Actual: len(args)}
}
var l int
switch t := args[0].(type) {
case *types.AttributeValueMemberB:
l = len(t.Value)
case *types.AttributeValueMemberS:
l = len(t.Value)
case *types.AttributeValueMemberL:
l = len(t.Value)
case *types.AttributeValueMemberM:
l = len(t.Value)
case *types.AttributeValueMemberSS:
l = len(t.Value)
case *types.AttributeValueMemberNS:
l = len(t.Value)
case *types.AttributeValueMemberBS:
l = len(t.Value)
case stringExprValue:
l = len(t)
case mappableExprValue:
l = t.len()
case slicableExprValue:
l = t.len()
default:
return nil, errors.New("cannot take size of arg")
}
return &types.AttributeValueMemberN{Value: strconv.Itoa(l)}, nil
return int64ExprValue(l), nil
},
"range": func(ctx context.Context, args []exprValue) (exprValue, error) {
if len(args) != 2 {
return nil, InvalidArgumentNumberError{Name: "range", Expected: 2, Actual: len(args)}
}
xVal, isXNum := args[0].(numberableExprValue)
if !isXNum {
return nil, InvalidArgumentTypeError{Name: "range", ArgIndex: 0, Expected: "N"}
}
yVal, isYNum := args[1].(numberableExprValue)
if !isYNum {
return nil, InvalidArgumentTypeError{Name: "range", ArgIndex: 1, Expected: "N"}
}
xInt, _ := xVal.asBigFloat().Int64()
yInt, _ := yVal.asBigFloat().Int64()
xs := make([]exprValue, 0)
for x := xInt; x <= yInt; x++ {
xs = append(xs, int64ExprValue(x))
}
return listExprValue(xs), nil
},
"marked": func(ctx context.Context, args []exprValue) (exprValue, error) {
if len(args) != 1 {
return nil, InvalidArgumentNumberError{Name: "marked", Expected: 1, Actual: len(args)}
}
fieldName, ok := args[0].(stringableExprValue)
if !ok {
return nil, InvalidArgumentTypeError{Name: "marked", ArgIndex: 0, Expected: "S"}
}
rs := currentResultSetFromContext(ctx)
if rs == nil {
return listExprValue{}, nil
}
var items = []exprValue{}
for i, itm := range rs.Items() {
if !rs.Marked(i) {
continue
}
attr, hasAttr := itm[fieldName.asString()]
if !hasAttr {
continue
}
exprAttrValue, err := newExprValueFromAttributeValue(attr)
if err != nil {
return nil, errors.Wrapf(err, "marked(): item %d, attr %v", i, fieldName.asString())
}
items = append(items, exprAttrValue)
}
return listExprValue(items), nil
},
"_x_now": func(ctx context.Context, args []exprValue) (exprValue, error) {
now := timeSourceFromContext(ctx).now().Unix()
return int64ExprValue(now), nil
},
"_x_add": func(ctx context.Context, args []exprValue) (exprValue, error) {
if len(args) != 2 {
return nil, InvalidArgumentNumberError{Name: "_x_add", Expected: 2, Actual: len(args)}
}
xVal, isXNum := args[0].(numberableExprValue)
if !isXNum {
return nil, InvalidArgumentTypeError{Name: "_x_add", ArgIndex: 0, Expected: "N"}
}
yVal, isYNum := args[1].(numberableExprValue)
if !isYNum {
return nil, InvalidArgumentTypeError{Name: "_x_add", ArgIndex: 1, Expected: "N"}
}
return bigNumExprValue{num: xVal.asBigFloat().Add(xVal.asBigFloat(), yVal.asBigFloat())}, nil
},
"_x_concat": func(ctx context.Context, args []exprValue) (exprValue, error) {
if len(args) != 2 {
return nil, InvalidArgumentNumberError{Name: "_x_concat", Expected: 2, Actual: len(args)}
}
xVal, isXNum := args[0].(stringableExprValue)
if !isXNum {
return nil, InvalidArgumentTypeError{Name: "_x_concat", ArgIndex: 0, Expected: "S"}
}
yVal, isYNum := args[1].(stringableExprValue)
if !isYNum {
return nil, InvalidArgumentTypeError{Name: "_x_concat", ArgIndex: 1, Expected: "S"}
}
return stringExprValue(xVal.asString() + yVal.asString()), nil
},
}

View file

@ -2,9 +2,8 @@ package queryexpr
import (
"github.com/aws/aws-sdk-go-v2/feature/dynamodb/expression"
"github.com/aws/aws-sdk-go-v2/service/dynamodb/types"
"github.com/lmika/audax/internal/dynamo-browse/models"
"github.com/lmika/audax/internal/dynamo-browse/models/attrutils"
"github.com/lmika/dynamo-browse/internal/dynamo-browse/models"
"github.com/lmika/dynamo-browse/internal/dynamo-browse/models/attrutils"
"github.com/pkg/errors"
)
@ -47,7 +46,7 @@ func (a *astComparisonOp) evalToIR(ctx *evalContext, info *models.TableInfo) (ir
return irGenericCmp{leftOpr, rightOpr, cmpType}, nil
}
func (a *astComparisonOp) evalItem(ctx *evalContext, item models.Item) (types.AttributeValue, error) {
func (a *astComparisonOp) evalItem(ctx *evalContext, item models.Item) (exprValue, error) {
left, err := a.Ref.evalItem(ctx, item)
if err != nil {
return nil, err
@ -61,20 +60,21 @@ func (a *astComparisonOp) evalItem(ctx *evalContext, item models.Item) (types.At
return nil, err
}
cmp, isComparable := attrutils.CompareScalarAttributes(left, right)
// TODO: use expr value here
cmp, isComparable := attrutils.CompareScalarAttributes(left.asAttributeValue(), right.asAttributeValue())
if !isComparable {
return nil, ValuesNotComparable{Left: left, Right: right}
return nil, ValuesNotComparable{Left: left.asAttributeValue(), Right: right.asAttributeValue()}
}
switch opToCmdType[a.Op] {
case cmpTypeLt:
return &types.AttributeValueMemberBOOL{Value: cmp < 0}, nil
return boolExprValue(cmp < 0), nil
case cmpTypeLe:
return &types.AttributeValueMemberBOOL{Value: cmp <= 0}, nil
return boolExprValue(cmp <= 0), nil
case cmpTypeGt:
return &types.AttributeValueMemberBOOL{Value: cmp > 0}, nil
return boolExprValue(cmp > 0), nil
case cmpTypeGe:
return &types.AttributeValueMemberBOOL{Value: cmp >= 0}, nil
return boolExprValue(cmp >= 0), nil
}
return nil, errors.Errorf("unrecognised operator: %v", a.Op)
}
@ -86,7 +86,7 @@ func (a *astComparisonOp) canModifyItem(ctx *evalContext, item models.Item) bool
return a.Ref.canModifyItem(ctx, item)
}
func (a *astComparisonOp) setEvalItem(ctx *evalContext, item models.Item, value types.AttributeValue) error {
func (a *astComparisonOp) setEvalItem(ctx *evalContext, item models.Item, value exprValue) error {
if a.Op != "" {
return PathNotSettableError{}
}
@ -143,34 +143,34 @@ func (a irKeyFieldCmp) canBeExecutedAsQuery(qci *queryCalcInfo) bool {
func (a irKeyFieldCmp) calcQueryForScan(info *models.TableInfo) (expression.ConditionBuilder, error) {
nb := a.name.calcName(info)
vb := a.value.goValue()
vb := a.value.exprValue()
switch a.cmpType {
case cmpTypeLt:
return nb.LessThan(expression.Value(vb)), nil
return nb.LessThan(buildExpressionFromValue(vb)), nil
case cmpTypeLe:
return nb.LessThanEqual(expression.Value(vb)), nil
return nb.LessThanEqual(buildExpressionFromValue(vb)), nil
case cmpTypeGt:
return nb.GreaterThan(expression.Value(vb)), nil
return nb.GreaterThan(buildExpressionFromValue(vb)), nil
case cmpTypeGe:
return nb.GreaterThanEqual(expression.Value(vb)), nil
return nb.GreaterThanEqual(buildExpressionFromValue(vb)), nil
}
return expression.ConditionBuilder{}, errors.New("unsupported cmp type")
}
func (a irKeyFieldCmp) calcQueryForQuery() (expression.KeyConditionBuilder, error) {
keyName := a.name.keyName()
vb := a.value.goValue()
vb := a.value.exprValue()
switch a.cmpType {
case cmpTypeLt:
return expression.Key(keyName).LessThan(expression.Value(vb)), nil
return expression.Key(keyName).LessThan(buildExpressionFromValue(vb)), nil
case cmpTypeLe:
return expression.Key(keyName).LessThanEqual(expression.Value(vb)), nil
return expression.Key(keyName).LessThanEqual(buildExpressionFromValue(vb)), nil
case cmpTypeGt:
return expression.Key(keyName).GreaterThan(expression.Value(vb)), nil
return expression.Key(keyName).GreaterThan(buildExpressionFromValue(vb)), nil
case cmpTypeGe:
return expression.Key(keyName).GreaterThanEqual(expression.Value(vb)), nil
return expression.Key(keyName).GreaterThanEqual(buildExpressionFromValue(vb)), nil
}
return expression.KeyConditionBuilder{}, errors.New("unsupported cmp type")
}

View file

@ -2,8 +2,8 @@ package queryexpr
import (
"github.com/aws/aws-sdk-go-v2/feature/dynamodb/expression"
"github.com/aws/aws-sdk-go-v2/service/dynamodb/types"
"github.com/lmika/audax/internal/dynamo-browse/models"
"github.com/lmika/dynamo-browse/internal/dynamo-browse/models"
"math/big"
"strings"
)
@ -36,7 +36,7 @@ func (a *astConjunction) evalToIR(ctx *evalContext, tableInfo *models.TableInfo)
return &irMultiConjunction{atoms: atoms}, nil
}
func (a *astConjunction) evalItem(ctx *evalContext, item models.Item) (types.AttributeValue, error) {
func (a *astConjunction) evalItem(ctx *evalContext, item models.Item) (exprValue, error) {
val, err := a.Operands[0].evalItem(ctx, item)
if err != nil {
return nil, err
@ -47,7 +47,7 @@ func (a *astConjunction) evalItem(ctx *evalContext, item models.Item) (types.Att
for _, opr := range a.Operands[1:] {
if !isAttributeTrue(val) {
return &types.AttributeValueMemberBOOL{Value: false}, nil
return boolExprValue(false), nil
}
val, err = opr.evalItem(ctx, item)
@ -56,7 +56,7 @@ func (a *astConjunction) evalItem(ctx *evalContext, item models.Item) (types.Att
}
}
return &types.AttributeValueMemberBOOL{Value: isAttributeTrue(val)}, nil
return boolExprValue(isAttributeTrue(val)), nil
}
func (a *astConjunction) canModifyItem(ctx *evalContext, item models.Item) bool {
@ -67,7 +67,7 @@ func (a *astConjunction) canModifyItem(ctx *evalContext, item models.Item) bool
return false
}
func (a *astConjunction) setEvalItem(ctx *evalContext, item models.Item, value types.AttributeValue) error {
func (a *astConjunction) setEvalItem(ctx *evalContext, item models.Item, value exprValue) error {
if len(a.Operands) == 1 {
return a.Operands[0].setEvalItem(ctx, item, value)
}
@ -168,16 +168,16 @@ func (d *irMultiConjunction) calcQueryForScan(info *models.TableInfo) (expressio
return conjExpr, nil
}
func isAttributeTrue(attr types.AttributeValue) bool {
func isAttributeTrue(attr exprValue) bool {
switch val := attr.(type) {
case *types.AttributeValueMemberS:
return val.Value != ""
case *types.AttributeValueMemberN:
return val.Value != "0"
case *types.AttributeValueMemberBOOL:
return val.Value
case *types.AttributeValueMemberNULL:
case nullExprValue:
return false
case boolExprValue:
return bool(val)
case stringableExprValue:
return val.asString() != ""
case numberableExprValue:
return val.asBigFloat().Cmp(&big.Float{}) != 0
}
return true
}

View file

@ -0,0 +1,40 @@
package queryexpr
import (
"context"
"time"
"github.com/lmika/dynamo-browse/internal/dynamo-browse/models"
)
type timeSource interface {
now() time.Time
}
type defaultTimeSource struct{}
func (tds defaultTimeSource) now() time.Time {
return time.Now()
}
type timeSourceContextKeyType struct{}
var timeSourceContextKey = timeSourceContextKeyType{}
func timeSourceFromContext(ctx context.Context) timeSource {
if tts, ok := ctx.Value(timeSourceContextKey).(timeSource); ok {
return tts
}
return defaultTimeSource{}
}
type currentResultSetContextKeyType struct{}
var currentResultSetContextKey = currentResultSetContextKeyType{}
func currentResultSetFromContext(ctx context.Context) *models.ResultSet {
if crs, ok := ctx.Value(currentResultSetContextKey).(*models.ResultSet); ok {
return crs
}
return nil
}

View file

@ -2,8 +2,7 @@ package queryexpr
import (
"github.com/aws/aws-sdk-go-v2/feature/dynamodb/expression"
"github.com/aws/aws-sdk-go-v2/service/dynamodb/types"
"github.com/lmika/audax/internal/dynamo-browse/models"
"github.com/lmika/dynamo-browse/internal/dynamo-browse/models"
"strings"
)
@ -24,7 +23,7 @@ func (a *astDisjunction) evalToIR(ctx *evalContext, tableInfo *models.TableInfo)
return &irDisjunction{conj: conj}, nil
}
func (a *astDisjunction) evalItem(ctx *evalContext, item models.Item) (types.AttributeValue, error) {
func (a *astDisjunction) evalItem(ctx *evalContext, item models.Item) (exprValue, error) {
val, err := a.Operands[0].evalItem(ctx, item)
if err != nil {
return nil, err
@ -35,7 +34,7 @@ func (a *astDisjunction) evalItem(ctx *evalContext, item models.Item) (types.Att
for _, opr := range a.Operands[1:] {
if isAttributeTrue(val) {
return &types.AttributeValueMemberBOOL{Value: true}, nil
return boolExprValue(true), nil
}
val, err = opr.evalItem(ctx, item)
@ -44,7 +43,7 @@ func (a *astDisjunction) evalItem(ctx *evalContext, item models.Item) (types.Att
}
}
return &types.AttributeValueMemberBOOL{Value: isAttributeTrue(val)}, nil
return boolExprValue(isAttributeTrue(val)), nil
}
func (a *astDisjunction) canModifyItem(ctx *evalContext, item models.Item) bool {
@ -55,7 +54,7 @@ func (a *astDisjunction) canModifyItem(ctx *evalContext, item models.Item) bool
return false
}
func (a *astDisjunction) setEvalItem(ctx *evalContext, item models.Item, value types.AttributeValue) error {
func (a *astDisjunction) setEvalItem(ctx *evalContext, item models.Item, value exprValue) error {
if len(a.Operands) == 1 {
return a.Operands[0].setEvalItem(ctx, item, value)
}

View file

@ -3,8 +3,7 @@ package queryexpr
import (
"fmt"
"github.com/aws/aws-sdk-go-v2/feature/dynamodb/expression"
"github.com/aws/aws-sdk-go-v2/service/dynamodb/types"
"github.com/lmika/audax/internal/dynamo-browse/models"
"github.com/lmika/dynamo-browse/internal/dynamo-browse/models"
"strings"
)
@ -16,21 +15,21 @@ func (dt *astRef) unqualifiedName() (string, bool) {
return dt.Name, true
}
func (dt *astRef) evalItem(ctx *evalContext, item models.Item) (types.AttributeValue, error) {
func (dt *astRef) evalItem(ctx *evalContext, item models.Item) (exprValue, error) {
res, hasV := item[dt.Name]
if !hasV {
return nil, nil
return undefinedExprValue{}, nil
}
return res, nil
return newExprValueFromAttributeValue(res)
}
func (dt *astRef) canModifyItem(ctx *evalContext, item models.Item) bool {
return true
}
func (dt *astRef) setEvalItem(ctx *evalContext, item models.Item, value types.AttributeValue) error {
item[dt.Name] = value
func (dt *astRef) setEvalItem(ctx *evalContext, item models.Item, value exprValue) error {
item[dt.Name] = value.asAttributeValue()
return nil
}
@ -71,7 +70,7 @@ func (i irNamePath) calcName(info *models.TableInfo) expression.NameBuilder {
switch v := qual.(type) {
case string:
fullName.WriteString("." + v)
case int:
case int64:
fullName.WriteString(fmt.Sprintf("[%v]", qual))
}
}

View file

@ -2,9 +2,8 @@ package queryexpr
import (
"github.com/aws/aws-sdk-go-v2/feature/dynamodb/expression"
"github.com/aws/aws-sdk-go-v2/service/dynamodb/types"
"github.com/lmika/audax/internal/dynamo-browse/models"
"github.com/lmika/audax/internal/dynamo-browse/models/attrutils"
"github.com/lmika/dynamo-browse/internal/dynamo-browse/models"
"github.com/lmika/dynamo-browse/internal/dynamo-browse/models/attrutils"
"github.com/pkg/errors"
"strings"
)
@ -59,7 +58,7 @@ func (a *astEqualityOp) evalToIR(ctx *evalContext, info *models.TableInfo) (irAt
return nil, errors.Errorf("unrecognised operator: %v", a.Op)
}
func (a *astEqualityOp) evalItem(ctx *evalContext, item models.Item) (types.AttributeValue, error) {
func (a *astEqualityOp) evalItem(ctx *evalContext, item models.Item) (exprValue, error) {
left, err := a.Ref.evalItem(ctx, item)
if err != nil {
return nil, err
@ -76,28 +75,28 @@ func (a *astEqualityOp) evalItem(ctx *evalContext, item models.Item) (types.Attr
switch a.Op {
case "=":
cmp, isComparable := attrutils.CompareScalarAttributes(left, right)
cmp, isComparable := attrutils.CompareScalarAttributes(left.asAttributeValue(), right.asAttributeValue())
if !isComparable {
return nil, ValuesNotComparable{Left: left, Right: right}
return nil, ValuesNotComparable{Left: left.asAttributeValue(), Right: right.asAttributeValue()}
}
return &types.AttributeValueMemberBOOL{Value: cmp == 0}, nil
return boolExprValue(cmp == 0), nil
case "!=":
cmp, isComparable := attrutils.CompareScalarAttributes(left, right)
cmp, isComparable := attrutils.CompareScalarAttributes(left.asAttributeValue(), right.asAttributeValue())
if !isComparable {
return nil, ValuesNotComparable{Left: left, Right: right}
return nil, ValuesNotComparable{Left: left.asAttributeValue(), Right: right.asAttributeValue()}
}
return &types.AttributeValueMemberBOOL{Value: cmp != 0}, nil
return boolExprValue(cmp != 0), nil
case "^=":
strValue, isStrValue := right.(*types.AttributeValueMemberS)
strValue, isStrValue := right.(stringableExprValue)
if !isStrValue {
return nil, errors.New("operand '^=' must be string")
}
leftAsStr, canBeString := attrutils.AttributeToString(left)
leftAsStr, canBeString := left.(stringableExprValue)
if !canBeString {
return nil, ValueNotConvertableToString{Val: left}
return nil, ValueNotConvertableToString{Val: left.asAttributeValue()}
}
return &types.AttributeValueMemberBOOL{Value: strings.HasPrefix(leftAsStr, strValue.Value)}, nil
return boolExprValue(strings.HasPrefix(leftAsStr.asString(), strValue.asString())), nil
}
return nil, errors.Errorf("unrecognised operator: %v", a.Op)
@ -110,7 +109,7 @@ func (a *astEqualityOp) canModifyItem(ctx *evalContext, item models.Item) bool {
return a.Ref.canModifyItem(ctx, item)
}
func (a *astEqualityOp) setEvalItem(ctx *evalContext, item models.Item, value types.AttributeValue) error {
func (a *astEqualityOp) setEvalItem(ctx *evalContext, item models.Item, value exprValue) error {
if a.Op != "" {
return PathNotSettableError{}
}
@ -157,8 +156,8 @@ func (a irKeyFieldEq) calcQueryForScan(info *models.TableInfo) (expression.Condi
}
func (a irKeyFieldEq) calcQueryForQuery() (expression.KeyConditionBuilder, error) {
vb := a.value.goValue()
return expression.Key(a.name.keyName()).Equal(expression.Value(vb)), nil
vb := a.value.exprValue()
return expression.Key(a.name.keyName()).Equal(buildExpressionFromValue(vb)), nil
}
type irGenericEq struct {
@ -203,21 +202,21 @@ func (a irFieldBeginsWith) canBeExecutedAsQuery(qci *queryCalcInfo) bool {
func (a irFieldBeginsWith) calcQueryForScan(info *models.TableInfo) (expression.ConditionBuilder, error) {
nb := a.name.calcName(info)
vb := a.value.goValue()
strValue, isStrValue := vb.(string)
vb := a.value.exprValue()
strValue, isStrValue := vb.(stringableExprValue)
if !isStrValue {
return expression.ConditionBuilder{}, errors.New("operand '^=' must be string")
}
return nb.BeginsWith(strValue), nil
return nb.BeginsWith(strValue.asString()), nil
}
func (a irFieldBeginsWith) calcQueryForQuery() (expression.KeyConditionBuilder, error) {
vb := a.value.goValue()
strValue, isStrValue := vb.(string)
vb := a.value.exprValue()
strValue, isStrValue := vb.(stringableExprValue)
if !isStrValue {
return expression.KeyConditionBuilder{}, errors.New("operand '^=' must be string")
}
return expression.Key(a.name.keyName()).BeginsWith(strValue), nil
return expression.Key(a.name.keyName()).BeginsWith(strValue.asString()), nil
}

View file

@ -3,8 +3,8 @@ package queryexpr
import (
"fmt"
"github.com/aws/aws-sdk-go-v2/service/dynamodb/types"
"github.com/lmika/audax/internal/dynamo-browse/models/attrutils"
"github.com/lmika/audax/internal/dynamo-browse/models/itemrender"
"github.com/lmika/dynamo-browse/internal/dynamo-browse/models/attrutils"
"github.com/lmika/dynamo-browse/internal/dynamo-browse/models/itemrender"
"strings"
)
@ -69,6 +69,10 @@ type ValueNotConvertableToString struct {
func (n ValueNotConvertableToString) Error() string {
render := itemrender.ToRenderer(n.Val)
if render == nil {
return "nil value is not convertable to string"
}
return fmt.Sprintf("values '%v', type %v, is not convertable to string", render.StringValue(), render.TypeName())
}
@ -98,6 +102,14 @@ func (n InvalidTypeForIsError) Error() string {
return "invalid type for 'is': " + n.TypeName
}
type InvalidTypeForBetweenError struct {
TypeName string
}
func (n InvalidTypeForBetweenError) Error() string {
return "invalid type for 'between': " + n.TypeName
}
type InvalidArgumentNumberError struct {
Name string
Expected int
@ -108,6 +120,16 @@ func (e InvalidArgumentNumberError) Error() string {
return fmt.Sprintf("function '%v' expected %v args but received %v", e.Name, e.Expected, e.Actual)
}
type InvalidArgumentTypeError struct {
Name string
ArgIndex int
Expected string
}
func (e InvalidArgumentTypeError) Error() string {
return fmt.Sprintf("function '%v' expected arg %v to be of type %v", e.Name, e.ArgIndex, e.Expected)
}
type UnrecognisedFunctionError struct {
Name string
}
@ -137,3 +159,20 @@ type ValueNotUsableAsASubref struct {
func (e ValueNotUsableAsASubref) Error() string {
return "value cannot be used as a subref"
}
type MultiplePlansWithIndexError struct {
PossibleIndices []string
}
func (e MultiplePlansWithIndexError) Error() string {
return fmt.Sprintf("multiple plans with index found. Specify index or scan with 'using' clause: possible indices are %v", e.PossibleIndices)
}
type NoPlausiblePlanWithIndexError struct {
PreferredIndex string
PossibleIndices []string
}
func (e NoPlausiblePlanWithIndexError) Error() string {
return fmt.Sprintf("no plan with index '%v' found: possible indices are %v", e.PreferredIndex, e.PossibleIndices)
}

View file

@ -3,25 +3,32 @@ package queryexpr
import (
"bytes"
"encoding/gob"
"hash/fnv"
"io"
"github.com/aws/aws-sdk-go-v2/service/dynamodb/types"
"github.com/lmika/audax/internal/dynamo-browse/models"
"github.com/lmika/audax/internal/dynamo-browse/models/attrcodec"
"github.com/lmika/audax/internal/dynamo-browse/models/attrutils"
"github.com/lmika/dynamo-browse/internal/dynamo-browse/models"
"github.com/lmika/dynamo-browse/internal/dynamo-browse/models/attrcodec"
"github.com/lmika/dynamo-browse/internal/dynamo-browse/models/attrutils"
"github.com/pkg/errors"
"golang.org/x/exp/maps"
"golang.org/x/exp/slices"
"hash/fnv"
"io"
)
type QueryExpr struct {
ast *astExpr
names map[string]string
values map[string]types.AttributeValue
ast *astExpr
index string
names map[string]string
values map[string]types.AttributeValue
currentResultSet *models.ResultSet
// tests fields only
timeSource timeSource
}
type serializedExpr struct {
Expr string
Index string
Names map[string]string
Values []byte
}
@ -39,6 +46,7 @@ func DeserializeFrom(r io.Reader) (*QueryExpr, error) {
}
qe.names = se.Names
qe.index = se.Index
if len(se.Values) > 0 {
vals, err := attrcodec.NewDecoder(bytes.NewReader(se.Values)).Decode()
@ -56,7 +64,7 @@ func DeserializeFrom(r io.Reader) (*QueryExpr, error) {
}
func (md *QueryExpr) SerializeTo(w io.Writer) error {
se := serializedExpr{Expr: md.String(), Names: md.names}
se := serializedExpr{Expr: md.String(), Index: md.index, Names: md.names}
if md.values != nil {
var bts bytes.Buffer
if err := attrcodec.NewEncoder(&bts).Encode(&types.AttributeValueMemberM{Value: md.values}); err != nil {
@ -90,6 +98,7 @@ func (md *QueryExpr) Equal(other *QueryExpr) bool {
}
return md.ast.String() == other.ast.String() &&
md.index == other.index &&
maps.Equal(md.names, other.names) &&
maps.EqualFunc(md.values, md.values, attrutils.Equals)
}
@ -104,6 +113,7 @@ func (md *QueryExpr) HashCode() uint64 {
h := fnv.New64a()
h.Write([]byte(md.ast.String()))
h.Write([]byte(md.index))
// the names must be in sorted order to maintain consistant key ordering
if len(md.names) > 0 {
@ -133,9 +143,11 @@ func (md *QueryExpr) HashCode() uint64 {
func (md *QueryExpr) WithNameParams(value map[string]string) *QueryExpr {
return &QueryExpr{
ast: md.ast,
names: value,
values: md.values,
ast: md.ast,
index: md.index,
names: value,
values: md.values,
currentResultSet: md.currentResultSet,
}
}
@ -157,18 +169,47 @@ func (md *QueryExpr) ValueParamOrNil(name string) types.AttributeValue {
func (md *QueryExpr) WithValueParams(value map[string]types.AttributeValue) *QueryExpr {
return &QueryExpr{
ast: md.ast,
names: md.names,
values: value,
ast: md.ast,
index: md.index,
names: md.names,
values: value,
currentResultSet: md.currentResultSet,
}
}
func (md *QueryExpr) WithIndex(index string) *QueryExpr {
return &QueryExpr{
ast: md.ast,
index: index,
names: md.names,
values: md.values,
currentResultSet: md.currentResultSet,
}
}
func (md *QueryExpr) WithCurrentResultSet(currentResultSet *models.ResultSet) *QueryExpr {
return &QueryExpr{
ast: md.ast,
index: md.index,
names: md.names,
values: md.values,
currentResultSet: currentResultSet,
}
}
func (md *QueryExpr) Plan(tableInfo *models.TableInfo) (*models.QueryExecutionPlan, error) {
return md.ast.calcQuery(md.evalContext(), tableInfo)
return md.ast.calcQuery(md.evalContext(), tableInfo, md.index)
}
func (md *QueryExpr) EvalItem(item models.Item) (types.AttributeValue, error) {
return md.ast.evalItem(md.evalContext(), item)
val, err := md.ast.evalItem(md.evalContext(), item)
if err != nil {
return nil, err
}
if val == nil {
return nil, nil
}
return val.asAttributeValue(), nil
}
func (md *QueryExpr) DeleteAttribute(item models.Item) error {
@ -176,7 +217,11 @@ func (md *QueryExpr) DeleteAttribute(item models.Item) error {
}
func (md *QueryExpr) SetEvalItem(item models.Item, newValue types.AttributeValue) error {
return md.ast.setEvalItem(md.evalContext(), item, newValue)
val, err := newExprValueFromAttributeValue(newValue)
if err != nil {
return err
}
return md.ast.setEvalItem(md.evalContext(), item, val)
}
func (md *QueryExpr) IsModifiablePath(item models.Item) bool {
@ -187,6 +232,7 @@ func (md *QueryExpr) evalContext() *evalContext {
return &evalContext{
namePlaceholders: md.names,
valuePlaceholders: md.values,
ctxResultSet: md.currentResultSet,
}
}
@ -237,6 +283,8 @@ type evalContext struct {
nameLookup func(string) (string, bool)
valuePlaceholders map[string]types.AttributeValue
valueLookup func(string) (types.AttributeValue, bool)
timeSource timeSource
ctxResultSet *models.ResultSet
}
func (ec *evalContext) lookupName(name string) (string, bool) {
@ -264,3 +312,10 @@ func (ec *evalContext) lookupValue(name string) (types.AttributeValue, bool) {
return nil, false
}
func (ec *evalContext) getTimeSource() timeSource {
if ts := ec.timeSource; ts != nil {
return ts
}
return defaultTimeSource{}
}

View file

@ -3,12 +3,14 @@ package queryexpr_test
import (
"bytes"
"fmt"
"testing"
"time"
"github.com/aws/aws-sdk-go-v2/aws"
"github.com/aws/aws-sdk-go-v2/service/dynamodb/types"
"github.com/lmika/audax/internal/dynamo-browse/models/queryexpr"
"testing"
"github.com/lmika/dynamo-browse/internal/dynamo-browse/models/queryexpr"
"github.com/lmika/audax/internal/dynamo-browse/models"
"github.com/lmika/dynamo-browse/internal/dynamo-browse/models"
"github.com/stretchr/testify/assert"
)
@ -34,6 +36,13 @@ func TestModExpr_Query(t *testing.T) {
SortKey: "sk",
},
},
{
Name: "with-apples-and-oranges",
Keys: models.KeyAttribute{
PartitionKey: "apples",
SortKey: "oranges",
},
},
},
}
@ -44,6 +53,11 @@ func TestModExpr_Query(t *testing.T) {
`#0 = :0`,
exprNameIsString(0, 0, "pk", "prefix"),
),
//scanCase("when request pk is fixed (reverse)",
// `prefix="pk"`,
// `#0 = :0`,
// exprNameIsString(0, 0, "pk", "prefix"),
//),
scanCase("when request pk is fixed in parens #1",
`(pk="prefix")`,
`#0 = :0`,
@ -112,6 +126,13 @@ func TestModExpr_Query(t *testing.T) {
exprNameIsString(0, 0, "pk", "prefix"),
exprNameIsNumber(1, 1, "sk", "100"),
),
scanCase("when request pk is equals and sk is greater or equal to",
`pk="prefix" and sk between 100 and 200`,
`(#0 = :0) AND (#1 BETWEEN :1 AND :2)`,
exprNameIsString(0, 0, "pk", "prefix"),
exprNameIsNumber(1, 1, "sk", "100"),
exprValueIsNumber(2, "200"),
),
scanCase("with placeholders",
`:partition=$valuePrefix and :sort=$valueAnother`,
@ -149,6 +170,13 @@ func TestModExpr_Query(t *testing.T) {
exprNameIsString(0, 0, "color", "yellow"),
exprNameIsString(1, 1, "shade", "dark"),
),
// Function calls
scanCase("use the value of fn call in query",
`pk = _x_concat("Hello ", "world")`,
`#0 = :0`,
exprNameIsString(0, 0, "pk", "Hello world"),
),
}
for _, scenario := range scenarios {
@ -238,6 +266,13 @@ func TestModExpr_Query(t *testing.T) {
exprNameIsString(0, 0, "pk", "prefix"),
),
scanCase("with between", `pk between "a" and "z"`,
`#0 BETWEEN :0 AND :1`,
exprName(0, "pk"),
exprValueIsString(0, "a"),
exprValueIsString(1, "z"),
),
scanCase("with in", `pk in ("alpha", "bravo", "charlie")`,
`#0 IN (:0, :1, :2)`,
exprName(0, "pk"),
@ -374,6 +409,53 @@ func TestModExpr_Query(t *testing.T) {
})
}
})
t.Run("with index clash", func(t *testing.T) {
t.Run("should return error if attempt to run query with two indices that can be chosen", func(t *testing.T) {
modExpr, err := queryexpr.Parse(`apples="this"`)
assert.NoError(t, err)
_, err = modExpr.Plan(tableInfo)
assert.Error(t, err)
})
t.Run("should run as scan if explicitly forced to", func(t *testing.T) {
modExpr, err := queryexpr.Parse(`apples="this" using scan`)
assert.NoError(t, err)
plan, err := modExpr.Plan(tableInfo)
assert.NoError(t, err)
assert.False(t, plan.CanQuery)
})
t.Run("should run as query with the 'with-apples' index", func(t *testing.T) {
modExpr, err := queryexpr.Parse(`apples="this" using index("with-apples")`)
assert.NoError(t, err)
plan, err := modExpr.Plan(tableInfo)
assert.NoError(t, err)
assert.True(t, plan.CanQuery)
assert.Equal(t, "with-apples", plan.IndexName)
})
t.Run("should run as query with the 'with-apples-and-oranges' index", func(t *testing.T) {
modExpr, err := queryexpr.Parse(`apples="this" using index("with-apples-and-oranges")`)
assert.NoError(t, err)
plan, err := modExpr.Plan(tableInfo)
assert.NoError(t, err)
assert.True(t, plan.CanQuery)
assert.Equal(t, "with-apples-and-oranges", plan.IndexName)
})
t.Run("should return error if the chosen index can't be used", func(t *testing.T) {
modExpr, err := queryexpr.Parse(`apples="this" using index("with-missing")`)
assert.NoError(t, err)
_, err = modExpr.Plan(tableInfo)
assert.Error(t, err)
})
})
}
func TestQueryExpr_EvalItem(t *testing.T) {
@ -395,7 +477,9 @@ func TestQueryExpr_EvalItem(t *testing.T) {
&types.AttributeValueMemberN{Value: "7"},
},
},
"one": &types.AttributeValueMemberN{Value: "1"},
"three": &types.AttributeValueMemberN{Value: "3"},
"five": &types.AttributeValueMemberN{Value: "5"},
}
)
@ -433,6 +517,20 @@ func TestQueryExpr_EvalItem(t *testing.T) {
{expr: "three < 2", expected: &types.AttributeValueMemberBOOL{Value: false}},
{expr: "three <= 2", expected: &types.AttributeValueMemberBOOL{Value: false}},
// Between
{expr: "3 between 1 and 5", expected: &types.AttributeValueMemberBOOL{Value: true}},
{expr: "three between 1 and 5", expected: &types.AttributeValueMemberBOOL{Value: true}},
{expr: "three between one and five", expected: &types.AttributeValueMemberBOOL{Value: true}},
{expr: "three between 10 and 15", expected: &types.AttributeValueMemberBOOL{Value: false}},
{expr: "three between 1 and 2", expected: &types.AttributeValueMemberBOOL{Value: false}},
{expr: "8 between five and 10", expected: &types.AttributeValueMemberBOOL{Value: true}},
{expr: "three between 1 and 3", expected: &types.AttributeValueMemberBOOL{Value: true}},
{expr: "three between 3 and 5", expected: &types.AttributeValueMemberBOOL{Value: true}},
{expr: `"e" between "a" and "z"`, expected: &types.AttributeValueMemberBOOL{Value: true}},
{expr: `"eee" between "aaa" and "zzz"`, expected: &types.AttributeValueMemberBOOL{Value: true}},
{expr: `"e" between "between" and "beyond"`, expected: &types.AttributeValueMemberBOOL{Value: false}},
// In
{expr: "three in (2, 3, 4, 5)", expected: &types.AttributeValueMemberBOOL{Value: true}},
{expr: "three in (20, 30, 40)", expected: &types.AttributeValueMemberBOOL{Value: false}},
@ -509,6 +607,50 @@ func TestQueryExpr_EvalItem(t *testing.T) {
}
})
t.Run("functions", func(t *testing.T) {
timeNow := time.Now()
contextResultSet := models.ResultSet{}
contextResultSet.SetItems([]models.Item{
{"pk": &types.AttributeValueMemberS{Value: "1"}, "num": &types.AttributeValueMemberN{Value: "1"}},
{"pk": &types.AttributeValueMemberS{Value: "2"}, "num": &types.AttributeValueMemberN{Value: "2"}},
{"pk": &types.AttributeValueMemberS{Value: "3"}, "num": &types.AttributeValueMemberN{Value: "3"}},
{"pk": &types.AttributeValueMemberS{Value: "4"}, "num": &types.AttributeValueMemberN{Value: "4"}},
})
contextResultSet.SetMark(0, true)
contextResultSet.SetMark(1, true)
scenarios := []struct {
expr string
expected types.AttributeValue
}{
// _x_now() -- unreleased version of now
{expr: `_x_now()`, expected: &types.AttributeValueMemberN{Value: fmt.Sprint(timeNow.Unix())}},
// Marked
{expr: `marked("num")`, expected: &types.AttributeValueMemberL{Value: []types.AttributeValue{
&types.AttributeValueMemberN{Value: "1"},
&types.AttributeValueMemberN{Value: "2"},
}}},
{expr: `one in marked("num")`, expected: &types.AttributeValueMemberBOOL{Value: true}},
{expr: `three in marked("num")`, expected: &types.AttributeValueMemberBOOL{Value: false}},
}
for _, scenario := range scenarios {
t.Run(scenario.expr, func(t *testing.T) {
modExpr, err := queryexpr.Parse(scenario.expr)
assert.NoError(t, err)
res, err := modExpr.
WithTestTimeSource(timeNow).
WithCurrentResultSet(&contextResultSet).
EvalItem(item)
assert.NoError(t, err)
assert.Equal(t, scenario.expected, res)
})
}
})
t.Run("unparsed expression", func(t *testing.T) {
scenarios := []struct {
expr string
@ -532,6 +674,10 @@ func TestQueryExpr_EvalItem(t *testing.T) {
}{
{expr: `alpha.bravo`, expectedError: queryexpr.ValueNotAMapError([]string{"alpha", "bravo"})},
{expr: `charlie.tree.bla`, expectedError: queryexpr.ValueNotAMapError([]string{"charlie", "tree", "bla"})},
{expr: `missing="no"`, expectedError: queryexpr.ValuesNotComparable{Right: &types.AttributeValueMemberS{Value: "no"}}},
{expr: `missing!="no"`, expectedError: queryexpr.ValuesNotComparable{Right: &types.AttributeValueMemberS{Value: "no"}}},
{expr: `missing^="no"`, expectedError: queryexpr.ValueNotConvertableToString{nil}},
}
for _, scenario := range scenarios {

View file

@ -0,0 +1,15 @@
package queryexpr
import (
"github.com/aws/aws-sdk-go-v2/service/dynamodb/types"
"github.com/lmika/dynamo-browse/internal/dynamo-browse/models"
)
type ExprFieldValueEvaluator struct {
Expr *QueryExpr
}
func (sfve ExprFieldValueEvaluator) EvaluateForItem(item models.Item) types.AttributeValue {
val, _ := sfve.Expr.EvalItem(item)
return val
}

View file

@ -2,12 +2,12 @@ package queryexpr
import (
"context"
"github.com/aws/aws-sdk-go-v2/feature/dynamodb/expression"
"github.com/aws/aws-sdk-go-v2/service/dynamodb/types"
"github.com/lmika/audax/internal/common/sliceutils"
"github.com/lmika/audax/internal/dynamo-browse/models"
"github.com/pkg/errors"
"strings"
"github.com/aws/aws-sdk-go-v2/feature/dynamodb/expression"
"github.com/lmika/dynamo-browse/internal/common/sliceutils"
"github.com/lmika/dynamo-browse/internal/dynamo-browse/models"
"github.com/pkg/errors"
)
func (a *astFunctionCall) evalToIR(ctx *evalContext, info *models.TableInfo) (irAtom, error) {
@ -29,7 +29,7 @@ func (a *astFunctionCall) evalToIR(ctx *evalContext, info *models.TableInfo) (ir
return nil, err
}
// TODO: do this properly
// Special handling of functions that have IR nodes
switch nameIr.keyName() {
case "size":
if len(irNodes) != 1 {
@ -40,20 +40,34 @@ func (a *astFunctionCall) evalToIR(ctx *evalContext, info *models.TableInfo) (ir
return nil, OperandNotANameError(a.Args[0].String())
}
return irSizeFn{name}, nil
case "range":
if len(irNodes) != 2 {
return nil, InvalidArgumentNumberError{Name: "range", Expected: 2, Actual: len(irNodes)}
}
// TEMP
fromVal := irNodes[0].(valueIRAtom).goValue().(int64)
toVal := irNodes[1].(valueIRAtom).goValue().(int64)
return irRangeFn{fromVal, toVal}, nil
}
return nil, UnrecognisedFunctionError{Name: nameIr.keyName()}
builtinFn, hasBuiltin := nativeFuncs[nameIr.keyName()]
if !hasBuiltin {
return nil, UnrecognisedFunctionError{Name: nameIr.keyName()}
}
// Normal functions which are evaluated to regular values
irValues, err := sliceutils.MapWithError(irNodes, func(a irAtom) (exprValue, error) {
v, isV := a.(valueIRAtom)
if !isV {
return nil, errors.New("cannot use value")
}
return v.exprValue(), nil
})
if err != nil {
return nil, err
}
val, err := builtinFn(context.Background(), irValues)
if err != nil {
return nil, err
}
return irValue{value: val}, nil
}
func (a *astFunctionCall) evalItem(ctx *evalContext, item models.Item) (types.AttributeValue, error) {
func (a *astFunctionCall) evalItem(ctx *evalContext, item models.Item) (exprValue, error) {
if !a.IsCall {
return a.Caller.evalItem(ctx, item)
}
@ -67,14 +81,16 @@ func (a *astFunctionCall) evalItem(ctx *evalContext, item models.Item) (types.At
return nil, UnrecognisedFunctionError{Name: name}
}
args, err := sliceutils.MapWithError(a.Args, func(a *astExpr) (types.AttributeValue, error) {
args, err := sliceutils.MapWithError(a.Args, func(a *astExpr) (exprValue, error) {
return a.evalItem(ctx, item)
})
if err != nil {
return nil, err
}
return fn(context.Background(), args)
cCtx := context.WithValue(context.Background(), timeSourceContextKey, ctx.timeSource)
cCtx = context.WithValue(cCtx, currentResultSetContextKey, ctx.ctxResultSet)
return fn(cCtx, args)
}
func (a *astFunctionCall) canModifyItem(ctx *evalContext, item models.Item) bool {
@ -85,7 +101,7 @@ func (a *astFunctionCall) canModifyItem(ctx *evalContext, item models.Item) bool
return a.Caller.canModifyItem(ctx, item)
}
func (a *astFunctionCall) setEvalItem(ctx *evalContext, item models.Item, value types.AttributeValue) error {
func (a *astFunctionCall) setEvalItem(ctx *evalContext, item models.Item, value exprValue) error {
// TODO: Should a function vall return an item?
if a.IsCall {
return PathNotSettableError{}
@ -147,3 +163,15 @@ func (i irRangeFn) calcGoValues(info *models.TableInfo) ([]any, error) {
}
return xs, nil
}
type multiValueFnResult struct {
items []any
}
func (i multiValueFnResult) calcQueryForScan(info *models.TableInfo) (expression.ConditionBuilder, error) {
return expression.ConditionBuilder{}, errors.New("cannot run as scan")
}
func (i multiValueFnResult) calcGoValues(info *models.TableInfo) ([]any, error) {
return i.items, nil
}

View file

@ -0,0 +1,17 @@
package queryexpr
import (
"time"
)
type testTimeSource time.Time
func (tds testTimeSource) now() time.Time {
return time.Time(tds)
}
func (a *QueryExpr) WithTestTimeSource(timeNow time.Time) *QueryExpr {
a.timeSource = testTimeSource(timeNow)
return a
}

View file

@ -1,13 +1,10 @@
package queryexpr
import (
"bytes"
"fmt"
"github.com/aws/aws-sdk-go-v2/feature/dynamodb/expression"
"github.com/aws/aws-sdk-go-v2/service/dynamodb/types"
"github.com/lmika/audax/internal/common/sliceutils"
"github.com/lmika/audax/internal/dynamo-browse/models"
"github.com/lmika/audax/internal/dynamo-browse/models/attrutils"
"github.com/lmika/dynamo-browse/internal/common/sliceutils"
"github.com/lmika/dynamo-browse/internal/dynamo-browse/models"
"github.com/lmika/dynamo-browse/internal/dynamo-browse/models/attrutils"
"github.com/pkg/errors"
"strings"
)
@ -71,6 +68,13 @@ func (a *astIn) evalToIR(ctx *evalContext, info *models.TableInfo) (irAtom, erro
return nil, OperandNotANameError(a.Ref.String())
}
ir = irContains{needle: lit, haystack: t}
case valueIRAtom:
nameIR, isNameIR := leftIR.(irNamePath)
if !isNameIR {
return nil, OperandNotANameError(a.Ref.String())
}
ir = irLiteralValues{name: nameIR, values: t}
case oprIRAtom:
nameIR, isNameIR := leftIR.(irNamePath)
if !isNameIR {
@ -78,13 +82,6 @@ func (a *astIn) evalToIR(ctx *evalContext, info *models.TableInfo) (irAtom, erro
}
ir = irIn{name: nameIR, values: []oprIRAtom{t}}
case multiValueIRAtom:
nameIR, isNameIR := leftIR.(irNamePath)
if !isNameIR {
return nil, OperandNotANameError(a.Ref.String())
}
ir = irLiteralValues{name: nameIR, values: t}
default:
return nil, OperandNotAnOperandError{}
}
@ -96,7 +93,7 @@ func (a *astIn) evalToIR(ctx *evalContext, info *models.TableInfo) (irAtom, erro
return ir, nil
}
func (a *astIn) evalItem(ctx *evalContext, item models.Item) (types.AttributeValue, error) {
func (a *astIn) evalItem(ctx *evalContext, item models.Item) (exprValue, error) {
val, err := a.Ref.evalItem(ctx, item)
if err != nil {
return nil, err
@ -112,14 +109,15 @@ func (a *astIn) evalItem(ctx *evalContext, item models.Item) (types.AttributeVal
if err != nil {
return nil, err
}
cmp, isComparable := attrutils.CompareScalarAttributes(val, evalOp)
// TODO: use native types here
cmp, isComparable := attrutils.CompareScalarAttributes(val.asAttributeValue(), evalOp.asAttributeValue())
if !isComparable {
continue
} else if cmp == 0 {
return &types.AttributeValueMemberBOOL{Value: true}, nil
return boolExprValue(true), nil
}
}
return &types.AttributeValueMemberBOOL{Value: false}, nil
return boolExprValue(false), nil
case a.SingleOperand != nil:
evalOp, err := a.SingleOperand.evalItem(ctx, item)
if err != nil {
@ -127,69 +125,38 @@ func (a *astIn) evalItem(ctx *evalContext, item models.Item) (types.AttributeVal
}
switch t := evalOp.(type) {
case *types.AttributeValueMemberS:
str, canToStr := attrutils.AttributeToString(val)
case stringableExprValue:
str, canToStr := val.(stringableExprValue)
if !canToStr {
return &types.AttributeValueMemberBOOL{Value: false}, nil
return boolExprValue(false), nil
}
return &types.AttributeValueMemberBOOL{Value: strings.Contains(t.Value, str)}, nil
case *types.AttributeValueMemberL:
for _, listItem := range t.Value {
cmp, isComparable := attrutils.CompareScalarAttributes(val, listItem)
return boolExprValue(strings.Contains(t.asString(), str.asString())), nil
case slicableExprValue:
for i := 0; i < t.len(); i++ {
va, err := t.valueAt(i)
if err != nil {
return nil, err
}
// TODO: use expr value types here
cmp, isComparable := attrutils.CompareScalarAttributes(val.asAttributeValue(), va.asAttributeValue())
if !isComparable {
continue
} else if cmp == 0 {
return &types.AttributeValueMemberBOOL{Value: true}, nil
return boolExprValue(true), nil
}
}
return &types.AttributeValueMemberBOOL{Value: false}, nil
case *types.AttributeValueMemberSS:
str, canToStr := attrutils.AttributeToString(val)
return boolExprValue(false), nil
case mappableExprValue:
str, canToStr := val.(stringableExprValue)
if !canToStr {
return &types.AttributeValueMemberBOOL{Value: false}, nil
return boolExprValue(false), nil
}
for _, listItem := range t.Value {
if str != listItem {
return &types.AttributeValueMemberBOOL{Value: false}, nil
}
}
return &types.AttributeValueMemberBOOL{Value: true}, nil
case *types.AttributeValueMemberBS:
b, isB := val.(*types.AttributeValueMemberB)
if !isB {
return &types.AttributeValueMemberBOOL{Value: false}, nil
}
for _, listItem := range t.Value {
if !bytes.Equal(b.Value, listItem) {
return &types.AttributeValueMemberBOOL{Value: false}, nil
}
}
return &types.AttributeValueMemberBOOL{Value: true}, nil
case *types.AttributeValueMemberNS:
n, isN := val.(*types.AttributeValueMemberN)
if !isN {
return &types.AttributeValueMemberBOOL{Value: false}, nil
}
for _, listItem := range t.Value {
// TODO: this is not actually right
if n.Value != listItem {
return &types.AttributeValueMemberBOOL{Value: false}, nil
}
}
return &types.AttributeValueMemberBOOL{Value: true}, nil
case *types.AttributeValueMemberM:
str, canToStr := attrutils.AttributeToString(val)
if !canToStr {
return &types.AttributeValueMemberBOOL{Value: false}, nil
}
_, hasItem := t.Value[str]
return &types.AttributeValueMemberBOOL{Value: hasItem}, nil
hasKey := t.hasKey(str.asString())
return boolExprValue(hasKey), nil
}
return nil, ValuesNotInnableError{Val: evalOp}
return nil, ValuesNotInnableError{Val: evalOp.asAttributeValue()}
}
return nil, errors.New("internal error: unhandled 'in' case")
}
@ -201,7 +168,7 @@ func (a *astIn) canModifyItem(ctx *evalContext, item models.Item) bool {
return a.Ref.canModifyItem(ctx, item)
}
func (a *astIn) setEvalItem(ctx *evalContext, item models.Item, value types.AttributeValue) error {
func (a *astIn) setEvalItem(ctx *evalContext, item models.Item, value exprValue) error {
if len(a.Operand) != 0 || a.SingleOperand != nil {
return PathNotSettableError{}
}
@ -263,19 +230,38 @@ func (i irIn) calcQueryForScan(info *models.TableInfo) (expression.ConditionBuil
type irLiteralValues struct {
name nameIRAtom
values multiValueIRAtom
values valueIRAtom
}
func (i irLiteralValues) calcQueryForScan(info *models.TableInfo) (expression.ConditionBuilder, error) {
vals, err := i.values.calcGoValues(info)
if err != nil {
return expression.ConditionBuilder{}, err
func (iv irLiteralValues) calcQueryForScan(info *models.TableInfo) (expression.ConditionBuilder, error) {
if sliceable, isSliceable := iv.values.exprValue().(slicableExprValue); isSliceable {
if sliceable.len() == 1 {
va, err := sliceable.valueAt(0)
if err != nil {
return expression.ConditionBuilder{}, err
}
return iv.name.calcName(info).In(buildExpressionFromValue(va)), nil
} else if sliceable.len() == 0 {
// name is not in an empty slice, so this branch always evaluates to false
// TODO: would be better to not even include this branch in some way?
return expression.Equal(expression.Value(false), expression.Value(true)), nil
}
items := make([]expression.OperandBuilder, sliceable.len())
for i := 0; i < sliceable.len(); i++ {
va, err := sliceable.valueAt(i)
if err != nil {
return expression.ConditionBuilder{}, err
}
items[i] = buildExpressionFromValue(va)
}
return iv.name.calcName(info).In(items[0], items[1:]...), nil
}
oprValues := sliceutils.Map(vals, func(t any) expression.OperandBuilder {
return expression.Value(t)
})
return i.name.calcName(info).In(oprValues[0], oprValues[1:]...), nil
return iv.name.calcName(info).In(buildExpressionFromValue(iv.values.exprValue())), nil
}
type irContains struct {
@ -284,8 +270,11 @@ type irContains struct {
}
func (i irContains) calcQueryForScan(info *models.TableInfo) (expression.ConditionBuilder, error) {
needle := i.needle.goValue()
haystack := i.haystack.calcName(info)
strNeedle, isString := i.needle.exprValue().(stringableExprValue)
if !isString {
return expression.ConditionBuilder{}, errors.New("value cannot be converted to string")
}
return haystack.Contains(fmt.Sprint(needle)), nil
haystack := i.haystack.calcName(info)
return haystack.Contains(strNeedle.asString()), nil
}

View file

@ -2,7 +2,7 @@ package queryexpr
import (
"github.com/aws/aws-sdk-go-v2/feature/dynamodb/expression"
"github.com/lmika/audax/internal/dynamo-browse/models"
"github.com/lmika/dynamo-browse/internal/dynamo-browse/models"
)
// TO DELETE = operandFieldName() string
@ -36,11 +36,7 @@ type nameIRAtom interface {
type valueIRAtom interface {
oprIRAtom
goValue() any
}
type multiValueIRAtom interface {
calcGoValues(info *models.TableInfo) ([]any, error)
exprValue() exprValue
}
func canExecuteAsQuery(ir irAtom, qci *queryCalcInfo) bool {

View file

@ -2,9 +2,7 @@ package queryexpr
import (
"github.com/aws/aws-sdk-go-v2/feature/dynamodb/expression"
"github.com/aws/aws-sdk-go-v2/service/dynamodb/types"
"github.com/lmika/audax/internal/dynamo-browse/models"
"github.com/lmika/audax/internal/dynamo-browse/models/attrutils"
"github.com/lmika/dynamo-browse/internal/dynamo-browse/models"
"reflect"
"strings"
)
@ -12,50 +10,50 @@ import (
type isTypeInfo struct {
isAny bool
attributeType expression.DynamoDBAttributeType
goType reflect.Type
goTypes []reflect.Type
}
var validIsTypeNames = map[string]isTypeInfo{
"ANY": {isAny: true},
"B": {
attributeType: expression.Binary,
goType: reflect.TypeOf(&types.AttributeValueMemberB{}),
// TODO
},
"BOOL": {
attributeType: expression.Boolean,
goType: reflect.TypeOf(&types.AttributeValueMemberBOOL{}),
goTypes: []reflect.Type{reflect.TypeOf(boolExprValue(false))},
},
"S": {
attributeType: expression.String,
goType: reflect.TypeOf(&types.AttributeValueMemberS{}),
goTypes: []reflect.Type{reflect.TypeOf(stringExprValue(""))},
},
"N": {
attributeType: expression.Number,
goType: reflect.TypeOf(&types.AttributeValueMemberN{}),
goTypes: []reflect.Type{reflect.TypeOf(int64ExprValue(0)), reflect.TypeOf(bigNumExprValue{})},
},
"NULL": {
attributeType: expression.Null,
goType: reflect.TypeOf(&types.AttributeValueMemberNULL{}),
goTypes: []reflect.Type{reflect.TypeOf(nullExprValue{})},
},
"L": {
attributeType: expression.List,
goType: reflect.TypeOf(&types.AttributeValueMemberL{}),
goTypes: []reflect.Type{reflect.TypeOf(listExprValue{}), reflect.TypeOf(listProxyValue{})},
},
"M": {
attributeType: expression.Map,
goType: reflect.TypeOf(&types.AttributeValueMemberM{}),
goTypes: []reflect.Type{reflect.TypeOf(mapExprValue{}), reflect.TypeOf(mapProxyValue{})},
},
"BS": {
attributeType: expression.BinarySet,
goType: reflect.TypeOf(&types.AttributeValueMemberBS{}),
// TODO
},
"NS": {
attributeType: expression.NumberSet,
goType: reflect.TypeOf(&types.AttributeValueMemberNS{}),
// TODO
},
"SS": {
attributeType: expression.StringSet,
goType: reflect.TypeOf(&types.AttributeValueMemberSS{}),
// TODO
},
}
@ -83,14 +81,14 @@ func (a *astIsOp) evalToIR(ctx *evalContext, info *models.TableInfo) (irAtom, er
if !isValueIR {
return nil, ValueMustBeLiteralError{}
}
strValue, isStringValue := valueIR.goValue().(string)
strValue, isStringValue := valueIR.exprValue().(stringableExprValue)
if !isStringValue {
return nil, ValueMustBeStringError{}
}
typeInfo, isValidType := validIsTypeNames[strings.ToUpper(strValue)]
typeInfo, isValidType := validIsTypeNames[strings.ToUpper(strValue.asString())]
if !isValidType {
return nil, InvalidTypeForIsError{TypeName: strValue}
return nil, InvalidTypeForIsError{TypeName: strValue.asString()}
}
var ir = irIs{name: nameIR, typeInfo: typeInfo}
@ -104,7 +102,7 @@ func (a *astIsOp) evalToIR(ctx *evalContext, info *models.TableInfo) (irAtom, er
return ir, nil
}
func (a *astIsOp) evalItem(ctx *evalContext, item models.Item) (types.AttributeValue, error) {
func (a *astIsOp) evalItem(ctx *evalContext, item models.Item) (exprValue, error) {
ref, err := a.Ref.evalItem(ctx, item)
if err != nil {
return nil, err
@ -118,26 +116,32 @@ func (a *astIsOp) evalItem(ctx *evalContext, item models.Item) (types.AttributeV
if err != nil {
return nil, err
}
str, canToStr := attrutils.AttributeToString(expTypeVal)
str, canToStr := expTypeVal.(stringableExprValue)
if !canToStr {
return nil, ValueMustBeStringError{}
}
typeInfo, hasTypeInfo := validIsTypeNames[strings.ToUpper(str)]
typeInfo, hasTypeInfo := validIsTypeNames[strings.ToUpper(str.asString())]
if !hasTypeInfo {
return nil, InvalidTypeForIsError{TypeName: str}
return nil, InvalidTypeForIsError{TypeName: str.asString()}
}
var resultOfIs bool
if typeInfo.isAny {
resultOfIs = ref != nil
resultOfIs = ref != undefinedExprValue{}
} else {
refType := reflect.TypeOf(ref)
resultOfIs = typeInfo.goType.AssignableTo(refType)
for _, t := range typeInfo.goTypes {
if t.AssignableTo(refType) {
resultOfIs = true
break
}
}
}
if a.HasNot {
resultOfIs = !resultOfIs
}
return &types.AttributeValueMemberBOOL{Value: resultOfIs}, nil
return boolExprValue(resultOfIs), nil
}
func (a *astIsOp) canModifyItem(ctx *evalContext, item models.Item) bool {
@ -147,7 +151,7 @@ func (a *astIsOp) canModifyItem(ctx *evalContext, item models.Item) bool {
return a.Ref.canModifyItem(ctx, item)
}
func (a *astIsOp) setEvalItem(ctx *evalContext, item models.Item, value types.AttributeValue) error {
func (a *astIsOp) setEvalItem(ctx *evalContext, item models.Item, value exprValue) error {
if a.Value != nil {
return PathNotSettableError{}
}

View file

@ -1,8 +1,7 @@
package queryexpr
import (
"github.com/aws/aws-sdk-go-v2/service/dynamodb/types"
"github.com/lmika/audax/internal/dynamo-browse/models"
"github.com/lmika/dynamo-browse/internal/dynamo-browse/models"
"github.com/pkg/errors"
)
@ -21,7 +20,12 @@ func (p *astPlaceholder) evalToIR(ctx *evalContext, info *models.TableInfo) (irA
return nil, MissingPlaceholderError{Placeholder: p.Placeholder}
}
return irValue{value: val}, nil
ev, err := newExprValueFromAttributeValue(val)
if err != nil {
return nil, err
}
return irValue{value: ev}, nil
} else if placeholderType == namePlaceholderPrefix {
name, hasName := ctx.lookupName(placeholder)
if !hasName {
@ -34,7 +38,7 @@ func (p *astPlaceholder) evalToIR(ctx *evalContext, info *models.TableInfo) (irA
return nil, errors.New("unrecognised placeholder")
}
func (p *astPlaceholder) evalItem(ctx *evalContext, item models.Item) (types.AttributeValue, error) {
func (p *astPlaceholder) evalItem(ctx *evalContext, item models.Item) (exprValue, error) {
placeholderType := p.Placeholder[0]
placeholder := p.Placeholder[1:]
@ -43,7 +47,7 @@ func (p *astPlaceholder) evalItem(ctx *evalContext, item models.Item) (types.Att
if !hasVal {
return nil, MissingPlaceholderError{Placeholder: p.Placeholder}
}
return val, nil
return newExprValueFromAttributeValue(val)
} else if placeholderType == namePlaceholderPrefix {
name, hasName := ctx.lookupName(placeholder)
if !hasName {
@ -55,7 +59,7 @@ func (p *astPlaceholder) evalItem(ctx *evalContext, item models.Item) (types.Att
return nil, nil
}
return res, nil
return newExprValueFromAttributeValue(res)
}
return nil, errors.New("unrecognised placeholder")
@ -66,7 +70,7 @@ func (p *astPlaceholder) canModifyItem(ctx *evalContext, item models.Item) bool
return placeholderType == namePlaceholderPrefix
}
func (p *astPlaceholder) setEvalItem(ctx *evalContext, item models.Item, value types.AttributeValue) error {
func (p *astPlaceholder) setEvalItem(ctx *evalContext, item models.Item, value exprValue) error {
placeholderType := p.Placeholder[0]
placeholder := p.Placeholder[1:]
@ -78,7 +82,7 @@ func (p *astPlaceholder) setEvalItem(ctx *evalContext, item models.Item, value t
return MissingPlaceholderError{Placeholder: p.Placeholder}
}
item[name] = value
item[name] = value.asAttributeValue()
return nil
}

View file

@ -1,10 +1,8 @@
package queryexpr
import (
"github.com/aws/aws-sdk-go-v2/service/dynamodb/types"
"github.com/lmika/audax/internal/common/sliceutils"
"github.com/lmika/audax/internal/dynamo-browse/models"
"strconv"
"github.com/lmika/dynamo-browse/internal/common/sliceutils"
"github.com/lmika/dynamo-browse/internal/dynamo-browse/models"
"strings"
)
@ -34,7 +32,7 @@ func (r *astSubRef) evalToIR(ctx *evalContext, info *models.TableInfo) (irAtom,
return irNamePath{name: namePath.name, quals: quals}, nil
}
func (r *astSubRef) evalItem(ctx *evalContext, item models.Item) (types.AttributeValue, error) {
func (r *astSubRef) evalItem(ctx *evalContext, item models.Item) (exprValue, error) {
res, err := r.Ref.evalItem(ctx, item)
if err != nil {
return nil, err
@ -48,7 +46,7 @@ func (r *astSubRef) evalItem(ctx *evalContext, item models.Item) (types.Attribut
return res, nil
}
func (r *astSubRef) evalSubRefs(ctx *evalContext, item models.Item, res types.AttributeValue, subRefs []*astSubRefType) (types.AttributeValue, error) {
func (r *astSubRef) evalSubRefs(ctx *evalContext, item models.Item, res exprValue, subRefs []*astSubRefType) (exprValue, error) {
for i, sr := range subRefs {
sv, err := sr.evalToStrOrInt(ctx, nil)
if err != nil {
@ -57,24 +55,30 @@ func (r *astSubRef) evalSubRefs(ctx *evalContext, item models.Item, res types.At
switch val := sv.(type) {
case string:
var hasV bool
mapRes, isMapRes := res.(*types.AttributeValueMemberM)
mapRes, isMapRes := res.(mappableExprValue)
if !isMapRes {
return nil, newValueNotAMapError(r, subRefs[:i+1])
}
res, hasV = mapRes.Value[val]
if !hasV {
return nil, nil
if mapRes.hasKey(val) {
res, err = mapRes.valueOf(val)
if err != nil {
return nil, err
}
} else {
res = nil
}
case int:
listRes, isMapRes := res.(*types.AttributeValueMemberL)
case int64:
listRes, isMapRes := res.(slicableExprValue)
if !isMapRes {
return nil, newValueNotAListError(r, subRefs[:i+1])
}
// TODO - deal with index properly
res = listRes.Value[val]
// TODO - deal with index properly (i.e. error handling)
res, err = listRes.valueAt(int(val))
if err != nil {
return nil, err
}
}
}
return res, nil
@ -84,7 +88,7 @@ func (r *astSubRef) canModifyItem(ctx *evalContext, item models.Item) bool {
return r.Ref.canModifyItem(ctx, item)
}
func (r *astSubRef) setEvalItem(ctx *evalContext, item models.Item, value types.AttributeValue) error {
func (r *astSubRef) setEvalItem(ctx *evalContext, item models.Item, value exprValue) error {
if len(r.SubRefs) == 0 {
return r.Ref.setEvalItem(ctx, item, value)
}
@ -108,20 +112,19 @@ func (r *astSubRef) setEvalItem(ctx *evalContext, item models.Item, value types.
switch val := sv.(type) {
case string:
mapRes, isMapRes := parentItem.(*types.AttributeValueMemberM)
mapRes, isMapRes := parentItem.(modifiableMapExprValue)
if !isMapRes {
return newValueNotAMapError(r, r.SubRefs)
}
mapRes.Value[val] = value
case int:
listRes, isMapRes := parentItem.(*types.AttributeValueMemberL)
mapRes.setValueOf(val, value)
case int64:
listRes, isMapRes := parentItem.(modifiableSliceExprValue)
if !isMapRes {
return newValueNotAListError(r, r.SubRefs)
}
// TODO: handle indexes
listRes.Value[val] = value
listRes.setValueAt(int(val), value)
}
return nil
}
@ -136,20 +139,6 @@ func (r *astSubRef) deleteAttribute(ctx *evalContext, item models.Item) error {
return err
}
/*
for i, key := range r.Quals {
mapItem, isMapItem := parentItem.(*types.AttributeValueMemberM)
if !isMapItem {
return PathNotSettableError{}
}
if isLast := i == len(r.Quals)-1; isLast {
delete(mapItem.Value, key)
} else {
parentItem = mapItem.Value[key]
}
}
*/
if len(r.SubRefs) > 1 {
parentItem, err = r.evalSubRefs(ctx, item, parentItem, r.SubRefs[0:len(r.SubRefs)-1])
if err != nil {
@ -164,23 +153,20 @@ func (r *astSubRef) deleteAttribute(ctx *evalContext, item models.Item) error {
switch val := sv.(type) {
case string:
mapRes, isMapRes := parentItem.(*types.AttributeValueMemberM)
mapRes, isMapRes := parentItem.(modifiableMapExprValue)
if !isMapRes {
return newValueNotAMapError(r, r.SubRefs)
}
delete(mapRes.Value, val)
case int:
listRes, isMapRes := parentItem.(*types.AttributeValueMemberL)
mapRes.deleteValueOf(val)
case int64:
listRes, isMapRes := parentItem.(modifiableSliceExprValue)
if !isMapRes {
return newValueNotAListError(r, r.SubRefs)
}
// TODO: handle indexes out of bounds
oldList := listRes.Value
newList := append([]types.AttributeValue{}, oldList[:val]...)
newList = append(newList, oldList[val+1:]...)
listRes.Value = newList
listRes.deleteValueAt(int(val))
}
return nil
}
@ -214,18 +200,10 @@ func (sr *astSubRefType) evalToStrOrInt(ctx *evalContext, item models.Item) (any
return nil, err
}
switch v := subEvalItem.(type) {
case *types.AttributeValueMemberS:
return v.Value, nil
case *types.AttributeValueMemberN:
intVal, err := strconv.Atoi(v.Value)
if err == nil {
return intVal, nil
}
flVal, err := strconv.ParseFloat(v.Value, 64)
if err == nil {
return int(flVal), nil
}
return nil, err
case stringableExprValue:
return v.asString(), nil
case numberableExprValue:
return v.asInt(), nil
}
return nil, ValueNotUsableAsASubref{}
}

View file

@ -1 +1,414 @@
package queryexpr
import (
"github.com/aws/aws-sdk-go-v2/feature/dynamodb/expression"
"github.com/aws/aws-sdk-go-v2/service/dynamodb/types"
"github.com/lmika/dynamo-browse/internal/common/maputils"
"github.com/lmika/dynamo-browse/internal/common/sliceutils"
"github.com/pkg/errors"
"math/big"
"strconv"
)
type exprValue interface {
typeName() string
asGoValue() any
asAttributeValue() types.AttributeValue
}
type stringableExprValue interface {
exprValue
asString() string
}
type numberableExprValue interface {
exprValue
asBigFloat() *big.Float
asInt() int64
}
type slicableExprValue interface {
exprValue
len() int
valueAt(idx int) (exprValue, error)
}
type modifiableSliceExprValue interface {
setValueAt(idx int, value exprValue)
deleteValueAt(idx int)
}
type mappableExprValue interface {
len() int
hasKey(name string) bool
valueOf(name string) (exprValue, error)
}
type modifiableMapExprValue interface {
setValueOf(name string, value exprValue)
deleteValueOf(name string)
}
func buildExpressionFromValue(ev exprValue) expression.ValueBuilder {
return expression.Value(ev)
}
func newExprValueFromAttributeValue(ev types.AttributeValue) (exprValue, error) {
if ev == nil {
return nil, nil
}
switch xVal := ev.(type) {
case *types.AttributeValueMemberS:
return stringExprValue(xVal.Value), nil
case *types.AttributeValueMemberN:
xNumVal, _, err := big.ParseFloat(xVal.Value, 10, 63, big.ToNearestEven)
if err != nil {
return nil, err
}
return bigNumExprValue{num: xNumVal}, nil
case *types.AttributeValueMemberBOOL:
return boolExprValue(xVal.Value), nil
case *types.AttributeValueMemberNULL:
return nullExprValue{}, nil
case *types.AttributeValueMemberL:
return listProxyValue{list: xVal}, nil
case *types.AttributeValueMemberM:
return mapProxyValue{mapValue: xVal}, nil
case *types.AttributeValueMemberSS:
return stringSetProxyValue{stringSet: xVal}, nil
case *types.AttributeValueMemberNS:
return numberSetProxyValue{numberSet: xVal}, nil
}
return nil, errors.New("cannot convert to expr value")
}
type undefinedExprValue struct{}
func (b undefinedExprValue) asGoValue() any {
return nil
}
func (b undefinedExprValue) asAttributeValue() types.AttributeValue {
return nil
}
func (s undefinedExprValue) typeName() string {
return "UNDEFINED"
}
type stringExprValue string
func (s stringExprValue) asGoValue() any {
return string(s)
}
func (s stringExprValue) asAttributeValue() types.AttributeValue {
return &types.AttributeValueMemberS{Value: string(s)}
}
func (s stringExprValue) asString() string {
return string(s)
}
func (s stringExprValue) typeName() string {
return "S"
}
type int64ExprValue int64
func (i int64ExprValue) asGoValue() any {
return int(i)
}
func (i int64ExprValue) asAttributeValue() types.AttributeValue {
return &types.AttributeValueMemberN{Value: strconv.Itoa(int(i))}
}
func (i int64ExprValue) asInt() int64 {
return int64(i)
}
func (i int64ExprValue) asBigFloat() *big.Float {
var f big.Float
f.SetInt64(int64(i))
return &f
}
func (s int64ExprValue) typeName() string {
return "N"
}
type bigNumExprValue struct {
num *big.Float
}
func (i bigNumExprValue) asGoValue() any {
return i.num
}
func (i bigNumExprValue) asAttributeValue() types.AttributeValue {
return &types.AttributeValueMemberN{Value: i.num.String()}
}
func (i bigNumExprValue) asInt() int64 {
x, _ := i.num.Int64()
return x
}
func (i bigNumExprValue) asBigFloat() *big.Float {
return i.num
}
func (s bigNumExprValue) typeName() string {
return "N"
}
type boolExprValue bool
func (b boolExprValue) asGoValue() any {
return bool(b)
}
func (b boolExprValue) asAttributeValue() types.AttributeValue {
return &types.AttributeValueMemberBOOL{Value: bool(b)}
}
func (s boolExprValue) typeName() string {
return "BOOL"
}
type nullExprValue struct{}
func (b nullExprValue) asGoValue() any {
return nil
}
func (b nullExprValue) asAttributeValue() types.AttributeValue {
return &types.AttributeValueMemberNULL{Value: true}
}
func (s nullExprValue) typeName() string {
return "NULL"
}
type listExprValue []exprValue
func (bs listExprValue) asGoValue() any {
return sliceutils.Map(bs, func(t exprValue) any {
return t.asGoValue()
})
}
func (bs listExprValue) asAttributeValue() types.AttributeValue {
return &types.AttributeValueMemberL{Value: sliceutils.Map(bs, func(t exprValue) types.AttributeValue {
return t.asAttributeValue()
})}
}
func (bs listExprValue) len() int {
return len(bs)
}
func (bs listExprValue) valueAt(i int) (exprValue, error) {
return bs[i], nil
}
func (s listExprValue) typeName() string {
return "L"
}
type mapExprValue map[string]exprValue
func (bs mapExprValue) asGoValue() any {
return maputils.MapValues(bs, func(t exprValue) any {
return t.asGoValue()
})
}
func (bs mapExprValue) asAttributeValue() types.AttributeValue {
return &types.AttributeValueMemberM{Value: maputils.MapValues(bs, func(t exprValue) types.AttributeValue {
return t.asAttributeValue()
})}
}
func (bs mapExprValue) len() int {
return len(bs)
}
func (bs mapExprValue) hasKey(name string) bool {
_, ok := bs[name]
return ok
}
func (bs mapExprValue) valueOf(name string) (exprValue, error) {
return bs[name], nil
}
func (s mapExprValue) typeName() string {
return "M"
}
type listProxyValue struct {
list *types.AttributeValueMemberL
}
func (bs listProxyValue) asGoValue() any {
resultingList := make([]any, len(bs.list.Value))
for i, item := range bs.list.Value {
if av, _ := newExprValueFromAttributeValue(item); av != nil {
resultingList[i] = av.asGoValue()
} else {
resultingList[i] = nil
}
}
return resultingList
}
func (bs listProxyValue) asAttributeValue() types.AttributeValue {
return bs.list
}
func (bs listProxyValue) len() int {
return len(bs.list.Value)
}
func (bs listProxyValue) valueAt(i int) (exprValue, error) {
return newExprValueFromAttributeValue(bs.list.Value[i])
}
func (bs listProxyValue) setValueAt(i int, newVal exprValue) {
bs.list.Value[i] = newVal.asAttributeValue()
}
func (bs listProxyValue) deleteValueAt(idx int) {
newList := append([]types.AttributeValue{}, bs.list.Value[:idx]...)
newList = append(newList, bs.list.Value[idx+1:]...)
bs.list = &types.AttributeValueMemberL{Value: newList}
}
func (s listProxyValue) typeName() string {
return "L"
}
type mapProxyValue struct {
mapValue *types.AttributeValueMemberM
}
func (bs mapProxyValue) asGoValue() any {
resultingMap := make(map[string]any)
for k, item := range bs.mapValue.Value {
if av, _ := newExprValueFromAttributeValue(item); av != nil {
resultingMap[k] = av.asGoValue()
} else {
resultingMap[k] = nil
}
}
return resultingMap
}
func (bs mapProxyValue) asAttributeValue() types.AttributeValue {
return bs.mapValue
}
func (bs mapProxyValue) len() int {
return len(bs.mapValue.Value)
}
func (bs mapProxyValue) hasKey(name string) bool {
_, ok := bs.mapValue.Value[name]
return ok
}
func (bs mapProxyValue) valueOf(name string) (exprValue, error) {
return newExprValueFromAttributeValue(bs.mapValue.Value[name])
}
func (bs mapProxyValue) setValueOf(name string, newVal exprValue) {
bs.mapValue.Value[name] = newVal.asAttributeValue()
}
func (bs mapProxyValue) deleteValueOf(name string) {
delete(bs.mapValue.Value, name)
}
func (s mapProxyValue) typeName() string {
return "M"
}
type stringSetProxyValue struct {
stringSet *types.AttributeValueMemberSS
}
func (bs stringSetProxyValue) asGoValue() any {
return bs.stringSet.Value
}
func (bs stringSetProxyValue) asAttributeValue() types.AttributeValue {
return bs.stringSet
}
func (bs stringSetProxyValue) len() int {
return len(bs.stringSet.Value)
}
func (bs stringSetProxyValue) valueAt(i int) (exprValue, error) {
return stringExprValue(bs.stringSet.Value[i]), nil
}
func (bs stringSetProxyValue) setValueAt(i int, newVal exprValue) {
if str, isStr := newVal.(stringableExprValue); isStr {
bs.stringSet.Value[i] = str.asString()
}
}
func (bs stringSetProxyValue) deleteValueAt(idx int) {
newList := append([]string{}, bs.stringSet.Value[:idx]...)
newList = append(newList, bs.stringSet.Value[idx+1:]...)
bs.stringSet = &types.AttributeValueMemberSS{Value: newList}
}
func (s stringSetProxyValue) typeName() string {
return "SS"
}
type numberSetProxyValue struct {
numberSet *types.AttributeValueMemberNS
}
func (bs numberSetProxyValue) asGoValue() any {
return bs.numberSet.Value
}
func (bs numberSetProxyValue) asAttributeValue() types.AttributeValue {
return bs.numberSet
}
func (bs numberSetProxyValue) len() int {
return len(bs.numberSet.Value)
}
func (bs numberSetProxyValue) valueAt(i int) (exprValue, error) {
fs, _, err := big.ParseFloat(bs.numberSet.Value[i], 10, 63, big.ToNearestEven)
if err != nil {
return nil, err
}
return bigNumExprValue{fs}, nil
}
func (bs numberSetProxyValue) setValueAt(i int, newVal exprValue) {
if str, isStr := newVal.(numberableExprValue); isStr {
bs.numberSet.Value[i] = str.asBigFloat().String()
}
}
func (bs numberSetProxyValue) deleteValueAt(idx int) {
newList := append([]string{}, bs.numberSet.Value[:idx]...)
newList = append(newList, bs.numberSet.Value[idx+1:]...)
bs.numberSet = &types.AttributeValueMemberNS{Value: newList}
}
func (s numberSetProxyValue) typeName() string {
return "NS"
}

View file

@ -2,59 +2,34 @@ package queryexpr
import (
"github.com/aws/aws-sdk-go-v2/feature/dynamodb/expression"
"github.com/lmika/audax/internal/dynamo-browse/models"
"github.com/lmika/dynamo-browse/internal/dynamo-browse/models"
"strconv"
"github.com/aws/aws-sdk-go-v2/service/dynamodb/types"
"github.com/pkg/errors"
)
func (a *astLiteralValue) evalToIR(ctx *evalContext, info *models.TableInfo) (irAtom, error) {
v, err := a.goValue()
v, err := a.exprValue()
if err != nil {
return nil, err
}
return irValue{value: v}, nil
}
func (a *astLiteralValue) dynamoValue() (types.AttributeValue, error) {
if a == nil {
return nil, nil
}
goValue, err := a.goValue()
if err != nil {
return nil, err
}
switch v := goValue.(type) {
case string:
return &types.AttributeValueMemberS{Value: v}, nil
case int64:
return &types.AttributeValueMemberN{Value: strconv.FormatInt(v, 10)}, nil
}
return nil, errors.New("unrecognised type")
}
func (a *astLiteralValue) goValue() (any, error) {
if a == nil {
return nil, nil
}
func (a *astLiteralValue) exprValue() (exprValue, error) {
switch {
case a.StringVal != nil:
s, err := strconv.Unquote(*a.StringVal)
if err != nil {
return nil, errors.Wrap(err, "cannot unquote string")
}
return s, nil
return stringExprValue(s), nil
case a.IntVal != nil:
return *a.IntVal, nil
return int64ExprValue(*a.IntVal), nil
case a.TrueBoolValue:
return true, nil
return boolExprValue(true), nil
case a.FalseBoolValue:
return false, nil
return boolExprValue(false), nil
}
return nil, errors.New("unrecognised type")
}
@ -78,17 +53,17 @@ func (a *astLiteralValue) String() string {
}
type irValue struct {
value any
value exprValue
}
func (i irValue) calcQueryForScan(info *models.TableInfo) (expression.ConditionBuilder, error) {
return expression.ConditionBuilder{}, NodeCannotBeConvertedToQueryError{}
}
func (i irValue) goValue() any {
func (i irValue) exprValue() exprValue {
return i.value
}
func (a irValue) calcOperand(info *models.TableInfo) expression.OperandBuilder {
return expression.Value(a.goValue())
return expression.Value(a.value.asGoValue())
}

View file

@ -0,0 +1,12 @@
package relitems
import (
"github.com/lmika/dynamo-browse/internal/dynamo-browse/models/queryexpr"
)
type RelatedItem struct {
Name string
Table string
Query *queryexpr.QueryExpr
OnSelect func() error
}

View file

@ -2,7 +2,7 @@ package serialisable
import (
"bytes"
"github.com/lmika/audax/internal/dynamo-browse/models/queryexpr"
"github.com/lmika/dynamo-browse/internal/dynamo-browse/models/queryexpr"
"time"
)

View file

@ -1,20 +1,67 @@
package models
import (
"github.com/lmika/audax/internal/dynamo-browse/models/attrutils"
"github.com/lmika/dynamo-browse/internal/dynamo-browse/models/attrutils"
"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 {
tableInfo *TableInfo
items []Item
criteria SortCriteria
items []Item
}
type SortField struct {
Field FieldValueEvaluator
Asc bool
}
type SortCriteria struct {
Fields []SortField
}
func (sc SortCriteria) FirstField() SortField {
if len(sc.Fields) == 0 {
return SortField{}
}
return sc.Fields[0]
}
func (sc SortCriteria) Equals(osc SortCriteria) bool {
if len(sc.Fields) != len(osc.Fields) {
return false
}
for i := range osc.Fields {
if sc.Fields[i].Field != osc.Fields[i].Field ||
sc.Fields[i].Asc != osc.Fields[i].Asc {
return false
}
}
return true
}
func (sc SortCriteria) Append(osc SortCriteria) SortCriteria {
newItems := make([]SortField, 0, len(osc.Fields))
newItems = append(newItems, sc.Fields...)
newItems = append(newItems, osc.Fields...)
return SortCriteria{Fields: newItems}
}
func PKSKSortFilter(ti *TableInfo) SortCriteria {
return SortCriteria{
Fields: []SortField{
{Field: SimpleFieldValueEvaluator(ti.Keys.PartitionKey), Asc: true},
{Field: SimpleFieldValueEvaluator(ti.Keys.SortKey), Asc: true},
},
}
}
// Sort sorts the items in place
func Sort(items []Item, tableInfo *TableInfo) {
si := sortedItems{items: items, tableInfo: tableInfo}
func Sort(items []Item, criteria SortCriteria) {
si := sortedItems{items: items, criteria: criteria}
sort.Sort(&si)
}
@ -23,30 +70,21 @@ func (si *sortedItems) Len() int {
}
func (si *sortedItems) Less(i, j int) bool {
// Compare primary keys
pv1, pv2 := si.items[i][si.tableInfo.Keys.PartitionKey], si.items[j][si.tableInfo.Keys.PartitionKey]
pc, ok := attrutils.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
if sortKey := si.tableInfo.Keys.SortKey; sortKey != "" {
sv1, sv2 := si.items[i][sortKey], si.items[j][sortKey]
sc, ok := attrutils.CompareScalarAttributes(sv1, sv2)
for _, field := range si.criteria.Fields {
// Compare primary keys
pv1, pv2 := field.Field.EvaluateForItem(si.items[i]), field.Field.EvaluateForItem(si.items[j])
pc, ok := attrutils.CompareScalarAttributes(pv1, pv2)
if !ok {
return i < j
}
if sc < 0 {
if !field.Asc {
pc = -pc
}
if pc < 0 {
return true
} else if sc > 0 {
} else if pc > 0 {
return false
}
}

View file

@ -4,7 +4,7 @@ import (
"testing"
"github.com/aws/aws-sdk-go-v2/service/dynamodb/types"
"github.com/lmika/audax/internal/dynamo-browse/models"
"github.com/lmika/dynamo-browse/internal/dynamo-browse/models"
"github.com/stretchr/testify/assert"
)
@ -15,7 +15,7 @@ func TestSort(t *testing.T) {
items := make([]models.Item, len(testStringData))
copy(items, testStringData)
models.Sort(items, tableInfo)
models.Sort(items, models.PKSKSortFilter(tableInfo))
assert.Equal(t, items[0], testStringData[1])
assert.Equal(t, items[1], testStringData[2])
@ -28,7 +28,7 @@ func TestSort(t *testing.T) {
items := make([]models.Item, len(testNumberData))
copy(items, testNumberData)
models.Sort(items, tableInfo)
models.Sort(items, models.PKSKSortFilter(tableInfo))
assert.Equal(t, items[0], testNumberData[2])
assert.Equal(t, items[1], testNumberData[1])
@ -41,7 +41,7 @@ func TestSort(t *testing.T) {
items := make([]models.Item, len(testBoolData))
copy(items, testBoolData)
models.Sort(items, tableInfo)
models.Sort(items, models.PKSKSortFilter(tableInfo))
assert.Equal(t, items[0], testBoolData[2])
assert.Equal(t, items[1], testBoolData[1])

View file

@ -7,9 +7,9 @@ import (
"github.com/aws/aws-sdk-go-v2/feature/dynamodb/expression"
"github.com/aws/aws-sdk-go-v2/service/dynamodb"
"github.com/aws/aws-sdk-go-v2/service/dynamodb/types"
"github.com/lmika/audax/internal/common/sliceutils"
"github.com/lmika/audax/internal/dynamo-browse/models"
"github.com/lmika/audax/internal/dynamo-browse/services/jobs"
"github.com/lmika/dynamo-browse/internal/common/sliceutils"
"github.com/lmika/dynamo-browse/internal/dynamo-browse/models"
"github.com/lmika/dynamo-browse/internal/dynamo-browse/services/jobs"
"github.com/pkg/errors"
"time"
)

View file

@ -3,12 +3,12 @@ package dynamo_test
import (
"context"
"fmt"
"github.com/lmika/audax/internal/dynamo-browse/models"
"github.com/lmika/dynamo-browse/internal/dynamo-browse/models"
"testing"
"github.com/aws/aws-sdk-go-v2/service/dynamodb/types"
"github.com/lmika/audax/internal/dynamo-browse/providers/dynamo"
"github.com/lmika/audax/test/testdynamo"
"github.com/lmika/dynamo-browse/internal/dynamo-browse/providers/dynamo"
"github.com/lmika/dynamo-browse/test/testdynamo"
"github.com/stretchr/testify/assert"
)

View file

@ -3,8 +3,8 @@ package inputhistorystore
import (
"context"
"github.com/asdine/storm"
"github.com/lmika/audax/internal/common/sliceutils"
"github.com/lmika/audax/internal/common/workspaces"
"github.com/lmika/dynamo-browse/internal/common/sliceutils"
"github.com/lmika/dynamo-browse/internal/common/workspaces"
"github.com/pkg/errors"
"sort"
"time"

View file

@ -0,0 +1,11 @@
package pasteboardprovider
type NilProvider struct{}
func (NilProvider) ReadText() (string, bool) {
return "", false
}
func (n NilProvider) WriteText(bts []byte) error {
return nil
}

View file

@ -0,0 +1,55 @@
package pasteboardprovider
import (
"github.com/pkg/errors"
"golang.design/x/clipboard"
"sync"
)
type Provider struct {
mutex *sync.Mutex
clipboardInit bool
}
func New() *Provider {
return &Provider{
mutex: new(sync.Mutex),
}
}
func (c *Provider) initClipboard() error {
c.mutex.Lock()
defer c.mutex.Unlock()
if c.clipboardInit {
return nil
}
if err := clipboard.Init(); err != nil {
return errors.Wrap(err, "unable to enable clipboard")
}
c.clipboardInit = true
return nil
}
func (c *Provider) WriteText(bts []byte) error {
if err := c.initClipboard(); err != nil {
return err
}
clipboard.Write(clipboard.FmtText, bts)
return nil
}
func (c *Provider) ReadText() (string, bool) {
if err := c.initClipboard(); err != nil {
return "", false
}
content := clipboard.Read(clipboard.FmtText)
if content == nil {
return "", false
}
return string(content), true
}

View file

@ -2,7 +2,7 @@ package settingstore
import (
"github.com/asdine/storm"
"github.com/lmika/audax/internal/common/workspaces"
"github.com/lmika/dynamo-browse/internal/common/workspaces"
"github.com/pkg/errors"
"io/fs"
"log"
@ -113,7 +113,7 @@ func (c *SettingStore) SetDefaultLimit(limit int) error {
func (c *SettingStore) getStringValue(key string, def string) (string, error) {
var val string
if err := c.ws.Get(settingBucket, keyTableReadOnly, &val); err != nil {
if err := c.ws.Get(settingBucket, key, &val); err != nil {
if errors.Is(err, storm.ErrNotFound) {
return def, nil
}

View file

@ -2,8 +2,8 @@ package workspacestore
import (
"github.com/asdine/storm"
"github.com/lmika/audax/internal/common/workspaces"
"github.com/lmika/audax/internal/dynamo-browse/models/serialisable"
"github.com/lmika/dynamo-browse/internal/common/workspaces"
"github.com/lmika/dynamo-browse/internal/dynamo-browse/models/serialisable"
"github.com/pkg/errors"
"log"
)

View file

@ -2,7 +2,7 @@ package inputhistory
import (
"context"
"github.com/lmika/audax/internal/dynamo-browse/services"
"github.com/lmika/dynamo-browse/internal/dynamo-browse/services"
"log"
"strings"
)

View file

@ -2,8 +2,8 @@ package itemrenderer
import (
"fmt"
"github.com/lmika/audax/internal/dynamo-browse/models"
"github.com/lmika/audax/internal/dynamo-browse/models/itemrender"
"github.com/lmika/dynamo-browse/internal/dynamo-browse/models"
"github.com/lmika/dynamo-browse/internal/dynamo-browse/models/itemrender"
"io"
"text/tabwriter"
)

View file

@ -0,0 +1,6 @@
package services
type PasteboardProvider interface {
ReadText() (string, bool)
WriteText(bts []byte) error
}

View file

@ -7,8 +7,11 @@ package scriptmanager
import (
"context"
"github.com/cloudcmds/tamarin/object"
"fmt"
"log"
"github.com/pkg/errors"
"github.com/risor-io/risor/object"
)
func printBuiltin(ctx context.Context, args ...object.Object) object.Object {
@ -53,3 +56,47 @@ func printfBuiltin(ctx context.Context, args ...object.Object) object.Object {
log.Printf("%s "+format, values...)
return object.Nil
}
// This is taken from the args package
func require(funcName string, count int, args []object.Object) *object.Error {
nArgs := len(args)
if nArgs != count {
if count == 1 {
return object.Errorf(
fmt.Sprintf("type error: %s() takes exactly 1 argument (%d given)",
funcName, nArgs))
}
return object.Errorf(
fmt.Sprintf("type error: %s() takes exactly %d arguments (%d given)",
funcName, count, nArgs))
}
return nil
}
func bindArgs(funcName string, args []object.Object, bindArgs ...any) *object.Error {
if err := require(funcName, len(bindArgs), args); err != nil {
return err
}
for i, bindArg := range bindArgs {
switch t := bindArg.(type) {
case *string:
str, err := object.AsString(args[i])
if err != nil {
return err
}
*t = str
case **object.Function:
fnRes, isFnRes := args[i].(*object.Function)
if !isFnRes {
return object.NewError(errors.Errorf("expected arg %v to be a function, was %T", i, bindArg))
}
*t = fnRes
default:
return object.NewError(errors.Errorf("unhandled arg type %v", i))
}
}
return nil
}

View file

@ -3,7 +3,7 @@ package scriptmanager
import (
"context"
"github.com/aws/aws-sdk-go-v2/service/dynamodb/types"
"github.com/lmika/audax/internal/dynamo-browse/models"
"github.com/lmika/dynamo-browse/internal/dynamo-browse/models"
)
//go:generate mockery --with-expecter --name UIService
@ -32,6 +32,7 @@ type SessionService interface {
type QueryOptions struct {
TableName string
IndexName string
NamePlaceholders map[string]string
ValuePlaceholders map[string]types.AttributeValue
}

View file

@ -5,10 +5,10 @@ package mocks
import (
context "context"
models "github.com/lmika/audax/internal/dynamo-browse/models"
models "github.com/lmika/dynamo-browse/internal/dynamo-browse/models"
mock "github.com/stretchr/testify/mock"
scriptmanager "github.com/lmika/audax/internal/dynamo-browse/services/scriptmanager"
scriptmanager "github.com/lmika/dynamo-browse/internal/dynamo-browse/services/scriptmanager"
)
// SessionService is an autogenerated mock type for the SessionService type

View file

@ -3,11 +3,13 @@ package scriptmanager
import (
"context"
"fmt"
"github.com/cloudcmds/tamarin/arg"
"github.com/cloudcmds/tamarin/object"
"github.com/cloudcmds/tamarin/scope"
"github.com/pkg/errors"
"regexp"
"github.com/aws/aws-sdk-go-v2/service/dynamodb/types"
"github.com/lmika/dynamo-browse/internal/dynamo-browse/models"
"github.com/lmika/dynamo-browse/internal/dynamo-browse/models/queryexpr"
"github.com/pkg/errors"
"github.com/risor-io/risor/object"
)
var (
@ -18,22 +20,18 @@ type extModule struct {
scriptPlugin *ScriptPlugin
}
func (m *extModule) register(scp *scope.Scope) {
modScope := scope.New(scope.Opts{})
mod := object.NewModule("ext", modScope)
modScope.AddBuiltins([]*object.Builtin{
object.NewBuiltin("command", m.command, mod),
object.NewBuiltin("key_binding", m.keyBinding, mod),
func (m *extModule) register() *object.Module {
return object.NewBuiltinsModule("ext", map[string]object.Object{
"command": object.NewBuiltin("command", m.command),
"key_binding": object.NewBuiltin("key_binding", m.keyBinding),
"related_items": object.NewBuiltin("related_items", m.relatedItem),
})
scp.Declare("ext", mod, true)
}
func (m *extModule) command(ctx context.Context, args ...object.Object) object.Object {
thisEnv := scriptEnvFromCtx(ctx)
if err := arg.Require("ext.command", 2, args); err != nil {
if err := require("ext.command", 2, args); err != nil {
return err
}
@ -59,11 +57,12 @@ func (m *extModule) command(ctx context.Context, args ...object.Object) object.O
}
newEnv := thisEnv
newEnv.options = m.scriptPlugin.scriptService.options
ctx = ctxWithScriptEnv(ctx, newEnv)
res := callFn(ctx, fnRes.Scope(), fnRes, objArgs)
if object.IsError(res) {
res, err := callFn(ctx, fnRes, objArgs)
if err != nil {
return errors.Errorf("command error '%v':%v - %v", m.scriptPlugin.name, cmdName, err)
} else if object.IsError(res) {
errObj := res.(*object.Error)
return errors.Errorf("command error '%v':%v - %v", m.scriptPlugin.name, cmdName, errObj.Inspect())
}
@ -80,7 +79,7 @@ func (m *extModule) command(ctx context.Context, args ...object.Object) object.O
func (m *extModule) keyBinding(ctx context.Context, args ...object.Object) object.Object {
thisEnv := scriptEnvFromCtx(ctx)
if err := arg.Require("ext.key_binding", 3, args); err != nil {
if err := require("ext.key_binding", 3, args); err != nil {
return err
}
@ -119,11 +118,12 @@ func (m *extModule) keyBinding(ctx context.Context, args ...object.Object) objec
}
newEnv := thisEnv
newEnv.options = m.scriptPlugin.scriptService.options
ctx = ctxWithScriptEnv(ctx, newEnv)
res := callFn(ctx, fnRes.Scope(), fnRes, objArgs)
if object.IsError(res) {
res, err := callFn(ctx, fnRes, objArgs)
if err != nil {
return errors.Errorf("command error '%v':%v - %v", m.scriptPlugin.name, bindingName, err)
} else if object.IsError(res) {
errObj := res.(*object.Error)
return errors.Errorf("command error '%v':%v - %v", m.scriptPlugin.name, bindingName, errObj.Inspect())
}
@ -141,3 +141,130 @@ func (m *extModule) keyBinding(ctx context.Context, args ...object.Object) objec
m.scriptPlugin.keyToKeyBinding[defaultKey] = fullBindingName
return nil
}
func (m *extModule) relatedItem(ctx context.Context, args ...object.Object) object.Object {
thisEnv := scriptEnvFromCtx(ctx)
var (
tableName string
callbackFn *object.Function
)
if err := bindArgs("ext.related_items", args, &tableName, &callbackFn); err != nil {
return err
}
callFn, hasCallFn := object.GetCallFunc(ctx)
if !hasCallFn {
return object.NewError(errors.New("no callFn found in context"))
}
newHandler := func(ctx context.Context, rs *models.ResultSet, index int) ([]relatedItem, error) {
newEnv := thisEnv
ctx = ctxWithScriptEnv(ctx, newEnv)
res, err := callFn(ctx, callbackFn, []object.Object{
newItemProxy(newResultSetProxy(rs), index),
})
if err != nil {
return nil, errors.Errorf("script error '%v':related_item - %v", m.scriptPlugin.name, err)
} else if object.IsError(res) {
errObj := res.(*object.Error)
return nil, errors.Errorf("script error '%v':related_item - %v", m.scriptPlugin.name, errObj.Inspect())
}
itr, objErr := object.AsIterator(res)
if err != nil {
return nil, objErr.Value()
}
var relItems []relatedItem
for next, hasNext := itr.Next(ctx); hasNext; next, hasNext = itr.Next(ctx) {
var newRelItem relatedItem
itemMap, objErr := object.AsMap(next)
if err != nil {
return nil, objErr.Value()
}
labelName, objErr := object.AsString(itemMap.Get("label"))
if objErr != nil {
continue
}
newRelItem.label = labelName
var tableStr = ""
if itemMap.Get("table") != object.Nil {
tableStr, objErr = object.AsString(itemMap.Get("table"))
if objErr != nil {
continue
}
}
newRelItem.table = tableStr
if selectFn, ok := itemMap.Get("on_select").(*object.Function); ok {
newRelItem.onSelect = func() error {
thisNewEnv := thisEnv
ctx = ctxWithScriptEnv(ctx, thisNewEnv)
res, err := callFn(ctx, selectFn, []object.Object{})
if err != nil {
return errors.Errorf("rel error '%v' - %v", m.scriptPlugin.name, err)
} else if object.IsError(res) {
errObj := res.(*object.Error)
return errors.Errorf("rel error '%v' - %v", m.scriptPlugin.name, errObj.Inspect())
}
return nil
}
} else {
queryExprStr, objErr := object.AsString(itemMap.Get("query"))
if objErr != nil {
continue
}
query, err := queryexpr.Parse(queryExprStr)
if err != nil {
continue
}
// Placeholders
if argsVal, isArgsValMap := object.AsMap(itemMap.Get("args")); isArgsValMap == nil {
namePlaceholders := make(map[string]string)
valuePlaceholders := make(map[string]types.AttributeValue)
for k, val := range argsVal.Value() {
switch v := val.(type) {
case *object.String:
namePlaceholders[k] = v.Value()
valuePlaceholders[k] = &types.AttributeValueMemberS{Value: v.Value()}
case *object.Int:
valuePlaceholders[k] = &types.AttributeValueMemberN{Value: fmt.Sprint(v.Value())}
case *object.Float:
valuePlaceholders[k] = &types.AttributeValueMemberN{Value: fmt.Sprint(v.Value())}
case *object.Bool:
valuePlaceholders[k] = &types.AttributeValueMemberBOOL{Value: v.Value()}
case *object.NilType:
valuePlaceholders[k] = &types.AttributeValueMemberNULL{Value: true}
default:
continue
}
}
query = query.WithNameParams(namePlaceholders).WithValueParams(valuePlaceholders)
}
newRelItem.query = query
}
relItems = append(relItems, newRelItem)
}
return relItems, nil
}
m.scriptPlugin.relatedItems = append(m.scriptPlugin.relatedItems, &relatedItemBuilder{
table: tableName,
itemProduction: newHandler,
})
return nil
}

View file

@ -0,0 +1,151 @@
package scriptmanager_test
import (
"context"
"testing"
"github.com/aws/aws-sdk-go-v2/service/dynamodb/types"
"github.com/lmika/dynamo-browse/internal/dynamo-browse/models"
"github.com/lmika/dynamo-browse/internal/dynamo-browse/services/scriptmanager"
"github.com/stretchr/testify/assert"
)
func TestExtModule_RelatedItems(t *testing.T) {
t.Run("should register a function which will return related items for an item", func(t *testing.T) {
scenarios := []struct {
desc string
code string
}{
{
desc: "single function, table name match",
code: `
ext.related_items("test-table", func(item) {
print("Hello")
return [
{"label": "Customer", "query": "pk=$foo", "args": {"foo": "foo"}},
{"label": "Payment", "query": "fla=$daa", "args": {"daa": "Hello"}},
]
})
`,
},
{
desc: "single function, table prefix match",
code: `
ext.related_items("test-*", func(item) {
print("Hello")
return [
{"label": "Customer", "query": "pk=$foo", "args": {"foo": "foo"}},
{"label": "Payment", "query": "fla=$daa", "args": {"daa": "Hello"}},
]
})
`,
},
{
desc: "multi function, table name match",
code: `
ext.related_items("test-table", func(item) {
print("Hello")
return [
{"label": "Customer", "query": "pk=$foo", "args": {"foo": "foo"}},
]
})
ext.related_items("test-table", func(item) {
return [
{"label": "Payment", "query": "fla=$daa", "args": {"daa": "Hello"}},
]
})
`,
},
{
desc: "multi function, table name prefix",
code: `
ext.related_items("test-*", func(item) {
print("Hello")
return [
{"label": "Customer", "query": "pk=$foo", "args": {"foo": "foo"}},
]
})
ext.related_items("test-*", func(item) {
return [
{"label": "Payment", "query": "fla=$daa", "args": {"daa": "Hello"}},
]
})
`,
},
}
for _, scenario := range scenarios {
t.Run(scenario.desc, func(t *testing.T) {
// Load the script
srv := scriptmanager.New(scriptmanager.WithFS(testScriptFile(t, "test.tm", scenario.code)))
ctx := context.Background()
plugin, err := srv.LoadScript(ctx, "test.tm")
assert.NoError(t, err)
assert.NotNil(t, plugin)
// Get related items of result set
rs := &models.ResultSet{
TableInfo: &models.TableInfo{
Name: "test-table",
},
}
rs.SetItems([]models.Item{
{"pk": &types.AttributeValueMemberS{Value: "abc"}},
{"pk": &types.AttributeValueMemberS{Value: "1232"}},
})
relItems, err := srv.RelatedItemOfItem(context.Background(), rs, 0)
assert.NoError(t, err)
assert.Len(t, relItems, 2)
assert.Equal(t, "Customer", relItems[0].Name)
assert.Equal(t, "pk=$foo", relItems[0].Query.String())
assert.Equal(t, "foo", relItems[0].Query.ValueParamOrNil("foo").(*types.AttributeValueMemberS).Value)
assert.Equal(t, "Payment", relItems[1].Name)
assert.Equal(t, "fla=$daa", relItems[1].Query.String())
assert.Equal(t, "Hello", relItems[1].Query.ValueParamOrNil("daa").(*types.AttributeValueMemberS).Value)
})
}
})
t.Run("should support rel_items with on select", func(t *testing.T) {
// Load the script
srv := scriptmanager.New(scriptmanager.WithFS(testScriptFile(t, "test.tm", `
ext.related_items("test-table", func(item) {
print("Hello")
return [
{"label": "Customer", "on_select": func() {
print("Selected")
}},
]
})
`)))
ctx := context.Background()
plugin, err := srv.LoadScript(ctx, "test.tm")
assert.NoError(t, err)
assert.NotNil(t, plugin)
// Get related items of result set
rs := &models.ResultSet{
TableInfo: &models.TableInfo{
Name: "test-table",
},
}
rs.SetItems([]models.Item{
{"pk": &types.AttributeValueMemberS{Value: "abc"}},
{"pk": &types.AttributeValueMemberS{Value: "1232"}},
})
relItems, err := srv.RelatedItemOfItem(context.Background(), rs, 0)
assert.NoError(t, err)
assert.Len(t, relItems, 1)
assert.Equal(t, "Customer", relItems[0].Name)
assert.NoError(t, relItems[0].OnSelect())
})
}

View file

@ -1,71 +0,0 @@
package scriptmanager
import (
"context"
"github.com/cloudcmds/tamarin/arg"
"github.com/cloudcmds/tamarin/object"
"github.com/cloudcmds/tamarin/scope"
"os"
"os/exec"
)
type osModule struct {
}
func (om *osModule) exec(ctx context.Context, args ...object.Object) object.Object {
if err := arg.Require("os.exec", 1, args); err != nil {
return err
}
cmdExec, objErr := object.AsString(args[0])
if objErr != nil {
return objErr
}
opts := scriptEnvFromCtx(ctx).options
if !opts.Permissions.AllowShellCommands {
return object.NewErrResult(object.Errorf("permission error: no permission to shell out"))
}
cmd := exec.Command(opts.OSExecShell, "-c", cmdExec)
out, err := cmd.Output()
if err != nil {
return object.NewErrResult(object.NewError(err))
}
return object.NewOkResult(object.NewString(string(out)))
}
func (om *osModule) env(ctx context.Context, args ...object.Object) object.Object {
if err := arg.Require("os.env", 1, args); err != nil {
return err
}
cmdEnvName, objErr := object.AsString(args[0])
if objErr != nil {
return objErr
}
opts := scriptEnvFromCtx(ctx).options
if !opts.Permissions.AllowEnv {
return object.Nil
}
envVal, hasVal := os.LookupEnv(cmdEnvName)
if !hasVal {
return object.Nil
}
return object.NewString(envVal)
}
func (om *osModule) register(scp *scope.Scope) {
modScope := scope.New(scope.Opts{})
mod := object.NewModule("os", modScope)
modScope.AddBuiltins([]*object.Builtin{
object.NewBuiltin("exec", om.exec, mod),
object.NewBuiltin("env", om.env, mod),
})
scp.Declare("os", mod, true)
}

View file

@ -2,8 +2,8 @@ package scriptmanager_test
import (
"context"
"github.com/lmika/audax/internal/dynamo-browse/services/scriptmanager"
"github.com/lmika/audax/internal/dynamo-browse/services/scriptmanager/mocks"
"github.com/lmika/dynamo-browse/internal/dynamo-browse/services/scriptmanager"
"github.com/lmika/dynamo-browse/internal/dynamo-browse/services/scriptmanager/mocks"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/mock"
"testing"
@ -15,49 +15,16 @@ func TestOSModule_Env(t *testing.T) {
t.Setenv("EMPTY_VALUE", "")
testFS := testScriptFile(t, "test.tm", `
assert(os.env("FULL_VALUE") == "this is a value")
assert(os.env("EMPTY_VALUE") == "")
assert(os.env("MISSING_VALUE") == nil)
assert(os.getenv("FULL_VALUE") == "this is a value")
assert(os.getenv("EMPTY_VALUE") == "")
assert(os.getenv("MISSING_VALUE") == "")
assert(bool(os.env("FULL_VALUE")) == true)
assert(bool(os.env("EMPTY_VALUE")) == false)
assert(bool(os.env("MISSING_VALUE")) == false)
assert(bool(os.getenv("FULL_VALUE")) == true)
assert(bool(os.getenv("EMPTY_VALUE")) == false)
assert(bool(os.getenv("MISSING_VALUE")) == false)
`)
srv := scriptmanager.New(scriptmanager.WithFS(testFS))
srv.SetDefaultOptions(scriptmanager.Options{
OSExecShell: "/bin/bash",
Permissions: scriptmanager.Permissions{
AllowEnv: true,
},
})
ctx := context.Background()
err := <-srv.RunAdHocScript(ctx, "test.tm")
assert.NoError(t, err)
})
t.Run("should return nil when no access to environment variables", func(t *testing.T) {
t.Setenv("FULL_VALUE", "this is a value")
t.Setenv("EMPTY_VALUE", "")
testFS := testScriptFile(t, "test.tm", `
assert(os.env("FULL_VALUE") == nil)
assert(os.env("EMPTY_VALUE") == nil)
assert(os.env("MISSING_VALUE") == nil)
assert(bool(os.env("FULL_VALUE")) == false)
assert(bool(os.env("EMPTY_VALUE")) == false)
assert(bool(os.env("MISSING_VALUE")) == false)
`)
srv := scriptmanager.New(scriptmanager.WithFS(testFS))
srv.SetDefaultOptions(scriptmanager.Options{
OSExecShell: "/bin/bash",
Permissions: scriptmanager.Permissions{
AllowEnv: false,
},
})
ctx := context.Background()
err := <-srv.RunAdHocScript(ctx, "test.tm")
@ -68,22 +35,14 @@ func TestOSModule_Env(t *testing.T) {
func TestOSModule_Exec(t *testing.T) {
t.Run("should run command and return stdout", func(t *testing.T) {
mockedUIService := mocks.NewUIService(t)
mockedUIService.EXPECT().PrintMessage(mock.Anything, "false")
mockedUIService.EXPECT().PrintMessage(mock.Anything, "hello world\n")
testFS := testScriptFile(t, "test.tm", `
res := os.exec('echo "hello world"')
ui.print(res.is_err())
ui.print(res.unwrap())
res := exec('echo', ["hello world"]).stdout
ui.print(res)
`)
srv := scriptmanager.New(scriptmanager.WithFS(testFS))
srv.SetDefaultOptions(scriptmanager.Options{
OSExecShell: "/bin/bash",
Permissions: scriptmanager.Permissions{
AllowShellCommands: true,
},
})
srv.SetIFaces(scriptmanager.Ifaces{
UI: mockedUIService,
})
@ -94,73 +53,4 @@ func TestOSModule_Exec(t *testing.T) {
mockedUIService.AssertExpectations(t)
})
t.Run("should refuse to execute command if do not have permissions", func(t *testing.T) {
mockedUIService := mocks.NewUIService(t)
mockedUIService.EXPECT().PrintMessage(mock.Anything, "true")
testFS := testScriptFile(t, "test.tm", `
res := os.exec('echo "hello world"')
ui.print(res.is_err())
`)
srv := scriptmanager.New(scriptmanager.WithFS(testFS))
srv.SetDefaultOptions(scriptmanager.Options{
OSExecShell: "/bin/bash",
Permissions: scriptmanager.Permissions{
AllowShellCommands: false,
},
})
srv.SetIFaces(scriptmanager.Ifaces{
UI: mockedUIService,
})
ctx := context.Background()
err := <-srv.RunAdHocScript(ctx, "test.tm")
assert.NoError(t, err)
mockedUIService.AssertExpectations(t)
})
t.Run("should be able to change permissions which will affect plugins", func(t *testing.T) {
mockedUIService := mocks.NewUIService(t)
mockedUIService.EXPECT().PrintMessage(mock.Anything, "Loaded the plugin\n")
mockedUIService.EXPECT().PrintMessage(mock.Anything, "true")
testFS := testScriptFile(t, "test.tm", `
ext.command("mycommand", func() {
ui.print(os.exec('echo "this cannot run"').is_err())
})
ui.print(os.exec('echo "Loaded the plugin"').unwrap())
`)
srv := scriptmanager.New(scriptmanager.WithFS(testFS))
srv.SetDefaultOptions(scriptmanager.Options{
OSExecShell: "/bin/bash",
Permissions: scriptmanager.Permissions{
AllowShellCommands: true,
},
})
srv.SetIFaces(scriptmanager.Ifaces{
UI: mockedUIService,
})
ctx := context.Background()
_, err := srv.LoadScript(ctx, "test.tm")
assert.NoError(t, err)
srv.SetDefaultOptions(scriptmanager.Options{
OSExecShell: "/bin/bash",
Permissions: scriptmanager.Permissions{
AllowShellCommands: false,
},
})
errChan := make(chan error)
assert.NoError(t, srv.LookupCommand("mycommand").Invoke(ctx, []string{}, errChan))
assert.NoError(t, waitForErr(t, errChan))
mockedUIService.AssertExpectations(t)
})
}

View file

@ -4,10 +4,8 @@ import (
"context"
"fmt"
"github.com/aws/aws-sdk-go-v2/service/dynamodb/types"
"github.com/cloudcmds/tamarin/arg"
"github.com/cloudcmds/tamarin/object"
"github.com/cloudcmds/tamarin/scope"
"github.com/pkg/errors"
"github.com/risor-io/risor/object"
)
type sessionModule struct {
@ -44,6 +42,11 @@ func (um *sessionModule) query(ctx context.Context, args ...object.Object) objec
}
}
// Index name
if val, isStr := objMap.Get("index").(*object.String); isStr {
options.IndexName = val.Value()
}
// Placeholders
if argsVal, isArgsValMap := objMap.Get("args").(*object.Map); isArgsValMap {
options.NamePlaceholders = make(map[string]string)
@ -72,13 +75,13 @@ func (um *sessionModule) query(ctx context.Context, args ...object.Object) objec
resp, err := um.sessionService.Query(ctx, expr, options)
if err != nil {
return object.NewErrResult(object.NewError(err))
return object.NewError(err)
}
return object.NewOkResult(&resultSetProxy{resultSet: resp})
return &resultSetProxy{resultSet: resp}
}
func (um *sessionModule) resultSet(ctx context.Context, args ...object.Object) object.Object {
if err := arg.Require("session.result_set", 0, args); err != nil {
if err := require("session.result_set", 0, args); err != nil {
return err
}
@ -90,7 +93,7 @@ func (um *sessionModule) resultSet(ctx context.Context, args ...object.Object) o
}
func (um *sessionModule) selectedItem(ctx context.Context, args ...object.Object) object.Object {
if err := arg.Require("session.result_set", 0, args); err != nil {
if err := require("session.result_set", 0, args); err != nil {
return err
}
@ -105,7 +108,7 @@ func (um *sessionModule) selectedItem(ctx context.Context, args ...object.Object
}
func (um *sessionModule) setResultSet(ctx context.Context, args ...object.Object) object.Object {
if err := arg.Require("session.set_result_set", 1, args); err != nil {
if err := require("session.set_result_set", 1, args); err != nil {
return err
}
@ -119,7 +122,7 @@ func (um *sessionModule) setResultSet(ctx context.Context, args ...object.Object
}
func (um *sessionModule) currentTable(ctx context.Context, args ...object.Object) object.Object {
if err := arg.Require("session.current_table", 0, args); err != nil {
if err := require("session.current_table", 0, args); err != nil {
return err
}
@ -131,17 +134,12 @@ func (um *sessionModule) currentTable(ctx context.Context, args ...object.Object
return &tableProxy{table: rs.TableInfo}
}
func (um *sessionModule) register(scp *scope.Scope) {
modScope := scope.New(scope.Opts{})
mod := object.NewModule("session", modScope)
modScope.AddBuiltins([]*object.Builtin{
object.NewBuiltin("query", um.query, mod),
object.NewBuiltin("current_table", um.currentTable, mod),
object.NewBuiltin("result_set", um.resultSet, mod),
object.NewBuiltin("selected_item", um.selectedItem, mod),
object.NewBuiltin("set_result_set", um.setResultSet, mod),
func (um *sessionModule) register() *object.Module {
return object.NewBuiltinsModule("session", map[string]object.Object{
"query": object.NewBuiltin("query", um.query),
"current_table": object.NewBuiltin("current_table", um.currentTable),
"result_set": object.NewBuiltin("result_set", um.resultSet),
"selected_item": object.NewBuiltin("selected_item", um.selectedItem),
"set_result_set": object.NewBuiltin("set_result_set", um.setResultSet),
})
scp.Declare("session", mod, true)
}

View file

@ -3,9 +3,9 @@ package scriptmanager_test
import (
"context"
"github.com/aws/aws-sdk-go-v2/service/dynamodb/types"
"github.com/lmika/audax/internal/dynamo-browse/models"
"github.com/lmika/audax/internal/dynamo-browse/services/scriptmanager"
"github.com/lmika/audax/internal/dynamo-browse/services/scriptmanager/mocks"
"github.com/lmika/dynamo-browse/internal/dynamo-browse/models"
"github.com/lmika/dynamo-browse/internal/dynamo-browse/services/scriptmanager"
"github.com/lmika/dynamo-browse/internal/dynamo-browse/services/scriptmanager/mocks"
"github.com/pkg/errors"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/mock"
@ -102,7 +102,7 @@ func TestModSession_Query(t *testing.T) {
mockedUIService.EXPECT().PrintMessage(mock.Anything, "res[1].attr('size(pk)') = 4")
testFS := testScriptFile(t, "test.tm", `
res := session.query("some expr").unwrap()
res := session.query("some expr")
ui.print(res.length)
ui.print("res[0]['pk'].S = ", res[0].attr("pk"))
ui.print("res[1]['pk'].S = ", res[1].attr("pk"))
@ -128,13 +128,9 @@ func TestModSession_Query(t *testing.T) {
mockedSessionService.EXPECT().Query(mock.Anything, "some expr", scriptmanager.QueryOptions{}).Return(nil, errors.New("bang"))
mockedUIService := mocks.NewUIService(t)
mockedUIService.EXPECT().PrintMessage(mock.Anything, "true")
mockedUIService.EXPECT().PrintMessage(mock.Anything, "err(\"bang\")")
testFS := testScriptFile(t, "test.tm", `
res := session.query("some expr")
ui.print(res.is_err())
ui.print(res)
`)
srv := scriptmanager.New(scriptmanager.WithFS(testFS))
@ -145,7 +141,7 @@ func TestModSession_Query(t *testing.T) {
ctx := context.Background()
err := <-srv.RunAdHocScript(ctx, "test.tm")
assert.NoError(t, err)
assert.Error(t, err)
mockedUIService.AssertExpectations(t)
mockedSessionService.AssertExpectations(t)
@ -165,7 +161,7 @@ func TestModSession_Query(t *testing.T) {
res := session.query("some expr", {
table: "some-table",
})
assert(!res.is_err())
assert(res)
`)
srv := scriptmanager.New(scriptmanager.WithFS(testFS))
@ -201,7 +197,7 @@ func TestModSession_Query(t *testing.T) {
res := session.query("some expr", {
table: session.result_set().table,
})
assert(!res.is_err())
assert(res)
`)
srv := scriptmanager.New(scriptmanager.WithFS(testFS))
@ -242,7 +238,7 @@ func TestModSession_Query(t *testing.T) {
value: "world",
},
})
assert(!res.is_err())
assert(res)
`)
srv := scriptmanager.New(scriptmanager.WithFS(testFS))
@ -288,7 +284,7 @@ func TestModSession_Query(t *testing.T) {
"nil": nil,
},
})
assert(!res.is_err())
assert(res)
`)
srv := scriptmanager.New(scriptmanager.WithFS(testFS))
@ -315,7 +311,6 @@ func TestModSession_Query(t *testing.T) {
"bad": func() { },
},
})
assert(res.is_err())
`)
srv := scriptmanager.New(scriptmanager.WithFS(testFS))
@ -411,7 +406,7 @@ func TestModSession_SetResultSet(t *testing.T) {
mockedUIService := mocks.NewUIService(t)
testFS := testScriptFile(t, "test.tm", `
res := session.query("some expr").unwrap()
res := session.query("some expr")
session.set_result_set(res)
`)

View file

@ -2,10 +2,9 @@ package scriptmanager
import (
"context"
"github.com/cloudcmds/tamarin/arg"
"github.com/cloudcmds/tamarin/object"
"github.com/cloudcmds/tamarin/scope"
"strings"
"github.com/risor-io/risor/object"
)
type uiModule struct {
@ -15,6 +14,10 @@ type uiModule struct {
func (um *uiModule) print(ctx context.Context, args ...object.Object) object.Object {
var msg strings.Builder
for _, arg := range args {
if arg == nil {
continue
}
switch a := arg.(type) {
case *object.String:
msg.WriteString(a.Value())
@ -28,7 +31,7 @@ func (um *uiModule) print(ctx context.Context, args ...object.Object) object.Obj
}
func (um *uiModule) prompt(ctx context.Context, args ...object.Object) object.Object {
if err := arg.Require("ui.prompt", 1, args); err != nil {
if err := require("ui.prompt", 1, args); err != nil {
return err
}
@ -47,14 +50,9 @@ func (um *uiModule) prompt(ctx context.Context, args ...object.Object) object.Ob
}
}
func (um *uiModule) register(scp *scope.Scope) {
modScope := scope.New(scope.Opts{})
mod := object.NewModule("ui", modScope)
modScope.AddBuiltins([]*object.Builtin{
object.NewBuiltin("print", um.print, mod),
object.NewBuiltin("prompt", um.prompt, mod),
func (um *uiModule) register() *object.Module {
return object.NewBuiltinsModule("ui", map[string]object.Object{
"print": object.NewBuiltin("print", um.print),
"prompt": object.NewBuiltin("prompt", um.prompt),
})
scp.Declare("ui", mod, true)
}

View file

@ -2,8 +2,8 @@ package scriptmanager_test
import (
"context"
"github.com/lmika/audax/internal/dynamo-browse/services/scriptmanager"
"github.com/lmika/audax/internal/dynamo-browse/services/scriptmanager/mocks"
"github.com/lmika/dynamo-browse/internal/dynamo-browse/services/scriptmanager"
"github.com/lmika/dynamo-browse/internal/dynamo-browse/services/scriptmanager/mocks"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/mock"
"testing"

View file

@ -2,42 +2,12 @@ package scriptmanager
import (
"context"
"os"
"github.com/risor-io/risor/limits"
)
type Options struct {
// OSExecShell is the shell to use for calls to 'os.exec'. If not defined,
// it will use the value of the SHELL environment variable, otherwise it will
// default to '/bin/bash'
OSExecShell string
// Permissions are the permissions the script can execute in
Permissions Permissions
}
func (opts Options) configuredShell() string {
if opts.OSExecShell != "" {
return opts.OSExecShell
}
if shell, hasShell := os.LookupEnv("SHELL"); hasShell {
return shell
}
return "/bin/bash"
}
// Permissions control the set of permissions of a script
type Permissions struct {
// AllowShellCommands determines whether or not a script can execute shell commands.
AllowShellCommands bool
// AllowEnv determines whether or not a script can access environment variables
AllowEnv bool
}
// scriptEnv is the runtime environment for a particular script execution
type scriptEnv struct {
filename string
options Options
}
type scriptEnvKeyType struct{}
@ -50,5 +20,7 @@ func scriptEnvFromCtx(ctx context.Context) scriptEnv {
}
func ctxWithScriptEnv(ctx context.Context, perms scriptEnv) context.Context {
return context.WithValue(ctx, scriptEnvKey, perms)
newCtx := context.WithValue(ctx, scriptEnvKey, perms)
newCtx = limits.WithLimits(newCtx, limits.New())
return newCtx
}

View file

@ -0,0 +1,57 @@
package scriptmanager
import (
"context"
"log"
"path"
"github.com/lmika/dynamo-browse/internal/dynamo-browse/models"
"github.com/lmika/dynamo-browse/internal/dynamo-browse/models/queryexpr"
"github.com/lmika/dynamo-browse/internal/dynamo-browse/models/relitems"
)
type relatedItem struct {
label string
table string
query *queryexpr.QueryExpr
onSelect func() error
}
type relatedItemBuilder struct {
table string
itemProduction func(ctx context.Context, rs *models.ResultSet, index int) ([]relatedItem, error)
}
func (s *Service) RelatedItemOfItem(ctx context.Context, rs *models.ResultSet, index int) ([]relitems.RelatedItem, error) {
riModels := []relitems.RelatedItem{}
for _, plugin := range s.plugins {
for _, rb := range plugin.relatedItems {
// TODO: should support matching
match, _ := tableMatchesGlob(rb.table, rs.TableInfo.Name)
log.Printf("RelatedItemOfItem: table = '%v', pattern = '%v', match = '%v'", rb.table, rs.TableInfo.Name, match)
if match {
relatedItems, err := rb.itemProduction(ctx, rs, index)
if err != nil {
// TODO: should probably return error if no rel items were found and an error was raised
return nil, err
}
// TODO: make this nicer
for _, ri := range relatedItems {
riModels = append(riModels, relitems.RelatedItem{
Name: ri.label,
Query: ri.query,
Table: ri.table,
OnSelect: ri.onSelect,
})
}
}
}
}
return riModels, nil
}
func tableMatchesGlob(tableName, pattern string) (bool, error) {
return path.Match(tableName, pattern)
}

View file

@ -2,17 +2,37 @@ package scriptmanager
import (
"context"
"github.com/cloudcmds/tamarin/arg"
"github.com/cloudcmds/tamarin/object"
"github.com/lmika/audax/internal/dynamo-browse/models"
"github.com/lmika/audax/internal/dynamo-browse/models/queryexpr"
"time"
"github.com/aws/aws-sdk-go-v2/service/dynamodb/types"
"github.com/lmika/dynamo-browse/internal/dynamo-browse/models"
"github.com/lmika/dynamo-browse/internal/dynamo-browse/models/attrutils"
"github.com/lmika/dynamo-browse/internal/dynamo-browse/models/queryexpr"
"github.com/pkg/errors"
"github.com/risor-io/risor/object"
"github.com/risor-io/risor/op"
)
type resultSetProxy struct {
resultSet *models.ResultSet
}
func newResultSetProxy(rs *models.ResultSet) *resultSetProxy {
return &resultSetProxy{resultSet: rs}
}
func (r *resultSetProxy) SetAttr(name string, value object.Object) error {
return errors.Errorf("attribute error: %v", name)
}
func (r *resultSetProxy) RunOperation(opType op.BinaryOpType, right object.Object) object.Object {
return object.Errorf("op error: unsupported %v", opType)
}
func (r *resultSetProxy) Cost() int {
return len(r.resultSet.Items())
}
func (r *resultSetProxy) Interface() interface{} {
return r.resultSet
}
@ -95,17 +115,105 @@ func (r *resultSetProxy) GetAttr(name string) (object.Object, bool) {
return &tableProxy{table: r.resultSet.TableInfo}, true
case "length":
return object.NewInt(int64(len(r.resultSet.Items()))), true
case "find":
return object.NewBuiltin("find", r.find), true
case "merge":
return object.NewBuiltin("merge", r.merge), true
}
return nil, false
}
func (i *resultSetProxy) find(ctx context.Context, args ...object.Object) object.Object {
if objErr := require("resultset.find", 1, args); objErr != nil {
return objErr
}
str, objErr := object.AsString(args[0])
if objErr != nil {
return objErr
}
modExpr, err := queryexpr.Parse(str)
if err != nil {
return object.Errorf("arg error: invalid path expression: %v", err)
}
for idx, item := range i.resultSet.Items() {
rs, err := modExpr.EvalItem(item)
if err != nil {
continue
}
if attrutils.Truthy(rs) {
return newItemProxy(i, idx)
}
}
return object.Nil
}
func (i *resultSetProxy) merge(ctx context.Context, args ...object.Object) object.Object {
type pksk struct {
pk types.AttributeValue
sk types.AttributeValue
}
if objErr := require("resultset.merge", 1, args); objErr != nil {
return objErr
}
otherRS, isRS := args[0].(*resultSetProxy)
if !isRS {
return object.NewError(errors.Errorf("type error: expected a resultset (got %v)", args[0].Type()))
}
if !i.resultSet.TableInfo.Equal(otherRS.resultSet.TableInfo) {
return object.Nil
}
itemsInI := make(map[pksk]models.Item)
newItems := make([]models.Item, 0, len(i.resultSet.Items())+len(otherRS.resultSet.Items()))
for _, item := range i.resultSet.Items() {
pk, sk := item.PKSK(i.resultSet.TableInfo)
itemsInI[pksk{pk, sk}] = item
newItems = append(newItems, item)
}
for _, item := range otherRS.resultSet.Items() {
pk, sk := item.PKSK(i.resultSet.TableInfo)
if _, hasItem := itemsInI[pksk{pk, sk}]; !hasItem {
newItems = append(newItems, item)
}
}
newResultSet := &models.ResultSet{
Created: time.Now(),
TableInfo: i.resultSet.TableInfo,
}
newResultSet.SetItems(newItems)
return &resultSetProxy{resultSet: newResultSet}
}
type itemProxy struct {
resultSetProxy *resultSetProxy
itemIndex int
item models.Item
}
func (i *itemProxy) SetAttr(name string, value object.Object) error {
return errors.Errorf("attribute error: %v", name)
}
func (i *itemProxy) RunOperation(opType op.BinaryOpType, right object.Object) object.Object {
return object.Errorf("op error: unsupported %v", opType)
}
func (i *itemProxy) Cost() int {
return len(i.item)
}
func newItemProxy(rs *resultSetProxy, itemIndex int) *itemProxy {
return &itemProxy{
resultSetProxy: rs,
@ -154,7 +262,7 @@ func (i *itemProxy) GetAttr(name string) (object.Object, bool) {
}
func (i *itemProxy) value(ctx context.Context, args ...object.Object) object.Object {
if objErr := arg.Require("item.attr", 1, args); objErr != nil {
if objErr := require("item.attr", 1, args); objErr != nil {
return objErr
}
@ -180,7 +288,7 @@ func (i *itemProxy) value(ctx context.Context, args ...object.Object) object.Obj
}
func (i *itemProxy) setValue(ctx context.Context, args ...object.Object) object.Object {
if objErr := arg.Require("item.set_attr", 2, args); objErr != nil {
if objErr := require("item.set_attr", 2, args); objErr != nil {
return objErr
}
@ -207,7 +315,7 @@ func (i *itemProxy) setValue(ctx context.Context, args ...object.Object) object.
}
func (i *itemProxy) deleteAttr(ctx context.Context, args ...object.Object) object.Object {
if objErr := arg.Require("item.delete_attr", 1, args); objErr != nil {
if objErr := require("item.delete_attr", 1, args); objErr != nil {
return objErr
}

View file

@ -2,13 +2,14 @@ package scriptmanager_test
import (
"context"
"testing"
"github.com/aws/aws-sdk-go-v2/service/dynamodb/types"
"github.com/lmika/audax/internal/dynamo-browse/models"
"github.com/lmika/audax/internal/dynamo-browse/services/scriptmanager"
"github.com/lmika/audax/internal/dynamo-browse/services/scriptmanager/mocks"
"github.com/lmika/dynamo-browse/internal/dynamo-browse/models"
"github.com/lmika/dynamo-browse/internal/dynamo-browse/services/scriptmanager"
"github.com/lmika/dynamo-browse/internal/dynamo-browse/services/scriptmanager/mocks"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/mock"
"testing"
)
func TestResultSetProxy(t *testing.T) {
@ -29,7 +30,7 @@ func TestResultSetProxy(t *testing.T) {
mockedUIService := mocks.NewUIService(t)
testFS := testScriptFile(t, "test.tm", `
res := session.query("some expr").unwrap()
res := session.query("some expr")
// Test properties of the result set
assert(res.table.name, "hello")
@ -60,6 +61,123 @@ func TestResultSetProxy(t *testing.T) {
})
}
func TestResultSetProxy_Find(t *testing.T) {
t.Run("should return the first item that matches the given expression", func(t *testing.T) {
rs := &models.ResultSet{}
rs.SetItems([]models.Item{
{"pk": &types.AttributeValueMemberS{Value: "abc"}},
{"pk": &types.AttributeValueMemberS{Value: "abc"}, "sk": &types.AttributeValueMemberS{Value: "abc"}, "primary": &types.AttributeValueMemberS{Value: "yes"}},
{"pk": &types.AttributeValueMemberS{Value: "1232"}, "findMe": &types.AttributeValueMemberS{Value: "yes"}},
{"pk": &types.AttributeValueMemberS{Value: "2345"}, "findMe": &types.AttributeValueMemberS{Value: "second"}},
})
mockedSessionService := mocks.NewSessionService(t)
mockedSessionService.EXPECT().Query(mock.Anything, "some expr", scriptmanager.QueryOptions{}).Return(rs, nil)
testFS := testScriptFile(t, "test.tm", `
res := session.query("some expr")
assert(res.find('findMe is "any"').attr("pk") == "1232")
assert(res.find('findMe = "second"').attr("pk") == "2345")
assert(res.find('pk = sk').attr("primary") == "yes")
assert(res.find('findMe = "missing"') == nil)
`)
srv := scriptmanager.New(scriptmanager.WithFS(testFS))
srv.SetIFaces(scriptmanager.Ifaces{
Session: mockedSessionService,
})
ctx := context.Background()
err := <-srv.RunAdHocScript(ctx, "test.tm")
assert.NoError(t, err)
mockedSessionService.AssertExpectations(t)
})
}
func TestResultSetProxy_Merge(t *testing.T) {
t.Run("should return a result set with items from both if both are from the same table", func(t *testing.T) {
td := &models.TableInfo{Name: "test", Keys: models.KeyAttribute{PartitionKey: "pk", SortKey: "sk"}}
rs1 := &models.ResultSet{TableInfo: td}
rs1.SetItems([]models.Item{
{"pk": &types.AttributeValueMemberS{Value: "abc"}, "sk": &types.AttributeValueMemberS{Value: "123"}},
})
rs2 := &models.ResultSet{TableInfo: td}
rs2.SetItems([]models.Item{
{"pk": &types.AttributeValueMemberS{Value: "bcd"}, "sk": &types.AttributeValueMemberS{Value: "234"}},
})
mockedSessionService := mocks.NewSessionService(t)
mockedSessionService.EXPECT().Query(mock.Anything, "rs1", scriptmanager.QueryOptions{}).Return(rs1, nil)
mockedSessionService.EXPECT().Query(mock.Anything, "rs2", scriptmanager.QueryOptions{}).Return(rs2, nil)
testFS := testScriptFile(t, "test.tm", `
r1 := session.query("rs1")
r2 := session.query("rs2")
res := r1.merge(r2)
assert(res[0].attr("pk") == "abc")
assert(res[0].attr("sk") == "123")
assert(res[1].attr("pk") == "bcd")
assert(res[1].attr("sk") == "234")
`)
srv := scriptmanager.New(scriptmanager.WithFS(testFS))
srv.SetIFaces(scriptmanager.Ifaces{
Session: mockedSessionService,
})
ctx := context.Background()
err := <-srv.RunAdHocScript(ctx, "test.tm")
assert.NoError(t, err)
mockedSessionService.AssertExpectations(t)
})
t.Run("should return nil if result-sets are from different tables", func(t *testing.T) {
td1 := &models.TableInfo{Name: "test", Keys: models.KeyAttribute{PartitionKey: "pk", SortKey: "sk"}}
rs1 := &models.ResultSet{TableInfo: td1}
rs1.SetItems([]models.Item{
{"pk": &types.AttributeValueMemberS{Value: "abc"}, "sk": &types.AttributeValueMemberS{Value: "123"}},
})
td2 := &models.TableInfo{Name: "test2", Keys: models.KeyAttribute{PartitionKey: "pk2", SortKey: "sk"}}
rs2 := &models.ResultSet{TableInfo: td2}
rs2.SetItems([]models.Item{
{"pk": &types.AttributeValueMemberS{Value: "bcd"}, "sk": &types.AttributeValueMemberS{Value: "234"}},
})
mockedSessionService := mocks.NewSessionService(t)
mockedSessionService.EXPECT().Query(mock.Anything, "rs1", scriptmanager.QueryOptions{}).Return(rs1, nil)
mockedSessionService.EXPECT().Query(mock.Anything, "rs2", scriptmanager.QueryOptions{}).Return(rs2, nil)
testFS := testScriptFile(t, "test.tm", `
r1 := session.query("rs1")
r2 := session.query("rs2")
res := r1.merge(r2)
assert(res == nil)
`)
srv := scriptmanager.New(scriptmanager.WithFS(testFS))
srv.SetIFaces(scriptmanager.Ifaces{
Session: mockedSessionService,
})
ctx := context.Background()
err := <-srv.RunAdHocScript(ctx, "test.tm")
assert.NoError(t, err)
mockedSessionService.AssertExpectations(t)
})
}
func TestResultSetProxy_GetAttr(t *testing.T) {
t.Run("should return the value of items within a result set", func(t *testing.T) {
rs := &models.ResultSet{}
@ -87,7 +205,7 @@ func TestResultSetProxy_GetAttr(t *testing.T) {
mockedSessionService.EXPECT().Query(mock.Anything, "some expr", scriptmanager.QueryOptions{}).Return(rs, nil)
testFS := testScriptFile(t, "test.tm", `
res := session.query("some expr").unwrap()
res := session.query("some expr")
assert(res[0].attr("pk") == "abc", "str attr")
assert(res[0].attr("sk") == 123, "num attr")
@ -164,7 +282,7 @@ func TestResultSetProxy_SetAttr(t *testing.T) {
mockedUIService := mocks.NewUIService(t)
testFS := testScriptFile(t, "test.tm", `
res := session.query("some expr").unwrap()
res := session.query("some expr")
res[0].set_attr("pk", "bla-di-bla")
res[0].set_attr("num", 123)
@ -215,7 +333,7 @@ func TestResultSetProxy_DeleteAttr(t *testing.T) {
mockedUIService := mocks.NewUIService(t)
testFS := testScriptFile(t, "test.tm", `
res := session.query("some expr").unwrap()
res := session.query("some expr")
res[0].delete_attr("deleteMe")
session.set_result_set(res)
`)

Some files were not shown because too many files have changed in this diff Show more