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

View file

@ -4,33 +4,36 @@ import (
"context" "context"
"flag" "flag"
"fmt" "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" "log"
"net" "net"
"os" "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() { func main() {
@ -40,6 +43,7 @@ func main() {
var flagRO = flag.Bool("ro", false, "enable readonly mode") var flagRO = flag.Bool("ro", false, "enable readonly mode")
var flagDefaultLimit = flag.Int("default-limit", 0, "default limit for queries and scans") var flagDefaultLimit = flag.Int("default-limit", 0, "default limit for queries and scans")
var flagWorkspace = flag.String("w", "", "workspace file") var flagWorkspace = flag.String("w", "", "workspace file")
var flagQuery = flag.String("q", "", "run query")
flag.Parse() flag.Parse()
ctx := context.Background() ctx := context.Background()
@ -84,6 +88,7 @@ func main() {
resultSetSnapshotStore := workspacestore.NewResultSetSnapshotStore(ws) resultSetSnapshotStore := workspacestore.NewResultSetSnapshotStore(ws)
settingStore := settingstore.New(ws) settingStore := settingstore.New(ws)
inputHistoryStore := inputhistorystore.NewInputHistoryStore(ws) inputHistoryStore := inputhistorystore.NewInputHistoryStore(ws)
pasteboardProvider := pasteboardprovider.New()
if *flagRO { if *flagRO {
if err := settingStore.SetReadOnly(*flagRO); err != nil { if err := settingStore.SetReadOnly(*flagRO); err != nil {
@ -105,19 +110,58 @@ func main() {
state := controllers.NewState() state := controllers.NewState()
jobsController := controllers.NewJobsController(jobsService, eventBus, false) 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) tableWriteController := controllers.NewTableWriteController(state, tableService, jobsController, tableReadController, settingStore)
columnsController := controllers.NewColumnsController(eventBus) columnsController := controllers.NewColumnsController(tableReadController, eventBus)
exportController := controllers.NewExportController(state, columnsController) exportController := controllers.NewExportController(state, tableService, jobsController, columnsController, pasteboardProvider)
settingsController := controllers.NewSettingsController(settingStore, eventBus) settingsController := controllers.NewSettingsController(settingStore, eventBus)
keyBindings := keybindings.Default() 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) keyBindingService := keybindings_service.NewService(keyBindings)
keyBindingController := controllers.NewKeyBindingController(keyBindingService, scriptController) keyBindingController := controllers.NewKeyBindingController(keyBindingService, scriptController)
commandController := commandctrl.NewCommandController(inputHistoryService) commandController := commandctrl.NewCommandController(inputHistoryService)
commandController.AddCommandLookupExtension(scriptController) commandController.AddCommandLookupExtension(scriptController)
commandController.SetCommandCompletionProvider(columnsController)
model := ui.NewModel( model := ui.NewModel(
tableReadController, tableReadController,
@ -131,6 +175,7 @@ func main() {
scriptController, scriptController,
eventBus, eventBus,
keyBindingController, keyBindingController,
pasteboardProvider,
keyBindings, 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 ( require (
github.com/alecthomas/participle/v2 v2.0.0-beta.5 github.com/alecthomas/participle/v2 v2.0.0-beta.5
github.com/asdine/storm v2.1.2+incompatible 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 v1.18.1
github.com/aws/aws-sdk-go-v2/config v1.13.1 github.com/aws/aws-sdk-go-v2/config v1.18.27
github.com/aws/aws-sdk-go-v2/credentials v1.8.0 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/attributevalue v1.10.12
github.com/aws/aws-sdk-go-v2/feature/dynamodb/expression v1.4.39 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/dynamodb v1.19.11
github.com/aws/aws-sdk-go-v2/service/sqs v1.16.0 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/aws/aws-sdk-go-v2/service/ssm v1.24.0
github.com/brianvoe/gofakeit/v6 v6.15.0 github.com/brianvoe/gofakeit/v6 v6.15.0
github.com/calyptia/go-bubble-table v0.2.1 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/ansi v0.0.0-20211031195517-c9f0611b6c70
github.com/muesli/reflow v0.3.0 github.com/muesli/reflow v0.3.0
github.com/pkg/errors v0.9.1 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.design/x/clipboard v0.6.2
golang.org/x/exp v0.0.0-20230108222341-4b8118a2686a golang.org/x/exp v0.0.0-20230108222341-4b8118a2686a
) )
@ -35,38 +37,68 @@ require (
require ( require (
github.com/DataDog/zstd v1.5.2 // indirect github.com/DataDog/zstd v1.5.2 // indirect
github.com/Sereal/Sereal v0.0.0-20220220040404-e0d1e550e879 // 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/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/aws/protocol/eventstream v1.4.10 // indirect
github.com/aws/aws-sdk-go-v2/internal/configsources v1.1.28 // indirect github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.13.4 // indirect
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.4.22 // indirect github.com/aws/aws-sdk-go-v2/internal/configsources v1.1.34 // indirect
github.com/aws/aws-sdk-go-v2/internal/ini v1.3.5 // 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/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/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/checksum v1.1.29 // indirect
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.7.0 // indirect github.com/aws/aws-sdk-go-v2/service/internal/endpoint-discovery v1.7.28 // indirect
github.com/aws/aws-sdk-go-v2/service/sso v1.9.0 // indirect github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.9.28 // indirect
github.com/aws/aws-sdk-go-v2/service/sts v1.14.0 // 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/aws/smithy-go v1.13.5 // indirect
github.com/aymanbagabas/go-osc52 v1.0.3 // indirect github.com/aymanbagabas/go-osc52 v1.0.3 // indirect
github.com/containerd/console v1.0.3 // indirect github.com/containerd/console v1.0.3 // indirect
github.com/davecgh/go-spew v1.1.1 // 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/protobuf v1.5.2 // indirect
github.com/golang/snappy v0.0.4 // indirect github.com/golang/snappy v0.0.4 // indirect
github.com/hashicorp/errwrap v1.0.0 // indirect github.com/hashicorp/errwrap v1.0.0 // indirect
github.com/hashicorp/go-multierror v1.1.1 // indirect github.com/hashicorp/go-multierror v1.1.1 // indirect
github.com/jackc/pgpassfile v1.0.0 // indirect github.com/jackc/pgpassfile v1.0.0 // indirect
github.com/jackc/pgservicefile v0.0.0-20200714003250-2b9c44734f2b // indirect github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a // indirect
github.com/jackc/pgx/v5 v5.0.4 // indirect github.com/jackc/pgx/v5 v5.4.1 // indirect
github.com/jmespath/go-jmespath v0.4.0 // indirect github.com/jmespath/go-jmespath v0.4.0 // indirect
github.com/juju/ansiterm v0.0.0-20210929141451-8b71cc96ebdc // indirect github.com/juju/ansiterm v0.0.0-20210929141451-8b71cc96ebdc // indirect
github.com/lucasb-eyer/go-colorful v1.2.0 // indirect github.com/lucasb-eyer/go-colorful v1.2.0 // indirect
github.com/lunixbochs/vtclean v1.0.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/mattn/go-localereader v0.0.1 // indirect
github.com/muesli/cancelreader v0.2.2 // indirect github.com/muesli/cancelreader v0.2.2 // indirect
github.com/muesli/termenv v0.13.0 // indirect github.com/muesli/termenv v0.13.0 // indirect
github.com/pmezard/go-difflib v1.0.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/rivo/uniseg v0.4.2 // indirect
github.com/sahilm/fuzzy v0.1.0 // indirect github.com/sahilm/fuzzy v0.1.0 // indirect
github.com/stretchr/objx v0.5.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/vmihailenco/msgpack v4.0.4+incompatible // indirect
github.com/wI2L/jsondiff v0.3.0 // indirect github.com/wI2L/jsondiff v0.3.0 // indirect
go.etcd.io/bbolt v1.3.6 // 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/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/mobile v0.0.0-20210716004757-34ab1303b554 // indirect
golang.org/x/sys v0.1.0 // indirect golang.org/x/sys v0.8.0 // indirect
golang.org/x/term v0.0.0-20220919170432-7a66f970e087 // indirect golang.org/x/term v0.8.0 // indirect
golang.org/x/text v0.3.8 // indirect golang.org/x/text v0.9.0 // indirect
google.golang.org/appengine v1.6.7 // indirect google.golang.org/appengine v1.6.7 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect
) )

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/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 h1:vUG4lAyuPCXO0TLbXvPv7EB7cNK1QV/luu55UHLrrn8=
github.com/DataDog/zstd v1.5.2/go.mod h1:g4AWEaM3yOg3HYfnJ3YIawPnVdXJh9QME85blwSAmyw= 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 h1:y6dsSYVb1G5eK6mgmy+BgI3Mw35a3WghArZ/Hbebrjo=
github.com/alecthomas/participle/v2 v2.0.0-beta.5/go.mod h1:RC764t6n4L8D8ITAJv0qdokritYSNR3wV5cVwmIEaMM= 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/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 h1:dczuIkyqwY2LrtXPz8ixMrU/OFgZp71kbKTHGrXYt/Q=
github.com/asdine/storm v2.1.2+incompatible/go.mod h1:RarYDc9hq1UPLImuiXK3BIWPJLdIygvV3PsInK0FbVQ= github.com/asdine/storm v2.1.2+incompatible/go.mod h1:RarYDc9hq1UPLImuiXK3BIWPJLdIygvV3PsInK0FbVQ=
github.com/atotto/clipboard v0.1.4 h1:EH0zSVneZPSuFR11BlR9YppQTVDbh5+16AmcJi4g1z4= 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.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 h1:wyC6p9Yfq6V2y98wfDsj6OnNQa4w2BLGCLIxzNhwOGY=
github.com/aws/aws-sdk-go-v2 v1.17.4/go.mod h1:uzbQtefpm44goOPmdKyAlXSNcwlRgF3ePWVW6EtJvvw= 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 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.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 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.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 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/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 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/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 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.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.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.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 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.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.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.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 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.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 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.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 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.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 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/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 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/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 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.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 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.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 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.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 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/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 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.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 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.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.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.11.2/go.mod h1:3xHYmszWVx2c0kIwQeEVf9uSm4fYZt67FBJnwub1bgM=
github.com/aws/smithy-go v1.13.5 h1:hgz0X/DX0dGqTYpGALqXJoRKRj5oQ7150i5FdTePzO8= 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/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 h1:lIr7SlA5PxZyMV30bDW0MGbiOPXwc63yRuCP0ARubLw=
github.com/containerd/console v1.0.3/go.mod h1:7LqA/THxQ86k76b8c/EMSiaJ3h1eZkMkXar0TQ1gf3U= 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.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 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 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 h1:0/KbAdpx3UXAx1kEOWHJeOkpbgRFGHVgv+CFIY7dBJI=
github.com/gofrs/uuid v4.3.1+incompatible/go.mod h1:b2aQJv3Z4Fp6yNu3cdSllBxTCLRxnplIgP/c0N/04lM= 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.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.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk=
github.com/golang/protobuf v1.5.2 h1:ROPKBNFfQgOUMifHyP+KYbvpjbdoFNs+aK7DXlji0Tw= 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/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 h1:H5DkEtf6CXdFp0N0Em5UCwQpXMWke8IA0+lD48awMYo=
github.com/hashicorp/go-multierror v1.1.1/go.mod h1:iw975J/qwKPdAO1clOe2L8331t/9/fmwbPZ6JB6eMoM= 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/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 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsIM=
github.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg= 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 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-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 h1:r5O6y84qHX/z/HZV40JBdx2obsHz7/uRj5b+CcYEdeY=
github.com/jackc/pgx/v5 v5.0.4/go.mod h1:U0ynklHtgg43fue9Ly30w3OCSTDPlXjig9ghrNGaguQ= 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 h1:BEgLn5cpjn8UN1mAw4NjwDrS35OdebyEtFe+9YPoQUg=
github.com/jmespath/go-jmespath v0.4.0/go.mod h1:T8mJZnbsbmF+m6zOOFylbeCJqk5+pHWvzYPziyZiYoo= 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= 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/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.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI=
github.com/kr/pretty v0.3.0 h1:WgNl7dwNpEZ6jJ9k1snq4pZsg7DOEN8hP9Xw0Tsjwk0= 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/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.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= 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/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 h1:xu2sLAri4lGiovBDQKxl5mrXyESr3gUr5m5SM5+LVb8=
github.com/lunixbochs/vtclean v1.0.0/go.mod h1:pHhQNgMf3btfWnGBVipUOjRYhoOsdGqdm/+2c2E2WMI= 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-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.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.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 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.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 h1:ygSAOl7ZXTx4RdPYinUpg6W99U8jWvWi9Ye2JC/oIi4=
github.com/mattn/go-localereader v0.0.1/go.mod h1:8fBrzywKY7BI3czFoHkuzRoWE9C+EiG4R1k4Cjx5p88= 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= 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.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 h1:+xnbZSEeDbOIg5/mE6JF0w6n9duR1l3/WmbinWVwUuU=
github.com/mattn/go-runewidth v0.0.14/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= 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-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 h1:kMlmsLSbjkikxQJ1IPwaM+7LJ9ltFu/fi8CRzvSnQmA=
github.com/muesli/ansi v0.0.0-20211031195517-c9f0611b6c70/go.mod h1:fQuZ0gauxyBcmsdE3ZT4NasjaRdxmbCS0jRHsrWu3Ho= 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.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 h1:wK20DRpJdDX8b7Ek2QfhvqhRQFZ237RGRO0RQ/Iqdy0=
github.com/muesli/termenv v0.13.0/go.mod h1:sP1+uffeLaEYpyOTb8pLCUctGcGLnoFjSn4YJK5e2bc= 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 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= 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 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 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.1.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
github.com/rivo/uniseg v0.2.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 h1:YwD0ulJSJytLpiaWua0sBDusfsCZohxjxzVTYjwxfV8=
github.com/rivo/uniseg v0.4.2/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88= 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/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 h1:FzWGaw2Opqyu+794ZQ9SYifWv2EIXpwP4q8dY1kDAwI=
github.com/sahilm/fuzzy v0.1.0/go.mod h1:VFvziUEIMCrT6A6tw2RFIXPXXmzXbOsSHF0DOI8ZK9Y= 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.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.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 h1:1zr/of2m5FGMsad5YfcqgdqdWrIhu+EBEJRhR1U7z/c=
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= 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.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.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.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.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
github.com/stretchr/testify v1.8.1 h1:w7B6lhMri9wdJUVmEZPGGhZzrYTPvgJArz7wNPgYKsk= 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.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 h1:9jvXn7olKEHU1S9vwoMGliaT8jq1vJ7IH/n9zD9Dnlw=
github.com/tidwall/gjson v1.14.3/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk= github.com/tidwall/gjson v1.14.3/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk=
github.com/tidwall/match v1.1.1 h1:+Ho715JplO36QYgwN9PGYNhgZvoUSc9X2c80KVTi+GA= 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.0/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU=
github.com/tidwall/pretty v1.2.1 h1:qjsOFOWWQl+N3RsoF5/ssm1pHmJJwhjlSbZ51I6wMl4= github.com/tidwall/pretty v1.2.1 h1:qjsOFOWWQl+N3RsoF5/ssm1pHmJJwhjlSbZ51I6wMl4=
github.com/tidwall/pretty v1.2.1/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU= 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 h1:dSLoQfGFAo3F6OoNhwUmLwVgaUXK79GlxNBwueZn0xI=
github.com/vmihailenco/msgpack v4.0.4+incompatible/go.mod h1:fy3FlTQTDXWkZ7Bh6AcGMlsjHatGryHQYUTf1ShIgkk= 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 h1:iTzQ9u/d86GE9RsBzVHX88f2EA1vQUboHwLhSQFc1s4=
github.com/wI2L/jsondiff v0.3.0/go.mod h1:y1IMzNNjlSsk3IUoJdRJO7VRBtzMvRgyo4Vu0LdHpTc= 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.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 h1:/ecaJf0sk1l4l6V4awd65v2C3ILy7MSj+s/x1ADCIMU=
go.etcd.io/bbolt v1.3.6/go.mod h1:qXsaaIqmgQH0T+OPdb99Bf+PKfBBQVAdyD6TY9G8XM4= 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 h1:a3Np4qfKnLWwfFJQhUWU3IDeRfmVuqWl+QPtP4CSYGw=
golang.design/x/clipboard v0.6.2/go.mod h1:kqBSweBP0/im4SZGGjLrppH0D400Hnfo5WbFKSNK8N4= 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-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-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-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 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.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-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 h1:tlXy25amD5A7gOfbXdqCGN5k8ESEed/Ee1E5RcrYnqU=
golang.org/x/exp v0.0.0-20230108222341-4b8118a2686a/go.mod h1:CxIveKay+FTh1D0yPZemJVgC/95VzuuOLq5Qi4xnoYc= 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 h1:gdeQX7xJSkTNF+Sw7++XNIOo4pGL0CjQv3N2Vm1Erxk=
golang.org/x/exp/shiny v0.0.0-20230213192124-5e25df0256eb/go.mod h1:UH99kUObWAZkDnWqppdQe5ZhPYESUw8I0zVV1uWBR+0= 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-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-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 h1:hTftEOvwiOq2+O8k2D5/Q7COC7k5Qcrgc2TFURJYnvQ=
golang.org/x/image v0.0.0-20211028202545-6944b10bf410/go.mod h1:023OzeP/+EPmXeapQh35lcL3II3LrY8Ic+EFFKVhULM= 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-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 h1:3In5TnfvnuXTF/uflgpYxSCEGP2NdYT37KsPh3VjZYU=
golang.org/x/mobile v0.0.0-20210716004757-34ab1303b554/go.mod h1:jFTmtFYCV0MFtXBU+J5V/+5AUeVS0ON/0WkE/KSrl6E= 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.1.0/go.mod h1:0QHyrYULN0/3qlju5TqG8bIK38QM8yzMo5ekMj3DlcY=
golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= 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-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-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-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-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-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM=
golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2 h1:CIJ76btIcR3eFI5EgSo6k1qKw9KJexJuRLI9G7Hp5wE= 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-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-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-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-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200116001909-b77594299b42/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-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-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-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.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 h1:kunALQeHf1/185U1i0GOB/fy1IPRDDpuoOOqRReG57U=
golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 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-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-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 h1:tPwmk4vmvVCMdr98VgL4JH+qZxPL8fqlUOHnyOM8N3w=
golang.org/x/term v0.0.0-20220919170432-7a66f970e087/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= 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.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.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= 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.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 h1:nAL+RVCQ9uMn3vJZbV+MRnydTJFPf8qqY42YiA6MrqY=
golang.org/x/text v0.3.8/go.mod h1:E6s5w1FMmriuDzIBO73fBruAKo1PCIq6d2Q6DHfQ8WQ= 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-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-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.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.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-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-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/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 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) { 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) 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 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 { func Map[T, U any](ts []T, fn func(t T) U) []U {
us := make([]U, len(ts)) us := make([]U, len(ts))
for i, t := range ts { for i, t := range ts {
@ -39,3 +47,22 @@ func Filter[T any](ts []T, fn func(t T) bool) []T {
} }
return us 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" "path/filepath"
"strings" "strings"
"github.com/lmika/audax/internal/common/ui/events" "github.com/lmika/dynamo-browse/internal/common/ui/events"
"github.com/lmika/shellwords" "github.com/lmika/shellwords"
) )
const commandsCategory = "commands" const commandsCategory = "commands"
type CommandController struct { type CommandController struct {
historyProvider IterProvider historyProvider IterProvider
commandList *CommandList commandList *CommandList
lookupExtensions []CommandLookupExtension lookupExtensions []CommandLookupExtension
completionProvider CommandCompletionProvider
} }
func NewCommandController(historyProvider IterProvider) *CommandController { func NewCommandController(historyProvider IterProvider) *CommandController {
@ -40,6 +41,10 @@ func (c *CommandController) AddCommandLookupExtension(ext CommandLookupExtension
c.lookupExtensions = append(c.lookupExtensions, ext) c.lookupExtensions = append(c.lookupExtensions, ext)
} }
func (c *CommandController) SetCommandCompletionProvider(provider CommandCompletionProvider) {
c.completionProvider = provider
}
func (c *CommandController) Prompt() tea.Msg { func (c *CommandController) Prompt() tea.Msg {
return events.PromptForInputMsg{ return events.PromptForInputMsg{
Prompt: ":", Prompt: ":",
@ -47,6 +52,24 @@ func (c *CommandController) Prompt() tea.Msg {
OnDone: func(value string) tea.Msg { OnDone: func(value string) tea.Msg {
return c.Execute(value) 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 ( import (
"context" "context"
"github.com/lmika/audax/internal/common/ui/events" "github.com/lmika/dynamo-browse/internal/common/ui/events"
"github.com/lmika/audax/internal/dynamo-browse/services" "github.com/lmika/dynamo-browse/internal/dynamo-browse/services"
"testing" "testing"
"github.com/lmika/audax/internal/common/ui/commandctrl" "github.com/lmika/dynamo-browse/internal/common/ui/commandctrl"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
) )

View file

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

View file

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

View file

@ -2,7 +2,7 @@ package dispatcher
import ( import (
tea "github.com/charmbracelet/bubbletea" 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 { type DispatcherContext struct {

View file

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

View file

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

View file

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

View file

@ -2,21 +2,25 @@ package controllers
import ( import (
tea "github.com/charmbracelet/bubbletea" tea "github.com/charmbracelet/bubbletea"
"github.com/lmika/audax/internal/common/ui/events" "github.com/lmika/dynamo-browse/internal/common/ui/events"
"github.com/lmika/audax/internal/dynamo-browse/models" "github.com/lmika/dynamo-browse/internal/dynamo-browse/models"
"github.com/lmika/audax/internal/dynamo-browse/models/columns" "github.com/lmika/dynamo-browse/internal/dynamo-browse/models/columns"
"github.com/lmika/audax/internal/dynamo-browse/models/queryexpr" "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" bus "github.com/lmika/events"
"strings"
) )
type ColumnsController struct { type ColumnsController struct {
tr *TableReadController
// State // State
colModel *columns.Columns colModel *columns.Columns
resultSet *models.ResultSet resultSet *models.ResultSet
} }
func NewColumnsController(eventBus *bus.Bus) *ColumnsController { func NewColumnsController(tr *TableReadController, eventBus *bus.Bus) *ColumnsController {
cc := &ColumnsController{} cc := &ColumnsController{tr: tr}
eventBus.On(newResultSetEvent, cc.onNewResultSet) eventBus.On(newResultSetEvent, cc.onNewResultSet)
return cc return cc
@ -79,7 +83,7 @@ func (cc *ColumnsController) AddColumn(afterIndex int) tea.Msg {
newCol := columns.Column{ newCol := columns.Column{
Name: colExpr.String(), Name: colExpr.String(),
Evaluator: columns.ExprFieldValueEvaluator{Expr: colExpr}, Evaluator: queryexpr.ExprFieldValueEvaluator{Expr: colExpr},
} }
if afterIndex >= len(cc.colModel.Columns)-1 { if afterIndex >= len(cc.colModel.Columns)-1 {
@ -115,3 +119,44 @@ func (cc *ColumnsController) DeleteColumn(afterIndex int) tea.Msg {
return ColumnsUpdated{} 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 ( import (
tea "github.com/charmbracelet/bubbletea" 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 { type promptSequence struct {

View file

@ -2,8 +2,12 @@ package controllers
import ( import (
"fmt" "fmt"
"strings"
"time"
tea "github.com/charmbracelet/bubbletea" 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 { type SetTableItemView struct {
@ -42,6 +46,24 @@ func (rs NewResultSet) ModeMessage() string {
return modeLine 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 { func (rs NewResultSet) StatusMessage() string {
if rs.statusMessage != "" { if rs.statusMessage != "" {
return rs.statusMessage return rs.statusMessage
@ -69,3 +91,9 @@ func (rs ResultSetUpdated) StatusMessage() string {
type ShowColumnOverlay struct{} type ShowColumnOverlay struct{}
type HideColumnOverlay 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 package controllers
import ( import (
"bytes"
"context"
"encoding/csv" "encoding/csv"
tea "github.com/charmbracelet/bubbletea" "fmt"
"github.com/lmika/audax/internal/common/ui/events" "io"
"github.com/lmika/audax/internal/dynamo-browse/models/attrutils"
"github.com/pkg/errors"
"os" "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 { type ExportController struct {
state *State state *State
columns *ColumnsController tableService TableReadService
jobController *JobsController
columns *ColumnsController
pasteboardProvider services.PasteboardProvider
} }
func NewExportController(state *State, columns *ColumnsController) *ExportController { func NewExportController(
return &ExportController{state, columns} 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() resultSet := c.state.ResultSet()
if resultSet == nil { if resultSet == nil {
return events.Error(errors.New("no result set")) return events.Error(errors.New("no result set"))
} }
f, err := os.Create(filename) return NewJob(c.jobController, fmt.Sprintf("Exporting to %v…", filename), func(ctx context.Context) (int, error) {
if err != nil { f, err := os.Create(filename)
return events.Error(errors.Wrapf(err, "cannot export to '%v'", filename)) if err != nil {
} return 0, errors.Wrapf(err, "cannot export to '%v'", filename)
defer f.Close() }
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() defer cw.Flush()
columns := c.columns.Columns().VisibleColumns() colNames := make([]string, len(cols))
for i, c := range cols {
colNames := make([]string, len(columns))
for i, c := range columns {
colNames[i] = c.Name colNames[i] = c.Name
} }
if err := cw.Write(colNames); err != nil { 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 _, item := range resultSet.Items() {
for i, col := range columns { for i, col := range cols {
row[i], _ = attrutils.AttributeToString(col.Evaluator.EvaluateForItem(item)) row[i], _ = attrutils.AttributeToString(col.Evaluator.EvaluateForItem(item))
} }
if err := cw.Write(row); err != nil { 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 return nil
} }
type ExportOptions struct {
// AllResults returns all results from the table
AllResults bool
}

View file

@ -1,6 +1,9 @@
package controllers_test package controllers_test
import ( import (
"fmt"
"github.com/lmika/dynamo-browse/internal/common/sliceutils"
"github.com/lmika/dynamo-browse/internal/dynamo-browse/controllers"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
"os" "os"
"strings" "strings"
@ -14,7 +17,7 @@ func TestExportController_ExportCSV(t *testing.T) {
tempFile := tempFile(t) tempFile := tempFile(t)
invokeCommand(t, srv.readController.Init()) invokeCommand(t, srv.readController.Init())
invokeCommand(t, srv.exportController.ExportCSV(tempFile)) invokeCommand(t, srv.exportController.ExportCSV(tempFile, controllers.ExportOptions{}))
bts, err := os.ReadFile(tempFile) bts, err := os.ReadFile(tempFile)
assert.NoError(t, err) 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) { t.Run("should return error if result set is not set", func(t *testing.T) {
srv := newService(t, serviceConfig{tableName: "non-existant-table"}) srv := newService(t, serviceConfig{tableName: "non-existant-table"})
tempFile := tempFile(t) tempFile := tempFile(t)
invokeCommandExpectingError(t, srv.readController.Init()) 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) { 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") invokeCommandWithPrompt(t, srv.columnsController.AddColumn(1), "address.street")
invokeCommand(t, srv.columnsController.ShiftColumnLeft(1)) 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) bts, err := os.ReadFile(tempFile)
assert.NoError(t, err) 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(1))
invokeCommand(t, srv.columnsController.ToggleVisible(2)) 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) bts, err := os.ReadFile(tempFile)
assert.NoError(t, err) assert.NoError(t, err)

View file

@ -2,10 +2,12 @@ package controllers
import ( import (
"context" "context"
"io/fs"
"github.com/aws/aws-sdk-go-v2/service/dynamodb/types" "github.com/aws/aws-sdk-go-v2/service/dynamodb/types"
tea "github.com/charmbracelet/bubbletea" tea "github.com/charmbracelet/bubbletea"
"github.com/lmika/audax/internal/dynamo-browse/models" "github.com/lmika/dynamo-browse/internal/dynamo-browse/models"
"io/fs" "github.com/lmika/dynamo-browse/internal/dynamo-browse/models/relitems"
) )
type TableReadService interface { type TableReadService interface {
@ -33,3 +35,7 @@ type CustomKeyBindingSource interface {
UnbindKey(key string) UnbindKey(key string)
Rebind(bindingName string, newKey string) error 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 ( import (
"context" "context"
tea "github.com/charmbracelet/bubbletea" tea "github.com/charmbracelet/bubbletea"
"github.com/lmika/audax/internal/common/ui/events" "github.com/lmika/dynamo-browse/internal/common/ui/events"
"github.com/lmika/audax/internal/dynamo-browse/services/jobs" "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] { 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 { if jb.onEither != nil {
return jb.onEither(res, err) return jb.onEither(res, err)
} else if err == nil { } else if err == nil {
if jb.onDone == nil {
return nil
}
return jb.onDone(res) return jb.onDone(res)
} else { } else {
if jb.onErr != nil { if jb.onErr != nil {

View file

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

View file

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

View file

@ -3,21 +3,24 @@ package controllers
import ( import (
"context" "context"
"fmt" "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" "log"
"strings" "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 { type ScriptController struct {
scriptManager *scriptmanager.Service scriptManager *scriptmanager.Service
tableReadController *TableReadController tableReadController *TableReadController
jobController *JobsController
settingsController *SettingsController settingsController *SettingsController
eventBus *bus.Bus eventBus *bus.Bus
sendMsg func(msg tea.Msg) sendMsg func(msg tea.Msg)
@ -26,12 +29,14 @@ type ScriptController struct {
func NewScriptController( func NewScriptController(
scriptManager *scriptmanager.Service, scriptManager *scriptmanager.Service,
tableReadController *TableReadController, tableReadController *TableReadController,
jobController *JobsController,
settingsController *SettingsController, settingsController *SettingsController,
eventBus *bus.Bus, eventBus *bus.Bus,
) *ScriptController { ) *ScriptController {
sc := &ScriptController{ sc := &ScriptController{
scriptManager: scriptManager, scriptManager: scriptManager,
tableReadController: tableReadController, tableReadController: tableReadController,
jobController: jobController,
settingsController: settingsController, settingsController: settingsController,
eventBus: eventBus, eventBus: eventBus,
} }
@ -61,13 +66,6 @@ func (sc *ScriptController) Init() {
} else { } else {
log.Printf("warn: script lookup paths are invalid: %v", err) 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)) { 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) { func (s *sessionImpl) Query(ctx context.Context, query string, opts scriptmanager.QueryOptions) (*models.ResultSet, error) {
// Parse the query // Parse the query
expr, err := queryexpr.Parse(query) expr, err := queryexpr.Parse(query)
if err != nil { if err != nil {
@ -182,12 +179,22 @@ func (s *sessionImpl) Query(ctx context.Context, query string, opts scriptmanage
if opts.ValuePlaceholders != nil { if opts.ValuePlaceholders != nil {
expr = expr.WithValueParams(opts.ValuePlaceholders) 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 // Get the table info
var tableInfo *models.TableInfo var (
tableInfo *models.TableInfo
err error
)
tableName := opts.TableName tableName := opts.TableName
currentResultSet := s.sc.tableReadController.state.ResultSet() currentResultSet := s.tableReadController.state.ResultSet()
if tableName != "" { if tableName != "" {
// Table specified. If it's the same as the current table, then use the existing table info // 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 // Otherwise, describe the table
tableInfo, err = s.sc.tableReadController.tableService.Describe(ctx, tableName) tableInfo, err = s.tableReadController.tableService.Describe(ctx, tableName)
if err != nil { if err != nil {
return nil, errors.Wrapf(err, "cannot describe table '%v'", tableName) 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 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 { if err != nil {
return nil, err return nil, err
} }
@ -244,3 +251,31 @@ func (sc *ScriptController) LookupBinding(theKey string) string {
func (sc *ScriptController) UnbindKey(key string) { func (sc *ScriptController) UnbindKey(key string) {
sc.scriptManager.UnbindKey(key) 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 package controllers_test
import ( 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" "testing"
"time" "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) { func TestScriptController_RunScript(t *testing.T) {
@ -53,7 +54,7 @@ func TestScriptController_RunScript(t *testing.T) {
srv := newService(t, serviceConfig{ srv := newService(t, serviceConfig{
tableName: "alpha-table", tableName: "alpha-table",
scriptFS: testScriptFile(t, "test.tm", ` scriptFS: testScriptFile(t, "test.tm", `
rs := session.query('pk="abc"').unwrap() rs := session.query('pk="abc"')
ui.print(rs.length) ui.print(rs.length)
`), `),
}) })
@ -72,7 +73,7 @@ func TestScriptController_RunScript(t *testing.T) {
srv := newService(t, serviceConfig{ srv := newService(t, serviceConfig{
tableName: "alpha-table", tableName: "alpha-table",
scriptFS: testScriptFile(t, "test.tm", ` 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) ui.print(rs.length)
`), `),
}) })
@ -93,7 +94,7 @@ func TestScriptController_RunScript(t *testing.T) {
srv := newService(t, serviceConfig{ srv := newService(t, serviceConfig{
tableName: "alpha-table", tableName: "alpha-table",
scriptFS: testScriptFile(t, "test.tm", ` scriptFS: testScriptFile(t, "test.tm", `
rs := session.query('pk="abc"').unwrap() rs := session.query('pk="abc"')
session.set_result_set(rs) session.set_result_set(rs)
`), `),
}) })
@ -112,7 +113,7 @@ func TestScriptController_RunScript(t *testing.T) {
srv := newService(t, serviceConfig{ srv := newService(t, serviceConfig{
tableName: "alpha-table", tableName: "alpha-table",
scriptFS: testScriptFile(t, "test.tm", ` scriptFS: testScriptFile(t, "test.tm", `
rs := session.query('pk="abc"').unwrap() rs := session.query('pk="abc"')
rs[0].set_attr("pk", "131") rs[0].set_attr("pk", "131")
session.set_result_set(rs) session.set_result_set(rs)
`), `),
@ -135,22 +136,35 @@ func TestScriptController_RunScript(t *testing.T) {
func TestScriptController_LookupCommand(t *testing.T) { func TestScriptController_LookupCommand(t *testing.T) {
t.Run("should schedule the script on a separate go-routine", func(t *testing.T) { t.Run("should schedule the script on a separate go-routine", func(t *testing.T) {
srv := newService(t, serviceConfig{ scenarios := []struct {
tableName: "alpha-table", descr string
scriptFS: testScriptFile(t, "test.tm", ` command string
ext.command("mycommand", func(name) { expectedOutput string
ui.print("Hello, ", name) }{
{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.scriptController.LoadScript("test.tm"))
invokeCommand(t, srv.commandController.Execute(`mycommand "test name"`)) 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.Len(t, srv.msgSender.msgs, 1)
assert.Equal(t, events.StatusMsg("Hello, test name"), srv.msgSender.msgs[0]) 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) { 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 ( import (
"fmt" "fmt"
tea "github.com/charmbracelet/bubbletea" 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" bus "github.com/lmika/events"
"github.com/pkg/errors" "github.com/pkg/errors"
"log" "log"

View file

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

View file

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

View file

@ -4,22 +4,24 @@ import (
"bytes" "bytes"
"context" "context"
"fmt" "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" "log"
"strings" "strings"
"sync" "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 type resultSetUpdateOp int
@ -33,6 +35,7 @@ const (
resultSetUpdateTouch resultSetUpdateTouch
resultSetUpdateNextPage resultSetUpdateNextPage
resultSetUpdateScript resultSetUpdateScript
resultSetUpdateResort
) )
type MarkOp int type MarkOp int
@ -57,11 +60,12 @@ type TableReadController struct {
eventBus *bus.Bus eventBus *bus.Bus
tableName string tableName string
loadFromLastView bool loadFromLastView bool
pasteboardProvider services.PasteboardProvider
relatedItemSupplier RelatedItemSupplier
// state // state
mutex *sync.Mutex mutex *sync.Mutex
state *State state *State
clipboardInit bool
} }
func NewTableReadController( func NewTableReadController(
@ -72,6 +76,8 @@ func NewTableReadController(
jobController *JobsController, jobController *JobsController,
inputHistoryService *inputhistory.Service, inputHistoryService *inputhistory.Service,
eventBus *bus.Bus, eventBus *bus.Bus,
pasteboardProvider services.PasteboardProvider,
relatedItemSupplier RelatedItemSupplier,
tableName string, tableName string,
) *TableReadController { ) *TableReadController {
return &TableReadController{ return &TableReadController{
@ -83,6 +89,8 @@ func NewTableReadController(
inputHistoryService: inputHistoryService, inputHistoryService: inputHistoryService,
eventBus: eventBus, eventBus: eventBus,
tableName: tableName, tableName: tableName,
pasteboardProvider: pasteboardProvider,
relatedItemSupplier: relatedItemSupplier,
mutex: new(sync.Mutex), 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() }).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 { func (c *TableReadController) PromptForQuery() tea.Msg {
return events.PromptForInputMsg{ return events.PromptForInputMsg{
Prompt: "query: ", Prompt: "query: ",
@ -276,13 +291,35 @@ func (c *TableReadController) setResultSetAndFilter(resultSet *models.ResultSet,
return c.state.buildNewResultSetMessage("") return c.state.buildNewResultSetMessage("")
} }
func (c *TableReadController) Mark(op MarkOp) tea.Msg { func (c *TableReadController) Mark(op MarkOp, where string) tea.Msg {
c.state.withResultSet(func(resultSet *models.ResultSet) { var (
for i := range resultSet.Items() { 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) { if resultSet.Hidden(i) {
continue 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 { switch op {
case MarkOpMark: case MarkOpMark:
resultSet.SetMark(i, true) resultSet.SetMark(i, true)
@ -292,7 +329,10 @@ func (c *TableReadController) Mark(op MarkOp) tea.Msg {
resultSet.SetMark(i, !resultSet.Marked(i)) resultSet.SetMark(i, !resultSet.Marked(i))
} }
} }
}) return nil
}); err != nil {
return events.Error(err)
}
return ResultSetUpdated{} return ResultSetUpdated{}
} }
@ -377,7 +417,7 @@ func (c *TableReadController) NextPage() tea.Msg {
resultSet := c.state.ResultSet() resultSet := c.state.ResultSet()
if resultSet == nil { if resultSet == nil {
return events.StatusMsg("Result-set is nil") return events.StatusMsg("Result-set is nil")
} else if resultSet.LastEvaluatedKey == nil { } else if !resultSet.HasNextPage() {
return events.StatusMsg("No more results") return events.StatusMsg("No more results")
} }
currentFilter := c.state.filter currentFilter := c.state.filter
@ -446,12 +486,8 @@ func (c *TableReadController) updateViewToSnapshot(viewSnapshot *serialisable.Vi
} }
func (c *TableReadController) CopyItemToClipboard(idx int) tea.Msg { func (c *TableReadController) CopyItemToClipboard(idx int) tea.Msg {
if err := c.initClipboard(); err != nil {
return events.Error(err)
}
itemCount := 0 itemCount := 0
c.state.withResultSet(func(resultSet *models.ResultSet) { if err := c.state.withResultSetReturningError(func(resultSet *models.ResultSet) error {
sb := new(strings.Builder) sb := new(strings.Builder)
_ = applyToMarkedItems(resultSet, idx, func(idx int, item models.Item) error { _ = applyToMarkedItems(resultSet, idx, func(idx int, item models.Item) error {
if sb.Len() > 0 { if sb.Len() > 0 {
@ -461,23 +497,14 @@ func (c *TableReadController) CopyItemToClipboard(idx int) tea.Msg {
itemCount += 1 itemCount += 1
return nil 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")) 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" "fmt"
"github.com/aws/aws-sdk-go-v2/service/dynamodb/types" "github.com/aws/aws-sdk-go-v2/service/dynamodb/types"
tea "github.com/charmbracelet/bubbletea" tea "github.com/charmbracelet/bubbletea"
"github.com/lmika/audax/internal/common/ui/events" "github.com/lmika/dynamo-browse/internal/common/ui/events"
"github.com/lmika/audax/internal/dynamo-browse/controllers" "github.com/lmika/dynamo-browse/internal/dynamo-browse/controllers"
"github.com/lmika/audax/test/testdynamo" "github.com/lmika/dynamo-browse/test/testdynamo"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
"os" "os"
"strings" "strings"
@ -89,7 +89,7 @@ func TestTableReadController_Query(t *testing.T) {
invokeCommand(t, srv.readController.Init()) invokeCommand(t, srv.readController.Init())
invokeCommandWithPrompts(t, srv.readController.PromptForQuery(), `pk ^= "abc"`) 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) bts, err := os.ReadFile(tempFile)
assert.NoError(t, err) assert.NoError(t, err)
@ -107,7 +107,7 @@ func TestTableReadController_Query(t *testing.T) {
invokeCommand(t, srv.readController.Init()) invokeCommand(t, srv.readController.Init())
invokeCommandWithPrompts(t, srv.readController.PromptForQuery(), `alpha = "This is some value"`) 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) bts, err := os.ReadFile(tempFile)
assert.NoError(t, err) assert.NoError(t, err)
@ -124,7 +124,7 @@ func TestTableReadController_Query(t *testing.T) {
tempFile := tempFile(t) tempFile := tempFile(t)
invokeCommandExpectingError(t, srv.readController.Init()) 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" "fmt"
"github.com/aws/aws-sdk-go-v2/service/dynamodb/types" "github.com/aws/aws-sdk-go-v2/service/dynamodb/types"
tea "github.com/charmbracelet/bubbletea" tea "github.com/charmbracelet/bubbletea"
"github.com/lmika/audax/internal/common/sliceutils" "github.com/lmika/dynamo-browse/internal/common/sliceutils"
"github.com/lmika/audax/internal/common/ui/events" "github.com/lmika/dynamo-browse/internal/common/ui/events"
"github.com/lmika/audax/internal/dynamo-browse/models" "github.com/lmika/dynamo-browse/internal/dynamo-browse/models"
"github.com/lmika/audax/internal/dynamo-browse/models/queryexpr" "github.com/lmika/dynamo-browse/internal/dynamo-browse/models/queryexpr"
"github.com/lmika/audax/internal/dynamo-browse/services/tables" "github.com/lmika/dynamo-browse/internal/dynamo-browse/services/tables"
"github.com/pkg/errors" "github.com/pkg/errors"
"log" "log"
"strconv" "strconv"
@ -122,6 +122,8 @@ func (twc *TableWriteController) SetAttributeValue(idx int, itemType models.Item
return twc.setBoolValue(idx, path) return twc.setBoolValue(idx, path)
case models.NullItemType: case models.NullItemType:
return twc.setNullValue(idx, path) return twc.setNullValue(idx, path)
case models.ExprValueItemType:
return twc.setToExpressionValue(idx, path)
default: default:
return events.Error(errors.New("unsupported attribute type")) 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 { func (twc *TableWriteController) setNumberValue(idx int, attr *queryexpr.QueryExpr) tea.Msg {
return events.PromptForInputMsg{ return events.PromptForInputMsg{
Prompt: "number value: ", 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 { if err := twc.state.withResultSetReturningError(func(set *models.ResultSet) error {
err := path.DeleteAttribute(set.Items()[idx]) if err := applyToMarkedItems(set, idx, func(idx int, item models.Item) error {
if err != nil { if err := path.DeleteAttribute(set.Items()[idx]); err != nil {
return err
}
set.SetDirty(idx, true)
return nil
}); err != nil {
return err return err
} }
set.SetDirty(idx, true)
set.RefreshColumns() set.RefreshColumns()
return nil return nil
}); err != nil { }); err != nil {
@ -418,6 +458,44 @@ func (twc *TableWriteController) assertReadWrite() error {
return nil 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 { func applyToN(prefix string, n int, singular, plural, suffix string) string {
if n == 1 { if n == 1 {
return fmt.Sprintf("%v%v %v%v", prefix, n, singular, suffix) return fmt.Sprintf("%v%v %v%v", prefix, n, singular, suffix)

View file

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

View file

@ -1,6 +1,6 @@
package controllers 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 { func applyToMarkedItems(rs *models.ResultSet, selectedIndex int, applyFn func(idx int, item models.Item) error) error {
if markedItems := rs.MarkedItems(); len(markedItems) > 0 { if markedItems := rs.MarkedItems(); len(markedItems) > 0 {

View file

@ -3,7 +3,7 @@ package attrcodec_test
import ( import (
"bytes" "bytes"
"github.com/aws/aws-sdk-go-v2/service/dynamodb/types" "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" "github.com/stretchr/testify/assert"
"strings" "strings"
"testing" "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 package columns
import ( import (
"github.com/aws/aws-sdk-go-v2/service/dynamodb/types" "github.com/lmika/dynamo-browse/internal/dynamo-browse/models"
"github.com/lmika/audax/internal/dynamo-browse/models"
"github.com/lmika/audax/internal/dynamo-browse/models/queryexpr"
) )
type Columns struct { type Columns struct {
@ -19,7 +17,7 @@ func NewColumnsFromResultSet(rs *models.ResultSet) *Columns {
for i, c := range rsCols { for i, c := range rsCols {
cols[i] = Column{ cols[i] = Column{
Name: c, 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 { if _, hasCol := existingColumns[c]; !hasCol {
newCols = append(newCols, Column{ newCols = append(newCols, Column{
Name: c, Name: c,
Evaluator: SimpleFieldValueEvaluator(c), Evaluator: models.SimpleFieldValueEvaluator(c),
}) })
} }
} }
@ -56,7 +54,7 @@ func (cols *Columns) AddMissingColumns(rs *models.ResultSet) {
} else { } else {
newCols[i] = Column{ newCols[i] = Column{
Name: c, Name: c,
Evaluator: SimpleFieldValueEvaluator(c), Evaluator: models.SimpleFieldValueEvaluator(c),
} }
} }
} }
@ -82,25 +80,6 @@ func (cols *Columns) VisibleColumns() []Column {
type Column struct { type Column struct {
Name string Name string
Evaluator FieldValueEvaluator Evaluator models.FieldValueEvaluator
Hidden bool 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 ( import (
"github.com/aws/aws-sdk-go-v2/service/dynamodb/types" "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 { type ItemIndex struct {
@ -16,7 +16,7 @@ type Item map[string]types.AttributeValue
func (i Item) Clone() Item { func (i Item) Clone() Item {
newItem := Item{} newItem := Item{}
// TODO: should be a deep clone? // TODO: should be a deep clone? YES!!
for k, v := range i { for k, v := range i {
newItem[k] = v newItem[k] = v
} }
@ -33,6 +33,14 @@ func (i Item) KeyValue(info *TableInfo) map[string]types.AttributeValue {
return itemKey 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) { func (i Item) AttributeValueAsString(key string) (string, bool) {
return attrutils.AttributeToString(i[key]) return attrutils.AttributeToString(i[key])
} }

View file

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

View file

@ -3,12 +3,14 @@ package models
import ( import (
"github.com/aws/aws-sdk-go-v2/service/dynamodb/types" "github.com/aws/aws-sdk-go-v2/service/dynamodb/types"
"sort" "sort"
"time"
) )
type ResultSet struct { type ResultSet struct {
// Query information // Query information
TableInfo *TableInfo TableInfo *TableInfo
Query Queryable Query Queryable
Created time.Time
ExclusiveStartKey map[string]types.AttributeValue ExclusiveStartKey map[string]types.AttributeValue
// Result information // Result information
@ -16,7 +18,8 @@ type ResultSet struct {
items []Item items []Item
attributes []ItemAttribute attributes []ItemAttribute
columns []string columns []string
sortCriteria SortCriteria
} }
type Queryable interface { type Queryable interface {
@ -46,6 +49,10 @@ func (rs *ResultSet) SetItems(items []Item) {
rs.attributes = make([]ItemAttribute, len(items)) rs.attributes = make([]ItemAttribute, len(items))
} }
func (rs *ResultSet) SortCriteria() SortCriteria {
return rs.sortCriteria
}
func (rs *ResultSet) AddNewItem(item Item, attrs ItemAttribute) { func (rs *ResultSet) AddNewItem(item Item, attrs ItemAttribute) {
rs.items = append(rs.items, item) rs.items = append(rs.items, item)
rs.attributes = append(rs.attributes, attrs) rs.attributes = append(rs.attributes, attrs)
@ -135,3 +142,12 @@ func (rs *ResultSet) RefreshColumns() {
rs.columns = columns 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 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) { func (a *astExpr) calcPatchMods(item models.Item) ([]patchMod, error) {
patchMods := make([]patchMod, 0) patchMods := make([]patchMod, 0)

View file

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

View file

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

View file

@ -2,7 +2,7 @@ package modexpr
import ( import (
"github.com/aws/aws-sdk-go-v2/service/dynamodb/types" "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 { type patchMod interface {

View file

@ -3,7 +3,7 @@ package models
import ( import (
"github.com/aws/aws-sdk-go-v2/aws" "github.com/aws/aws-sdk-go-v2/aws"
"github.com/aws/aws-sdk-go-v2/feature/dynamodb/expression" "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 { type QueryExecutionPlan struct {

View file

@ -4,17 +4,23 @@ import (
"github.com/alecthomas/participle/v2" "github.com/alecthomas/participle/v2"
"github.com/alecthomas/participle/v2/lexer" "github.com/alecthomas/participle/v2/lexer"
"github.com/aws/aws-sdk-go-v2/feature/dynamodb/expression" "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/sliceutils"
"github.com/lmika/audax/internal/common/sliceutils" "github.com/lmika/dynamo-browse/internal/dynamo-browse/models"
"github.com/lmika/audax/internal/dynamo-browse/models"
"github.com/pkg/errors" "github.com/pkg/errors"
"strconv"
) )
// Modelled on the expression language here // Modelled on the expression language here
// https://docs.aws.amazon.com/amazondynamodb/latest/developerguide/Expressions.OperatorsAndFunctions.html // https://docs.aws.amazon.com/amazondynamodb/latest/developerguide/Expressions.OperatorsAndFunctions.html
type astExpr struct { 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 { type astDisjunction struct {
@ -38,9 +44,15 @@ type astIn struct {
} }
type astComparisonOp struct { type astComparisonOp struct {
Ref *astEqualityOp `parser:"@@"` Ref *astBetweenOp `parser:"@@"`
Op string `parser:"( @('<' | '<=' | '>' | '>=')"` Op string `parser:"( @('<' | '<=' | '>' | '>=')"`
Value *astEqualityOp `parser:"@@ )?"` Value *astBetweenOp `parser:"@@ )?"`
}
type astBetweenOp struct {
Ref *astEqualityOp `parser:"@@"`
From *astEqualityOp `parser:"( 'between' @@ "`
To *astEqualityOp `parser:" 'and' @@ )?"`
} }
type astEqualityOp struct { type astEqualityOp struct {
@ -58,7 +70,6 @@ type astIsOp struct {
type astSubRef struct { type astSubRef struct {
Ref *astFunctionCall `parser:"@@"` Ref *astFunctionCall `parser:"@@"`
SubRefs []*astSubRefType `parser:"@@*"` SubRefs []*astSubRefType `parser:"@@*"`
//Quals []string `parser:"('.' @Ident)*"`
} }
type astSubRefType struct { type astSubRefType struct {
@ -121,7 +132,58 @@ func Parse(expr string) (*QueryExpr, error) {
return &QueryExpr{ast: ast}, nil 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 { type queryTestAttempt struct {
index string index string
keysUnderTest models.KeyAttribute keysUnderTest models.KeyAttribute
@ -153,11 +215,11 @@ func (a *astExpr) calcQuery(ctx *evalContext, info *models.TableInfo) (*models.Q
return nil, err return nil, err
} }
return &models.QueryExecutionPlan{ plans = append(plans, &models.QueryExecutionPlan{
CanQuery: true, CanQuery: true,
IndexName: attempt.index, IndexName: attempt.index,
Expression: expr, Expression: expr,
}, nil })
} }
} }
@ -174,21 +236,22 @@ func (a *astExpr) calcQuery(ctx *evalContext, info *models.TableInfo) (*models.Q
return nil, err return nil, err
} }
return &models.QueryExecutionPlan{ plans = append(plans, &models.QueryExecutionPlan{
CanQuery: false, CanQuery: false,
Expression: expr, Expression: expr,
}, nil })
return plans, nil
} }
func (a *astExpr) evalToIR(ctx *evalContext, tableInfo *models.TableInfo) (irAtom, error) { func (a *astExpr) evalToIR(ctx *evalContext, tableInfo *models.TableInfo) (irAtom, error) {
return a.Root.evalToIR(ctx, tableInfo) 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) 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) return a.Root.setEvalItem(ctx, item, value)
} }

View file

@ -1,8 +1,7 @@
package queryexpr package queryexpr
import ( import (
"github.com/aws/aws-sdk-go-v2/service/dynamodb/types" "github.com/lmika/dynamo-browse/internal/dynamo-browse/models"
"github.com/lmika/audax/internal/dynamo-browse/models"
"github.com/pkg/errors" "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") 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) { func (a *astAtom) unqualifiedName() (string, bool) {
switch { switch {
case a.Ref != nil: case a.Ref != nil:
@ -39,12 +29,12 @@ func (a *astAtom) unqualifiedName() (string, bool) {
return "", false 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 { switch {
case a.Ref != nil: case a.Ref != nil:
return a.Ref.evalItem(ctx, item) return a.Ref.evalItem(ctx, item)
case a.Literal != nil: case a.Literal != nil:
return a.Literal.dynamoValue() return a.Literal.exprValue()
case a.Placeholder != nil: case a.Placeholder != nil:
return a.Placeholder.evalItem(ctx, item) return a.Placeholder.evalItem(ctx, item)
case a.Paren != nil: case a.Paren != nil:
@ -66,7 +56,7 @@ func (a *astAtom) canModifyItem(ctx *evalContext, item models.Item) bool {
return false 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 { switch {
case a.Ref != nil: case a.Ref != nil:
return a.Ref.setEvalItem(ctx, item, value) 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 ( import (
"github.com/aws/aws-sdk-go-v2/feature/dynamodb/expression" "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/dynamo-browse/models"
"github.com/lmika/audax/internal/dynamo-browse/models"
"strings" "strings"
) )
@ -20,7 +19,7 @@ func (a *astBooleanNot) evalToIR(ctx *evalContext, tableInfo *models.TableInfo)
return &irBoolNot{atom: irNode}, nil 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) val, err := a.Operand.evalItem(ctx, item)
if err != nil { if err != nil {
return nil, err return nil, err
@ -30,7 +29,7 @@ func (a *astBooleanNot) evalItem(ctx *evalContext, item models.Item) (types.Attr
return val, nil 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 { 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) 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 { if a.HasNot {
return PathNotSettableError{} return PathNotSettableError{}
} }

View file

@ -2,38 +2,127 @@ package queryexpr
import ( import (
"context" "context"
"github.com/aws/aws-sdk-go-v2/service/dynamodb/types"
"github.com/pkg/errors" "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{ 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 { if len(args) != 1 {
return nil, InvalidArgumentNumberError{Name: "size", Expected: 1, Actual: len(args)} return nil, InvalidArgumentNumberError{Name: "size", Expected: 1, Actual: len(args)}
} }
var l int var l int
switch t := args[0].(type) { switch t := args[0].(type) {
case *types.AttributeValueMemberB: case stringExprValue:
l = len(t.Value) l = len(t)
case *types.AttributeValueMemberS: case mappableExprValue:
l = len(t.Value) l = t.len()
case *types.AttributeValueMemberL: case slicableExprValue:
l = len(t.Value) l = t.len()
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)
default: default:
return nil, errors.New("cannot take size of arg") 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 ( import (
"github.com/aws/aws-sdk-go-v2/feature/dynamodb/expression" "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/dynamo-browse/models"
"github.com/lmika/audax/internal/dynamo-browse/models" "github.com/lmika/dynamo-browse/internal/dynamo-browse/models/attrutils"
"github.com/lmika/audax/internal/dynamo-browse/models/attrutils"
"github.com/pkg/errors" "github.com/pkg/errors"
) )
@ -47,7 +46,7 @@ func (a *astComparisonOp) evalToIR(ctx *evalContext, info *models.TableInfo) (ir
return irGenericCmp{leftOpr, rightOpr, cmpType}, nil 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) left, err := a.Ref.evalItem(ctx, item)
if err != nil { if err != nil {
return nil, err return nil, err
@ -61,20 +60,21 @@ func (a *astComparisonOp) evalItem(ctx *evalContext, item models.Item) (types.At
return nil, err return nil, err
} }
cmp, isComparable := attrutils.CompareScalarAttributes(left, right) // TODO: use expr value here
cmp, isComparable := attrutils.CompareScalarAttributes(left.asAttributeValue(), right.asAttributeValue())
if !isComparable { if !isComparable {
return nil, ValuesNotComparable{Left: left, Right: right} return nil, ValuesNotComparable{Left: left.asAttributeValue(), Right: right.asAttributeValue()}
} }
switch opToCmdType[a.Op] { switch opToCmdType[a.Op] {
case cmpTypeLt: case cmpTypeLt:
return &types.AttributeValueMemberBOOL{Value: cmp < 0}, nil return boolExprValue(cmp < 0), nil
case cmpTypeLe: case cmpTypeLe:
return &types.AttributeValueMemberBOOL{Value: cmp <= 0}, nil return boolExprValue(cmp <= 0), nil
case cmpTypeGt: case cmpTypeGt:
return &types.AttributeValueMemberBOOL{Value: cmp > 0}, nil return boolExprValue(cmp > 0), nil
case cmpTypeGe: case cmpTypeGe:
return &types.AttributeValueMemberBOOL{Value: cmp >= 0}, nil return boolExprValue(cmp >= 0), nil
} }
return nil, errors.Errorf("unrecognised operator: %v", a.Op) 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) 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 != "" { if a.Op != "" {
return PathNotSettableError{} return PathNotSettableError{}
} }
@ -143,34 +143,34 @@ func (a irKeyFieldCmp) canBeExecutedAsQuery(qci *queryCalcInfo) bool {
func (a irKeyFieldCmp) calcQueryForScan(info *models.TableInfo) (expression.ConditionBuilder, error) { func (a irKeyFieldCmp) calcQueryForScan(info *models.TableInfo) (expression.ConditionBuilder, error) {
nb := a.name.calcName(info) nb := a.name.calcName(info)
vb := a.value.goValue() vb := a.value.exprValue()
switch a.cmpType { switch a.cmpType {
case cmpTypeLt: case cmpTypeLt:
return nb.LessThan(expression.Value(vb)), nil return nb.LessThan(buildExpressionFromValue(vb)), nil
case cmpTypeLe: case cmpTypeLe:
return nb.LessThanEqual(expression.Value(vb)), nil return nb.LessThanEqual(buildExpressionFromValue(vb)), nil
case cmpTypeGt: case cmpTypeGt:
return nb.GreaterThan(expression.Value(vb)), nil return nb.GreaterThan(buildExpressionFromValue(vb)), nil
case cmpTypeGe: case cmpTypeGe:
return nb.GreaterThanEqual(expression.Value(vb)), nil return nb.GreaterThanEqual(buildExpressionFromValue(vb)), nil
} }
return expression.ConditionBuilder{}, errors.New("unsupported cmp type") return expression.ConditionBuilder{}, errors.New("unsupported cmp type")
} }
func (a irKeyFieldCmp) calcQueryForQuery() (expression.KeyConditionBuilder, error) { func (a irKeyFieldCmp) calcQueryForQuery() (expression.KeyConditionBuilder, error) {
keyName := a.name.keyName() keyName := a.name.keyName()
vb := a.value.goValue() vb := a.value.exprValue()
switch a.cmpType { switch a.cmpType {
case cmpTypeLt: case cmpTypeLt:
return expression.Key(keyName).LessThan(expression.Value(vb)), nil return expression.Key(keyName).LessThan(buildExpressionFromValue(vb)), nil
case cmpTypeLe: case cmpTypeLe:
return expression.Key(keyName).LessThanEqual(expression.Value(vb)), nil return expression.Key(keyName).LessThanEqual(buildExpressionFromValue(vb)), nil
case cmpTypeGt: case cmpTypeGt:
return expression.Key(keyName).GreaterThan(expression.Value(vb)), nil return expression.Key(keyName).GreaterThan(buildExpressionFromValue(vb)), nil
case cmpTypeGe: 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") return expression.KeyConditionBuilder{}, errors.New("unsupported cmp type")
} }

View file

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

View file

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

View file

@ -2,9 +2,8 @@ package queryexpr
import ( import (
"github.com/aws/aws-sdk-go-v2/feature/dynamodb/expression" "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/dynamo-browse/models"
"github.com/lmika/audax/internal/dynamo-browse/models" "github.com/lmika/dynamo-browse/internal/dynamo-browse/models/attrutils"
"github.com/lmika/audax/internal/dynamo-browse/models/attrutils"
"github.com/pkg/errors" "github.com/pkg/errors"
"strings" "strings"
) )
@ -59,7 +58,7 @@ func (a *astEqualityOp) evalToIR(ctx *evalContext, info *models.TableInfo) (irAt
return nil, errors.Errorf("unrecognised operator: %v", a.Op) 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) left, err := a.Ref.evalItem(ctx, item)
if err != nil { if err != nil {
return nil, err return nil, err
@ -76,28 +75,28 @@ func (a *astEqualityOp) evalItem(ctx *evalContext, item models.Item) (types.Attr
switch a.Op { switch a.Op {
case "=": case "=":
cmp, isComparable := attrutils.CompareScalarAttributes(left, right) cmp, isComparable := attrutils.CompareScalarAttributes(left.asAttributeValue(), right.asAttributeValue())
if !isComparable { 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 "!=": case "!=":
cmp, isComparable := attrutils.CompareScalarAttributes(left, right) cmp, isComparable := attrutils.CompareScalarAttributes(left.asAttributeValue(), right.asAttributeValue())
if !isComparable { 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 "^=": case "^=":
strValue, isStrValue := right.(*types.AttributeValueMemberS) strValue, isStrValue := right.(stringableExprValue)
if !isStrValue { if !isStrValue {
return nil, errors.New("operand '^=' must be string") return nil, errors.New("operand '^=' must be string")
} }
leftAsStr, canBeString := attrutils.AttributeToString(left) leftAsStr, canBeString := left.(stringableExprValue)
if !canBeString { 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) 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) 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 != "" { if a.Op != "" {
return PathNotSettableError{} return PathNotSettableError{}
} }
@ -157,8 +156,8 @@ func (a irKeyFieldEq) calcQueryForScan(info *models.TableInfo) (expression.Condi
} }
func (a irKeyFieldEq) calcQueryForQuery() (expression.KeyConditionBuilder, error) { func (a irKeyFieldEq) calcQueryForQuery() (expression.KeyConditionBuilder, error) {
vb := a.value.goValue() vb := a.value.exprValue()
return expression.Key(a.name.keyName()).Equal(expression.Value(vb)), nil return expression.Key(a.name.keyName()).Equal(buildExpressionFromValue(vb)), nil
} }
type irGenericEq struct { type irGenericEq struct {
@ -203,21 +202,21 @@ func (a irFieldBeginsWith) canBeExecutedAsQuery(qci *queryCalcInfo) bool {
func (a irFieldBeginsWith) calcQueryForScan(info *models.TableInfo) (expression.ConditionBuilder, error) { func (a irFieldBeginsWith) calcQueryForScan(info *models.TableInfo) (expression.ConditionBuilder, error) {
nb := a.name.calcName(info) nb := a.name.calcName(info)
vb := a.value.goValue() vb := a.value.exprValue()
strValue, isStrValue := vb.(string) strValue, isStrValue := vb.(stringableExprValue)
if !isStrValue { if !isStrValue {
return expression.ConditionBuilder{}, errors.New("operand '^=' must be string") 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) { func (a irFieldBeginsWith) calcQueryForQuery() (expression.KeyConditionBuilder, error) {
vb := a.value.goValue() vb := a.value.exprValue()
strValue, isStrValue := vb.(string) strValue, isStrValue := vb.(stringableExprValue)
if !isStrValue { if !isStrValue {
return expression.KeyConditionBuilder{}, errors.New("operand '^=' must be string") 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 ( import (
"fmt" "fmt"
"github.com/aws/aws-sdk-go-v2/service/dynamodb/types" "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"
"github.com/lmika/audax/internal/dynamo-browse/models/itemrender" "github.com/lmika/dynamo-browse/internal/dynamo-browse/models/itemrender"
"strings" "strings"
) )
@ -69,6 +69,10 @@ type ValueNotConvertableToString struct {
func (n ValueNotConvertableToString) Error() string { func (n ValueNotConvertableToString) Error() string {
render := itemrender.ToRenderer(n.Val) 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()) 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 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 { type InvalidArgumentNumberError struct {
Name string Name string
Expected int 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) 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 { type UnrecognisedFunctionError struct {
Name string Name string
} }
@ -137,3 +159,20 @@ type ValueNotUsableAsASubref struct {
func (e ValueNotUsableAsASubref) Error() string { func (e ValueNotUsableAsASubref) Error() string {
return "value cannot be used as a subref" 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 ( import (
"bytes" "bytes"
"encoding/gob" "encoding/gob"
"hash/fnv"
"io"
"github.com/aws/aws-sdk-go-v2/service/dynamodb/types" "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/lmika/audax/internal/dynamo-browse/models/attrcodec" "github.com/lmika/dynamo-browse/internal/dynamo-browse/models/attrcodec"
"github.com/lmika/audax/internal/dynamo-browse/models/attrutils" "github.com/lmika/dynamo-browse/internal/dynamo-browse/models/attrutils"
"github.com/pkg/errors" "github.com/pkg/errors"
"golang.org/x/exp/maps" "golang.org/x/exp/maps"
"golang.org/x/exp/slices" "golang.org/x/exp/slices"
"hash/fnv"
"io"
) )
type QueryExpr struct { type QueryExpr struct {
ast *astExpr ast *astExpr
names map[string]string index string
values map[string]types.AttributeValue names map[string]string
values map[string]types.AttributeValue
currentResultSet *models.ResultSet
// tests fields only
timeSource timeSource
} }
type serializedExpr struct { type serializedExpr struct {
Expr string Expr string
Index string
Names map[string]string Names map[string]string
Values []byte Values []byte
} }
@ -39,6 +46,7 @@ func DeserializeFrom(r io.Reader) (*QueryExpr, error) {
} }
qe.names = se.Names qe.names = se.Names
qe.index = se.Index
if len(se.Values) > 0 { if len(se.Values) > 0 {
vals, err := attrcodec.NewDecoder(bytes.NewReader(se.Values)).Decode() 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 { 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 { if md.values != nil {
var bts bytes.Buffer var bts bytes.Buffer
if err := attrcodec.NewEncoder(&bts).Encode(&types.AttributeValueMemberM{Value: md.values}); err != nil { 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() && return md.ast.String() == other.ast.String() &&
md.index == other.index &&
maps.Equal(md.names, other.names) && maps.Equal(md.names, other.names) &&
maps.EqualFunc(md.values, md.values, attrutils.Equals) maps.EqualFunc(md.values, md.values, attrutils.Equals)
} }
@ -104,6 +113,7 @@ func (md *QueryExpr) HashCode() uint64 {
h := fnv.New64a() h := fnv.New64a()
h.Write([]byte(md.ast.String())) h.Write([]byte(md.ast.String()))
h.Write([]byte(md.index))
// the names must be in sorted order to maintain consistant key ordering // the names must be in sorted order to maintain consistant key ordering
if len(md.names) > 0 { if len(md.names) > 0 {
@ -133,9 +143,11 @@ func (md *QueryExpr) HashCode() uint64 {
func (md *QueryExpr) WithNameParams(value map[string]string) *QueryExpr { func (md *QueryExpr) WithNameParams(value map[string]string) *QueryExpr {
return &QueryExpr{ return &QueryExpr{
ast: md.ast, ast: md.ast,
names: value, index: md.index,
values: md.values, 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 { func (md *QueryExpr) WithValueParams(value map[string]types.AttributeValue) *QueryExpr {
return &QueryExpr{ return &QueryExpr{
ast: md.ast, ast: md.ast,
names: md.names, index: md.index,
values: value, 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) { 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) { 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 { 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 { 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 { func (md *QueryExpr) IsModifiablePath(item models.Item) bool {
@ -187,6 +232,7 @@ func (md *QueryExpr) evalContext() *evalContext {
return &evalContext{ return &evalContext{
namePlaceholders: md.names, namePlaceholders: md.names,
valuePlaceholders: md.values, valuePlaceholders: md.values,
ctxResultSet: md.currentResultSet,
} }
} }
@ -237,6 +283,8 @@ type evalContext struct {
nameLookup func(string) (string, bool) nameLookup func(string) (string, bool)
valuePlaceholders map[string]types.AttributeValue valuePlaceholders map[string]types.AttributeValue
valueLookup func(string) (types.AttributeValue, bool) valueLookup func(string) (types.AttributeValue, bool)
timeSource timeSource
ctxResultSet *models.ResultSet
} }
func (ec *evalContext) lookupName(name string) (string, bool) { func (ec *evalContext) lookupName(name string) (string, bool) {
@ -264,3 +312,10 @@ func (ec *evalContext) lookupValue(name string) (types.AttributeValue, bool) {
return nil, false 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 ( import (
"bytes" "bytes"
"fmt" "fmt"
"testing"
"time"
"github.com/aws/aws-sdk-go-v2/aws" "github.com/aws/aws-sdk-go-v2/aws"
"github.com/aws/aws-sdk-go-v2/service/dynamodb/types" "github.com/aws/aws-sdk-go-v2/service/dynamodb/types"
"github.com/lmika/audax/internal/dynamo-browse/models/queryexpr" "github.com/lmika/dynamo-browse/internal/dynamo-browse/models/queryexpr"
"testing"
"github.com/lmika/audax/internal/dynamo-browse/models" "github.com/lmika/dynamo-browse/internal/dynamo-browse/models"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
) )
@ -34,6 +36,13 @@ func TestModExpr_Query(t *testing.T) {
SortKey: "sk", 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`, `#0 = :0`,
exprNameIsString(0, 0, "pk", "prefix"), 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", scanCase("when request pk is fixed in parens #1",
`(pk="prefix")`, `(pk="prefix")`,
`#0 = :0`, `#0 = :0`,
@ -112,6 +126,13 @@ func TestModExpr_Query(t *testing.T) {
exprNameIsString(0, 0, "pk", "prefix"), exprNameIsString(0, 0, "pk", "prefix"),
exprNameIsNumber(1, 1, "sk", "100"), 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", scanCase("with placeholders",
`:partition=$valuePrefix and :sort=$valueAnother`, `:partition=$valuePrefix and :sort=$valueAnother`,
@ -149,6 +170,13 @@ func TestModExpr_Query(t *testing.T) {
exprNameIsString(0, 0, "color", "yellow"), exprNameIsString(0, 0, "color", "yellow"),
exprNameIsString(1, 1, "shade", "dark"), 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 { for _, scenario := range scenarios {
@ -238,6 +266,13 @@ func TestModExpr_Query(t *testing.T) {
exprNameIsString(0, 0, "pk", "prefix"), 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")`, scanCase("with in", `pk in ("alpha", "bravo", "charlie")`,
`#0 IN (:0, :1, :2)`, `#0 IN (:0, :1, :2)`,
exprName(0, "pk"), 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) { func TestQueryExpr_EvalItem(t *testing.T) {
@ -395,7 +477,9 @@ func TestQueryExpr_EvalItem(t *testing.T) {
&types.AttributeValueMemberN{Value: "7"}, &types.AttributeValueMemberN{Value: "7"},
}, },
}, },
"one": &types.AttributeValueMemberN{Value: "1"},
"three": &types.AttributeValueMemberN{Value: "3"}, "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}},
{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 // In
{expr: "three in (2, 3, 4, 5)", expected: &types.AttributeValueMemberBOOL{Value: true}}, {expr: "three in (2, 3, 4, 5)", expected: &types.AttributeValueMemberBOOL{Value: true}},
{expr: "three in (20, 30, 40)", expected: &types.AttributeValueMemberBOOL{Value: false}}, {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) { t.Run("unparsed expression", func(t *testing.T) {
scenarios := []struct { scenarios := []struct {
expr string expr string
@ -532,6 +674,10 @@ func TestQueryExpr_EvalItem(t *testing.T) {
}{ }{
{expr: `alpha.bravo`, expectedError: queryexpr.ValueNotAMapError([]string{"alpha", "bravo"})}, {expr: `alpha.bravo`, expectedError: queryexpr.ValueNotAMapError([]string{"alpha", "bravo"})},
{expr: `charlie.tree.bla`, expectedError: queryexpr.ValueNotAMapError([]string{"charlie", "tree", "bla"})}, {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 { 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 ( import (
"context" "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" "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) { 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 return nil, err
} }
// TODO: do this properly // Special handling of functions that have IR nodes
switch nameIr.keyName() { switch nameIr.keyName() {
case "size": case "size":
if len(irNodes) != 1 { 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 nil, OperandNotANameError(a.Args[0].String())
} }
return irSizeFn{name}, nil 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 { if !a.IsCall {
return a.Caller.evalItem(ctx, item) 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} 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) return a.evalItem(ctx, item)
}) })
if err != nil { if err != nil {
return nil, err 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 { 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) 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? // TODO: Should a function vall return an item?
if a.IsCall { if a.IsCall {
return PathNotSettableError{} return PathNotSettableError{}
@ -147,3 +163,15 @@ func (i irRangeFn) calcGoValues(info *models.TableInfo) ([]any, error) {
} }
return xs, nil 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 package queryexpr
import ( import (
"bytes"
"fmt"
"github.com/aws/aws-sdk-go-v2/feature/dynamodb/expression" "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/sliceutils"
"github.com/lmika/audax/internal/common/sliceutils" "github.com/lmika/dynamo-browse/internal/dynamo-browse/models"
"github.com/lmika/audax/internal/dynamo-browse/models" "github.com/lmika/dynamo-browse/internal/dynamo-browse/models/attrutils"
"github.com/lmika/audax/internal/dynamo-browse/models/attrutils"
"github.com/pkg/errors" "github.com/pkg/errors"
"strings" "strings"
) )
@ -71,6 +68,13 @@ func (a *astIn) evalToIR(ctx *evalContext, info *models.TableInfo) (irAtom, erro
return nil, OperandNotANameError(a.Ref.String()) return nil, OperandNotANameError(a.Ref.String())
} }
ir = irContains{needle: lit, haystack: t} 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: case oprIRAtom:
nameIR, isNameIR := leftIR.(irNamePath) nameIR, isNameIR := leftIR.(irNamePath)
if !isNameIR { if !isNameIR {
@ -78,13 +82,6 @@ func (a *astIn) evalToIR(ctx *evalContext, info *models.TableInfo) (irAtom, erro
} }
ir = irIn{name: nameIR, values: []oprIRAtom{t}} 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: default:
return nil, OperandNotAnOperandError{} return nil, OperandNotAnOperandError{}
} }
@ -96,7 +93,7 @@ func (a *astIn) evalToIR(ctx *evalContext, info *models.TableInfo) (irAtom, erro
return ir, nil 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) val, err := a.Ref.evalItem(ctx, item)
if err != nil { if err != nil {
return nil, err return nil, err
@ -112,14 +109,15 @@ func (a *astIn) evalItem(ctx *evalContext, item models.Item) (types.AttributeVal
if err != nil { if err != nil {
return nil, err return nil, err
} }
cmp, isComparable := attrutils.CompareScalarAttributes(val, evalOp) // TODO: use native types here
cmp, isComparable := attrutils.CompareScalarAttributes(val.asAttributeValue(), evalOp.asAttributeValue())
if !isComparable { if !isComparable {
continue continue
} else if cmp == 0 { } 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: case a.SingleOperand != nil:
evalOp, err := a.SingleOperand.evalItem(ctx, item) evalOp, err := a.SingleOperand.evalItem(ctx, item)
if err != nil { if err != nil {
@ -127,69 +125,38 @@ func (a *astIn) evalItem(ctx *evalContext, item models.Item) (types.AttributeVal
} }
switch t := evalOp.(type) { switch t := evalOp.(type) {
case *types.AttributeValueMemberS: case stringableExprValue:
str, canToStr := attrutils.AttributeToString(val) str, canToStr := val.(stringableExprValue)
if !canToStr { if !canToStr {
return &types.AttributeValueMemberBOOL{Value: false}, nil return boolExprValue(false), nil
} }
return &types.AttributeValueMemberBOOL{Value: strings.Contains(t.Value, str)}, nil return boolExprValue(strings.Contains(t.asString(), str.asString())), nil
case *types.AttributeValueMemberL: case slicableExprValue:
for _, listItem := range t.Value { for i := 0; i < t.len(); i++ {
cmp, isComparable := attrutils.CompareScalarAttributes(val, listItem) 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 { if !isComparable {
continue continue
} else if cmp == 0 { } 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 *types.AttributeValueMemberSS: case mappableExprValue:
str, canToStr := attrutils.AttributeToString(val) str, canToStr := val.(stringableExprValue)
if !canToStr { if !canToStr {
return &types.AttributeValueMemberBOOL{Value: false}, nil return boolExprValue(false), nil
} }
hasKey := t.hasKey(str.asString())
for _, listItem := range t.Value { return boolExprValue(hasKey), nil
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
} }
return nil, ValuesNotInnableError{Val: evalOp} return nil, ValuesNotInnableError{Val: evalOp.asAttributeValue()}
} }
return nil, errors.New("internal error: unhandled 'in' case") 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) 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 { if len(a.Operand) != 0 || a.SingleOperand != nil {
return PathNotSettableError{} return PathNotSettableError{}
} }
@ -263,19 +230,38 @@ func (i irIn) calcQueryForScan(info *models.TableInfo) (expression.ConditionBuil
type irLiteralValues struct { type irLiteralValues struct {
name nameIRAtom name nameIRAtom
values multiValueIRAtom values valueIRAtom
} }
func (i irLiteralValues) calcQueryForScan(info *models.TableInfo) (expression.ConditionBuilder, error) { func (iv irLiteralValues) calcQueryForScan(info *models.TableInfo) (expression.ConditionBuilder, error) {
vals, err := i.values.calcGoValues(info) if sliceable, isSliceable := iv.values.exprValue().(slicableExprValue); isSliceable {
if err != nil { if sliceable.len() == 1 {
return expression.ConditionBuilder{}, err 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 iv.name.calcName(info).In(buildExpressionFromValue(iv.values.exprValue())), nil
return expression.Value(t)
})
return i.name.calcName(info).In(oprValues[0], oprValues[1:]...), nil
} }
type irContains struct { type irContains struct {
@ -284,8 +270,11 @@ type irContains struct {
} }
func (i irContains) calcQueryForScan(info *models.TableInfo) (expression.ConditionBuilder, error) { func (i irContains) calcQueryForScan(info *models.TableInfo) (expression.ConditionBuilder, error) {
needle := i.needle.goValue() strNeedle, isString := i.needle.exprValue().(stringableExprValue)
haystack := i.haystack.calcName(info) 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 ( import (
"github.com/aws/aws-sdk-go-v2/feature/dynamodb/expression" "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 // TO DELETE = operandFieldName() string
@ -36,11 +36,7 @@ type nameIRAtom interface {
type valueIRAtom interface { type valueIRAtom interface {
oprIRAtom oprIRAtom
goValue() any exprValue() exprValue
}
type multiValueIRAtom interface {
calcGoValues(info *models.TableInfo) ([]any, error)
} }
func canExecuteAsQuery(ir irAtom, qci *queryCalcInfo) bool { func canExecuteAsQuery(ir irAtom, qci *queryCalcInfo) bool {

View file

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

View file

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

View file

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

View file

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

View file

@ -1,20 +1,67 @@
package models package models
import ( import (
"github.com/lmika/audax/internal/dynamo-browse/models/attrutils" "github.com/lmika/dynamo-browse/internal/dynamo-browse/models/attrutils"
"sort" "sort"
) )
// sortedItems is a collection of items that is sorted. // sortedItems is a collection of items that is sorted.
// Items are sorted based on the PK, and SK in ascending order // Items are sorted based on the PK, and SK in ascending order
type sortedItems struct { type sortedItems struct {
tableInfo *TableInfo criteria SortCriteria
items []Item 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 // Sort sorts the items in place
func Sort(items []Item, tableInfo *TableInfo) { func Sort(items []Item, criteria SortCriteria) {
si := sortedItems{items: items, tableInfo: tableInfo} si := sortedItems{items: items, criteria: criteria}
sort.Sort(&si) sort.Sort(&si)
} }
@ -23,30 +70,21 @@ func (si *sortedItems) Len() int {
} }
func (si *sortedItems) Less(i, j int) bool { func (si *sortedItems) Less(i, j int) bool {
// Compare primary keys for _, field := range si.criteria.Fields {
pv1, pv2 := si.items[i][si.tableInfo.Keys.PartitionKey], si.items[j][si.tableInfo.Keys.PartitionKey] // Compare primary keys
pc, ok := attrutils.CompareScalarAttributes(pv1, pv2) pv1, pv2 := field.Field.EvaluateForItem(si.items[i]), field.Field.EvaluateForItem(si.items[j])
if !ok { pc, ok := attrutils.CompareScalarAttributes(pv1, pv2)
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)
if !ok { if !ok {
return i < j return i < j
} }
if sc < 0 { if !field.Asc {
pc = -pc
}
if pc < 0 {
return true return true
} else if sc > 0 { } else if pc > 0 {
return false return false
} }
} }

View file

@ -4,7 +4,7 @@ import (
"testing" "testing"
"github.com/aws/aws-sdk-go-v2/service/dynamodb/types" "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" "github.com/stretchr/testify/assert"
) )
@ -15,7 +15,7 @@ func TestSort(t *testing.T) {
items := make([]models.Item, len(testStringData)) items := make([]models.Item, len(testStringData))
copy(items, 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[0], testStringData[1])
assert.Equal(t, items[1], testStringData[2]) assert.Equal(t, items[1], testStringData[2])
@ -28,7 +28,7 @@ func TestSort(t *testing.T) {
items := make([]models.Item, len(testNumberData)) items := make([]models.Item, len(testNumberData))
copy(items, 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[0], testNumberData[2])
assert.Equal(t, items[1], testNumberData[1]) assert.Equal(t, items[1], testNumberData[1])
@ -41,7 +41,7 @@ func TestSort(t *testing.T) {
items := make([]models.Item, len(testBoolData)) items := make([]models.Item, len(testBoolData))
copy(items, 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[0], testBoolData[2])
assert.Equal(t, items[1], testBoolData[1]) 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/feature/dynamodb/expression"
"github.com/aws/aws-sdk-go-v2/service/dynamodb" "github.com/aws/aws-sdk-go-v2/service/dynamodb"
"github.com/aws/aws-sdk-go-v2/service/dynamodb/types" "github.com/aws/aws-sdk-go-v2/service/dynamodb/types"
"github.com/lmika/audax/internal/common/sliceutils" "github.com/lmika/dynamo-browse/internal/common/sliceutils"
"github.com/lmika/audax/internal/dynamo-browse/models" "github.com/lmika/dynamo-browse/internal/dynamo-browse/models"
"github.com/lmika/audax/internal/dynamo-browse/services/jobs" "github.com/lmika/dynamo-browse/internal/dynamo-browse/services/jobs"
"github.com/pkg/errors" "github.com/pkg/errors"
"time" "time"
) )

View file

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

View file

@ -3,8 +3,8 @@ package inputhistorystore
import ( import (
"context" "context"
"github.com/asdine/storm" "github.com/asdine/storm"
"github.com/lmika/audax/internal/common/sliceutils" "github.com/lmika/dynamo-browse/internal/common/sliceutils"
"github.com/lmika/audax/internal/common/workspaces" "github.com/lmika/dynamo-browse/internal/common/workspaces"
"github.com/pkg/errors" "github.com/pkg/errors"
"sort" "sort"
"time" "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 ( import (
"github.com/asdine/storm" "github.com/asdine/storm"
"github.com/lmika/audax/internal/common/workspaces" "github.com/lmika/dynamo-browse/internal/common/workspaces"
"github.com/pkg/errors" "github.com/pkg/errors"
"io/fs" "io/fs"
"log" "log"
@ -113,7 +113,7 @@ func (c *SettingStore) SetDefaultLimit(limit int) error {
func (c *SettingStore) getStringValue(key string, def string) (string, error) { func (c *SettingStore) getStringValue(key string, def string) (string, error) {
var val string 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) { if errors.Is(err, storm.ErrNotFound) {
return def, nil return def, nil
} }

View file

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

View file

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

View file

@ -2,8 +2,8 @@ package itemrenderer
import ( import (
"fmt" "fmt"
"github.com/lmika/audax/internal/dynamo-browse/models" "github.com/lmika/dynamo-browse/internal/dynamo-browse/models"
"github.com/lmika/audax/internal/dynamo-browse/models/itemrender" "github.com/lmika/dynamo-browse/internal/dynamo-browse/models/itemrender"
"io" "io"
"text/tabwriter" "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 ( import (
"context" "context"
"github.com/cloudcmds/tamarin/object" "fmt"
"log" "log"
"github.com/pkg/errors"
"github.com/risor-io/risor/object"
) )
func printBuiltin(ctx context.Context, args ...object.Object) object.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...) log.Printf("%s "+format, values...)
return object.Nil 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 ( import (
"context" "context"
"github.com/aws/aws-sdk-go-v2/service/dynamodb/types" "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 //go:generate mockery --with-expecter --name UIService
@ -32,6 +32,7 @@ type SessionService interface {
type QueryOptions struct { type QueryOptions struct {
TableName string TableName string
IndexName string
NamePlaceholders map[string]string NamePlaceholders map[string]string
ValuePlaceholders map[string]types.AttributeValue ValuePlaceholders map[string]types.AttributeValue
} }

View file

@ -5,10 +5,10 @@ package mocks
import ( import (
context "context" 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" 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 // SessionService is an autogenerated mock type for the SessionService type

View file

@ -3,11 +3,13 @@ package scriptmanager
import ( import (
"context" "context"
"fmt" "fmt"
"github.com/cloudcmds/tamarin/arg"
"github.com/cloudcmds/tamarin/object"
"github.com/cloudcmds/tamarin/scope"
"github.com/pkg/errors"
"regexp" "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 ( var (
@ -18,22 +20,18 @@ type extModule struct {
scriptPlugin *ScriptPlugin scriptPlugin *ScriptPlugin
} }
func (m *extModule) register(scp *scope.Scope) { func (m *extModule) register() *object.Module {
modScope := scope.New(scope.Opts{}) return object.NewBuiltinsModule("ext", map[string]object.Object{
mod := object.NewModule("ext", modScope) "command": object.NewBuiltin("command", m.command),
"key_binding": object.NewBuiltin("key_binding", m.keyBinding),
modScope.AddBuiltins([]*object.Builtin{ "related_items": object.NewBuiltin("related_items", m.relatedItem),
object.NewBuiltin("command", m.command, mod),
object.NewBuiltin("key_binding", m.keyBinding, mod),
}) })
scp.Declare("ext", mod, true)
} }
func (m *extModule) command(ctx context.Context, args ...object.Object) object.Object { func (m *extModule) command(ctx context.Context, args ...object.Object) object.Object {
thisEnv := scriptEnvFromCtx(ctx) thisEnv := scriptEnvFromCtx(ctx)
if err := arg.Require("ext.command", 2, args); err != nil { if err := require("ext.command", 2, args); err != nil {
return err return err
} }
@ -59,11 +57,12 @@ func (m *extModule) command(ctx context.Context, args ...object.Object) object.O
} }
newEnv := thisEnv newEnv := thisEnv
newEnv.options = m.scriptPlugin.scriptService.options
ctx = ctxWithScriptEnv(ctx, newEnv) ctx = ctxWithScriptEnv(ctx, newEnv)
res := callFn(ctx, fnRes.Scope(), fnRes, objArgs) res, err := callFn(ctx, fnRes, objArgs)
if object.IsError(res) { 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) errObj := res.(*object.Error)
return errors.Errorf("command error '%v':%v - %v", m.scriptPlugin.name, cmdName, errObj.Inspect()) 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 { func (m *extModule) keyBinding(ctx context.Context, args ...object.Object) object.Object {
thisEnv := scriptEnvFromCtx(ctx) 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 return err
} }
@ -119,11 +118,12 @@ func (m *extModule) keyBinding(ctx context.Context, args ...object.Object) objec
} }
newEnv := thisEnv newEnv := thisEnv
newEnv.options = m.scriptPlugin.scriptService.options
ctx = ctxWithScriptEnv(ctx, newEnv) ctx = ctxWithScriptEnv(ctx, newEnv)
res := callFn(ctx, fnRes.Scope(), fnRes, objArgs) res, err := callFn(ctx, fnRes, objArgs)
if object.IsError(res) { 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) errObj := res.(*object.Error)
return errors.Errorf("command error '%v':%v - %v", m.scriptPlugin.name, bindingName, errObj.Inspect()) 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 m.scriptPlugin.keyToKeyBinding[defaultKey] = fullBindingName
return nil 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 ( import (
"context" "context"
"github.com/lmika/audax/internal/dynamo-browse/services/scriptmanager" "github.com/lmika/dynamo-browse/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/mocks"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
"github.com/stretchr/testify/mock" "github.com/stretchr/testify/mock"
"testing" "testing"
@ -15,49 +15,16 @@ func TestOSModule_Env(t *testing.T) {
t.Setenv("EMPTY_VALUE", "") t.Setenv("EMPTY_VALUE", "")
testFS := testScriptFile(t, "test.tm", ` testFS := testScriptFile(t, "test.tm", `
assert(os.env("FULL_VALUE") == "this is a value") assert(os.getenv("FULL_VALUE") == "this is a value")
assert(os.env("EMPTY_VALUE") == "") assert(os.getenv("EMPTY_VALUE") == "")
assert(os.env("MISSING_VALUE") == nil) assert(os.getenv("MISSING_VALUE") == "")
assert(bool(os.env("FULL_VALUE")) == true) assert(bool(os.getenv("FULL_VALUE")) == true)
assert(bool(os.env("EMPTY_VALUE")) == false) assert(bool(os.getenv("EMPTY_VALUE")) == false)
assert(bool(os.env("MISSING_VALUE")) == false) assert(bool(os.getenv("MISSING_VALUE")) == false)
`) `)
srv := scriptmanager.New(scriptmanager.WithFS(testFS)) 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() ctx := context.Background()
err := <-srv.RunAdHocScript(ctx, "test.tm") err := <-srv.RunAdHocScript(ctx, "test.tm")
@ -68,22 +35,14 @@ func TestOSModule_Env(t *testing.T) {
func TestOSModule_Exec(t *testing.T) { func TestOSModule_Exec(t *testing.T) {
t.Run("should run command and return stdout", func(t *testing.T) { t.Run("should run command and return stdout", func(t *testing.T) {
mockedUIService := mocks.NewUIService(t) mockedUIService := mocks.NewUIService(t)
mockedUIService.EXPECT().PrintMessage(mock.Anything, "false")
mockedUIService.EXPECT().PrintMessage(mock.Anything, "hello world\n") mockedUIService.EXPECT().PrintMessage(mock.Anything, "hello world\n")
testFS := testScriptFile(t, "test.tm", ` testFS := testScriptFile(t, "test.tm", `
res := os.exec('echo "hello world"') res := exec('echo', ["hello world"]).stdout
ui.print(res.is_err()) ui.print(res)
ui.print(res.unwrap())
`) `)
srv := scriptmanager.New(scriptmanager.WithFS(testFS)) srv := scriptmanager.New(scriptmanager.WithFS(testFS))
srv.SetDefaultOptions(scriptmanager.Options{
OSExecShell: "/bin/bash",
Permissions: scriptmanager.Permissions{
AllowShellCommands: true,
},
})
srv.SetIFaces(scriptmanager.Ifaces{ srv.SetIFaces(scriptmanager.Ifaces{
UI: mockedUIService, UI: mockedUIService,
}) })
@ -94,73 +53,4 @@ func TestOSModule_Exec(t *testing.T) {
mockedUIService.AssertExpectations(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" "context"
"fmt" "fmt"
"github.com/aws/aws-sdk-go-v2/service/dynamodb/types" "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/pkg/errors"
"github.com/risor-io/risor/object"
) )
type sessionModule struct { 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 // Placeholders
if argsVal, isArgsValMap := objMap.Get("args").(*object.Map); isArgsValMap { if argsVal, isArgsValMap := objMap.Get("args").(*object.Map); isArgsValMap {
options.NamePlaceholders = make(map[string]string) 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) resp, err := um.sessionService.Query(ctx, expr, options)
if err != nil { 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 { 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 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 { 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 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 { 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 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 { 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 return err
} }
@ -131,17 +134,12 @@ func (um *sessionModule) currentTable(ctx context.Context, args ...object.Object
return &tableProxy{table: rs.TableInfo} return &tableProxy{table: rs.TableInfo}
} }
func (um *sessionModule) register(scp *scope.Scope) { func (um *sessionModule) register() *object.Module {
modScope := scope.New(scope.Opts{}) return object.NewBuiltinsModule("session", map[string]object.Object{
mod := object.NewModule("session", modScope) "query": object.NewBuiltin("query", um.query),
"current_table": object.NewBuiltin("current_table", um.currentTable),
modScope.AddBuiltins([]*object.Builtin{ "result_set": object.NewBuiltin("result_set", um.resultSet),
object.NewBuiltin("query", um.query, mod), "selected_item": object.NewBuiltin("selected_item", um.selectedItem),
object.NewBuiltin("current_table", um.currentTable, mod), "set_result_set": object.NewBuiltin("set_result_set", um.setResultSet),
object.NewBuiltin("result_set", um.resultSet, mod),
object.NewBuiltin("selected_item", um.selectedItem, mod),
object.NewBuiltin("set_result_set", um.setResultSet, mod),
}) })
scp.Declare("session", mod, true)
} }

View file

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

View file

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

View file

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

View file

@ -2,42 +2,12 @@ package scriptmanager
import ( import (
"context" "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 // scriptEnv is the runtime environment for a particular script execution
type scriptEnv struct { type scriptEnv struct {
filename string filename string
options Options
} }
type scriptEnvKeyType struct{} type scriptEnvKeyType struct{}
@ -50,5 +20,7 @@ func scriptEnvFromCtx(ctx context.Context) scriptEnv {
} }
func ctxWithScriptEnv(ctx context.Context, perms scriptEnv) context.Context { 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 ( import (
"context" "context"
"github.com/cloudcmds/tamarin/arg" "time"
"github.com/cloudcmds/tamarin/object"
"github.com/lmika/audax/internal/dynamo-browse/models" "github.com/aws/aws-sdk-go-v2/service/dynamodb/types"
"github.com/lmika/audax/internal/dynamo-browse/models/queryexpr" "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/pkg/errors"
"github.com/risor-io/risor/object"
"github.com/risor-io/risor/op"
) )
type resultSetProxy struct { type resultSetProxy struct {
resultSet *models.ResultSet 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{} { func (r *resultSetProxy) Interface() interface{} {
return r.resultSet return r.resultSet
} }
@ -95,17 +115,105 @@ func (r *resultSetProxy) GetAttr(name string) (object.Object, bool) {
return &tableProxy{table: r.resultSet.TableInfo}, true return &tableProxy{table: r.resultSet.TableInfo}, true
case "length": case "length":
return object.NewInt(int64(len(r.resultSet.Items()))), true 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 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 { type itemProxy struct {
resultSetProxy *resultSetProxy resultSetProxy *resultSetProxy
itemIndex int itemIndex int
item models.Item 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 { func newItemProxy(rs *resultSetProxy, itemIndex int) *itemProxy {
return &itemProxy{ return &itemProxy{
resultSetProxy: rs, 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 { 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 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 { 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 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 { 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 return objErr
} }

View file

@ -2,13 +2,14 @@ package scriptmanager_test
import ( import (
"context" "context"
"testing"
"github.com/aws/aws-sdk-go-v2/service/dynamodb/types" "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/lmika/audax/internal/dynamo-browse/services/scriptmanager" "github.com/lmika/dynamo-browse/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/mocks"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
"github.com/stretchr/testify/mock" "github.com/stretchr/testify/mock"
"testing"
) )
func TestResultSetProxy(t *testing.T) { func TestResultSetProxy(t *testing.T) {
@ -29,7 +30,7 @@ func TestResultSetProxy(t *testing.T) {
mockedUIService := mocks.NewUIService(t) mockedUIService := mocks.NewUIService(t)
testFS := testScriptFile(t, "test.tm", ` testFS := testScriptFile(t, "test.tm", `
res := session.query("some expr").unwrap() res := session.query("some expr")
// Test properties of the result set // Test properties of the result set
assert(res.table.name, "hello") 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) { func TestResultSetProxy_GetAttr(t *testing.T) {
t.Run("should return the value of items within a result set", func(t *testing.T) { t.Run("should return the value of items within a result set", func(t *testing.T) {
rs := &models.ResultSet{} 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) mockedSessionService.EXPECT().Query(mock.Anything, "some expr", scriptmanager.QueryOptions{}).Return(rs, nil)
testFS := testScriptFile(t, "test.tm", ` 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("pk") == "abc", "str attr")
assert(res[0].attr("sk") == 123, "num attr") assert(res[0].attr("sk") == 123, "num attr")
@ -164,7 +282,7 @@ func TestResultSetProxy_SetAttr(t *testing.T) {
mockedUIService := mocks.NewUIService(t) mockedUIService := mocks.NewUIService(t)
testFS := testScriptFile(t, "test.tm", ` 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("pk", "bla-di-bla")
res[0].set_attr("num", 123) res[0].set_attr("num", 123)
@ -215,7 +333,7 @@ func TestResultSetProxy_DeleteAttr(t *testing.T) {
mockedUIService := mocks.NewUIService(t) mockedUIService := mocks.NewUIService(t)
testFS := testScriptFile(t, "test.tm", ` testFS := testScriptFile(t, "test.tm", `
res := session.query("some expr").unwrap() res := session.query("some expr")
res[0].delete_attr("deleteMe") res[0].delete_attr("deleteMe")
session.set_result_set(res) session.set_result_set(res)
`) `)

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