Compare commits
10 commits
835ddd5630
...
e37b8099a3
Author | SHA1 | Date | |
---|---|---|---|
|
e37b8099a3 | ||
|
f5bf31a903 | ||
|
5d95d44a97 | ||
|
12909c89ee | ||
|
ceb064a346 | ||
|
7ca0cf6982 | ||
|
ed53173a1d | ||
|
20a9a8c758 | ||
|
f65c5778a9 | ||
|
4b4d515ade |
2
.github/workflows/ci.yaml
vendored
2
.github/workflows/ci.yaml
vendored
|
@ -24,7 +24,7 @@ jobs:
|
|||
- name: Setup Go
|
||||
uses: actions/setup-go@v3
|
||||
with:
|
||||
go-version: 1.18
|
||||
go-version: 1.22
|
||||
- name: Configure
|
||||
run: |
|
||||
git config --global url."https://${{ secrets.GO_MODULES_TOKEN }}:x-oauth-basic@github.com/lmika".insteadOf "https://github.com/lmika"
|
||||
|
|
10
.github/workflows/release.yaml
vendored
10
.github/workflows/release.yaml
vendored
|
@ -20,7 +20,7 @@ jobs:
|
|||
- name: Setup Go
|
||||
uses: actions/setup-go@v3
|
||||
with:
|
||||
go-version: 1.18
|
||||
go-version: 1.22
|
||||
- name: Configure
|
||||
run: |
|
||||
git config --global url."https://${{ secrets.GO_MODULES_TOKEN }}:x-oauth-basic@github.com/lmika".insteadOf "https://github.com/lmika"
|
||||
|
@ -41,7 +41,7 @@ jobs:
|
|||
- name: Setup Go
|
||||
uses: actions/setup-go@v3
|
||||
with:
|
||||
go-version: 1.18
|
||||
go-version: 1.22
|
||||
- name: Configure
|
||||
run: |
|
||||
git config --global url."https://${{ secrets.GO_MODULES_TOKEN }}:x-oauth-basic@github.com/lmika".insteadOf "https://github.com/lmika"
|
||||
|
@ -52,7 +52,7 @@ jobs:
|
|||
- name: Release
|
||||
if: startsWith(github.ref, 'refs/tags/')
|
||||
run: |
|
||||
goreleaser release -f macos.goreleaser.yml --skip-validate --rm-dist
|
||||
goreleaser release -f macos.goreleaser.yml --skip=validate --clean
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
HOMEBREW_GITHUB_TOKEN: ${{ secrets.HOMEBREW_GITHUB_TOKEN }}
|
||||
|
@ -66,7 +66,7 @@ jobs:
|
|||
- name: Setup Go
|
||||
uses: actions/setup-go@v3
|
||||
with:
|
||||
go-version: 1.18
|
||||
go-version: 1.22
|
||||
- name: Configure
|
||||
run: |
|
||||
git config --global url."https://${{ secrets.GO_MODULES_TOKEN }}:x-oauth-basic@github.com/lmika".insteadOf "https://github.com/lmika"
|
||||
|
@ -75,7 +75,7 @@ jobs:
|
|||
if: startsWith(github.ref, 'refs/tags/')
|
||||
with:
|
||||
version: latest
|
||||
args: release -f linux.goreleaser.yml --skip-validate --rm-dist
|
||||
args: release -f linux.goreleaser.yml --skip=validate --clean
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
HOMEBREW_GITHUB_TOKEN: ${{ secrets.HOMEBREW_GITHUB_TOKEN }}
|
|
@ -4,33 +4,36 @@ import (
|
|||
"context"
|
||||
"flag"
|
||||
"fmt"
|
||||
"github.com/aws/aws-sdk-go-v2/config"
|
||||
"github.com/aws/aws-sdk-go-v2/service/dynamodb"
|
||||
tea "github.com/charmbracelet/bubbletea"
|
||||
"github.com/lmika/audax/internal/common/ui/commandctrl"
|
||||
"github.com/lmika/audax/internal/common/ui/logging"
|
||||
"github.com/lmika/audax/internal/common/ui/osstyle"
|
||||
"github.com/lmika/audax/internal/common/workspaces"
|
||||
"github.com/lmika/audax/internal/dynamo-browse/controllers"
|
||||
"github.com/lmika/audax/internal/dynamo-browse/providers/dynamo"
|
||||
"github.com/lmika/audax/internal/dynamo-browse/providers/inputhistorystore"
|
||||
"github.com/lmika/audax/internal/dynamo-browse/providers/settingstore"
|
||||
"github.com/lmika/audax/internal/dynamo-browse/providers/workspacestore"
|
||||
"github.com/lmika/audax/internal/dynamo-browse/services/inputhistory"
|
||||
"github.com/lmika/audax/internal/dynamo-browse/services/itemrenderer"
|
||||
"github.com/lmika/audax/internal/dynamo-browse/services/jobs"
|
||||
keybindings_service "github.com/lmika/audax/internal/dynamo-browse/services/keybindings"
|
||||
"github.com/lmika/audax/internal/dynamo-browse/services/scriptmanager"
|
||||
"github.com/lmika/audax/internal/dynamo-browse/services/tables"
|
||||
"github.com/lmika/audax/internal/dynamo-browse/services/viewsnapshot"
|
||||
"github.com/lmika/audax/internal/dynamo-browse/ui"
|
||||
"github.com/lmika/audax/internal/dynamo-browse/ui/keybindings"
|
||||
"github.com/lmika/audax/internal/dynamo-browse/ui/teamodels/styles"
|
||||
bus "github.com/lmika/events"
|
||||
"github.com/lmika/gopkgs/cli"
|
||||
"log"
|
||||
"net"
|
||||
"os"
|
||||
|
||||
"github.com/aws/aws-sdk-go-v2/config"
|
||||
"github.com/aws/aws-sdk-go-v2/service/dynamodb"
|
||||
tea "github.com/charmbracelet/bubbletea"
|
||||
"github.com/lmika/dynamo-browse/internal/common/ui/commandctrl"
|
||||
"github.com/lmika/dynamo-browse/internal/common/ui/logging"
|
||||
"github.com/lmika/dynamo-browse/internal/common/ui/osstyle"
|
||||
"github.com/lmika/dynamo-browse/internal/common/workspaces"
|
||||
"github.com/lmika/dynamo-browse/internal/dynamo-browse/controllers"
|
||||
"github.com/lmika/dynamo-browse/internal/dynamo-browse/models/queryexpr"
|
||||
"github.com/lmika/dynamo-browse/internal/dynamo-browse/providers/dynamo"
|
||||
"github.com/lmika/dynamo-browse/internal/dynamo-browse/providers/inputhistorystore"
|
||||
"github.com/lmika/dynamo-browse/internal/dynamo-browse/providers/pasteboardprovider"
|
||||
"github.com/lmika/dynamo-browse/internal/dynamo-browse/providers/settingstore"
|
||||
"github.com/lmika/dynamo-browse/internal/dynamo-browse/providers/workspacestore"
|
||||
"github.com/lmika/dynamo-browse/internal/dynamo-browse/services/inputhistory"
|
||||
"github.com/lmika/dynamo-browse/internal/dynamo-browse/services/itemrenderer"
|
||||
"github.com/lmika/dynamo-browse/internal/dynamo-browse/services/jobs"
|
||||
keybindings_service "github.com/lmika/dynamo-browse/internal/dynamo-browse/services/keybindings"
|
||||
"github.com/lmika/dynamo-browse/internal/dynamo-browse/services/scriptmanager"
|
||||
"github.com/lmika/dynamo-browse/internal/dynamo-browse/services/tables"
|
||||
"github.com/lmika/dynamo-browse/internal/dynamo-browse/services/viewsnapshot"
|
||||
"github.com/lmika/dynamo-browse/internal/dynamo-browse/ui"
|
||||
"github.com/lmika/dynamo-browse/internal/dynamo-browse/ui/keybindings"
|
||||
"github.com/lmika/dynamo-browse/internal/dynamo-browse/ui/teamodels/styles"
|
||||
bus "github.com/lmika/events"
|
||||
"github.com/lmika/gopkgs/cli"
|
||||
)
|
||||
|
||||
func main() {
|
||||
|
@ -40,6 +43,7 @@ func main() {
|
|||
var flagRO = flag.Bool("ro", false, "enable readonly mode")
|
||||
var flagDefaultLimit = flag.Int("default-limit", 0, "default limit for queries and scans")
|
||||
var flagWorkspace = flag.String("w", "", "workspace file")
|
||||
var flagQuery = flag.String("q", "", "run query")
|
||||
flag.Parse()
|
||||
|
||||
ctx := context.Background()
|
||||
|
@ -84,6 +88,7 @@ func main() {
|
|||
resultSetSnapshotStore := workspacestore.NewResultSetSnapshotStore(ws)
|
||||
settingStore := settingstore.New(ws)
|
||||
inputHistoryStore := inputhistorystore.NewInputHistoryStore(ws)
|
||||
pasteboardProvider := pasteboardprovider.New()
|
||||
|
||||
if *flagRO {
|
||||
if err := settingStore.SetReadOnly(*flagRO); err != nil {
|
||||
|
@ -105,19 +110,58 @@ func main() {
|
|||
|
||||
state := controllers.NewState()
|
||||
jobsController := controllers.NewJobsController(jobsService, eventBus, false)
|
||||
tableReadController := controllers.NewTableReadController(state, tableService, workspaceService, itemRendererService, jobsController, inputHistoryService, eventBus, *flagTable)
|
||||
tableReadController := controllers.NewTableReadController(
|
||||
state,
|
||||
tableService,
|
||||
workspaceService,
|
||||
itemRendererService,
|
||||
jobsController,
|
||||
inputHistoryService,
|
||||
eventBus,
|
||||
pasteboardProvider,
|
||||
scriptManagerService,
|
||||
*flagTable,
|
||||
)
|
||||
tableWriteController := controllers.NewTableWriteController(state, tableService, jobsController, tableReadController, settingStore)
|
||||
columnsController := controllers.NewColumnsController(eventBus)
|
||||
exportController := controllers.NewExportController(state, columnsController)
|
||||
columnsController := controllers.NewColumnsController(tableReadController, eventBus)
|
||||
exportController := controllers.NewExportController(state, tableService, jobsController, columnsController, pasteboardProvider)
|
||||
settingsController := controllers.NewSettingsController(settingStore, eventBus)
|
||||
keyBindings := keybindings.Default()
|
||||
scriptController := controllers.NewScriptController(scriptManagerService, tableReadController, settingsController, eventBus)
|
||||
scriptController := controllers.NewScriptController(scriptManagerService, tableReadController, jobsController, settingsController, eventBus)
|
||||
|
||||
if *flagQuery != "" {
|
||||
if *flagTable == "" {
|
||||
cli.Fatalf("-t will need to be set for -q")
|
||||
}
|
||||
|
||||
ctx := context.Background()
|
||||
|
||||
query, err := queryexpr.Parse(*flagQuery)
|
||||
if err != nil {
|
||||
cli.Fatalf("query: %v", err)
|
||||
}
|
||||
|
||||
ti, err := tableService.Describe(ctx, *flagTable)
|
||||
if err != nil {
|
||||
cli.Fatalf("cannot describe table: %v", err)
|
||||
}
|
||||
|
||||
rs, err := tableService.ScanOrQuery(ctx, ti, query, nil)
|
||||
if err != nil {
|
||||
cli.Fatalf("cannot execute query: %v", err)
|
||||
}
|
||||
if err := exportController.ExportToWriter(os.Stdout, rs); err != nil {
|
||||
cli.Fatalf("cannot export results of query: %v", err)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
keyBindingService := keybindings_service.NewService(keyBindings)
|
||||
keyBindingController := controllers.NewKeyBindingController(keyBindingService, scriptController)
|
||||
|
||||
commandController := commandctrl.NewCommandController(inputHistoryService)
|
||||
commandController.AddCommandLookupExtension(scriptController)
|
||||
commandController.SetCommandCompletionProvider(columnsController)
|
||||
|
||||
model := ui.NewModel(
|
||||
tableReadController,
|
||||
|
@ -131,6 +175,7 @@ func main() {
|
|||
scriptController,
|
||||
eventBus,
|
||||
keyBindingController,
|
||||
pasteboardProvider,
|
||||
keyBindings,
|
||||
)
|
||||
|
||||
|
|
82
go.mod
82
go.mod
|
@ -1,17 +1,19 @@
|
|||
module github.com/lmika/audax
|
||||
module github.com/lmika/dynamo-browse
|
||||
|
||||
go 1.18
|
||||
go 1.22
|
||||
|
||||
toolchain go1.22.0
|
||||
|
||||
require (
|
||||
github.com/alecthomas/participle/v2 v2.0.0-beta.5
|
||||
github.com/asdine/storm v2.1.2+incompatible
|
||||
github.com/aws/aws-sdk-go-v2 v1.17.4
|
||||
github.com/aws/aws-sdk-go-v2/config v1.13.1
|
||||
github.com/aws/aws-sdk-go-v2/credentials v1.8.0
|
||||
github.com/aws/aws-sdk-go-v2 v1.18.1
|
||||
github.com/aws/aws-sdk-go-v2/config v1.18.27
|
||||
github.com/aws/aws-sdk-go-v2/credentials v1.13.26
|
||||
github.com/aws/aws-sdk-go-v2/feature/dynamodb/attributevalue v1.10.12
|
||||
github.com/aws/aws-sdk-go-v2/feature/dynamodb/expression v1.4.39
|
||||
github.com/aws/aws-sdk-go-v2/service/dynamodb v1.18.3
|
||||
github.com/aws/aws-sdk-go-v2/service/sqs v1.16.0
|
||||
github.com/aws/aws-sdk-go-v2/service/dynamodb v1.19.11
|
||||
github.com/aws/aws-sdk-go-v2/service/sqs v1.23.2
|
||||
github.com/aws/aws-sdk-go-v2/service/ssm v1.24.0
|
||||
github.com/brianvoe/gofakeit/v6 v6.15.0
|
||||
github.com/calyptia/go-bubble-table v0.2.1
|
||||
|
@ -27,7 +29,7 @@ require (
|
|||
github.com/muesli/ansi v0.0.0-20211031195517-c9f0611b6c70
|
||||
github.com/muesli/reflow v0.3.0
|
||||
github.com/pkg/errors v0.9.1
|
||||
github.com/stretchr/testify v1.8.1
|
||||
github.com/stretchr/testify v1.8.4
|
||||
golang.design/x/clipboard v0.6.2
|
||||
golang.org/x/exp v0.0.0-20230108222341-4b8118a2686a
|
||||
)
|
||||
|
@ -35,38 +37,68 @@ require (
|
|||
require (
|
||||
github.com/DataDog/zstd v1.5.2 // indirect
|
||||
github.com/Sereal/Sereal v0.0.0-20220220040404-e0d1e550e879 // indirect
|
||||
github.com/anthonynsimon/bild v0.13.0 // indirect
|
||||
github.com/atotto/clipboard v0.1.4 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.10.0 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/internal/configsources v1.1.28 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.4.22 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/internal/ini v1.3.5 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.4.10 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.13.4 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/internal/configsources v1.1.34 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.4.28 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/internal/ini v1.3.35 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/internal/v4a v1.0.26 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/cloudformation v1.30.0 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/cloudfront v1.26.8 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/cloudtrail v1.27.1 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/cloudwatch v1.26.2 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/dynamodbstreams v1.14.3 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/ebs v1.16.14 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/ec2 v1.102.0 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/ecr v1.18.13 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/ecs v1.27.4 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/eks v1.27.14 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/elasticache v1.27.2 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/elasticsearchservice v1.19.2 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/glue v1.52.0 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/iam v1.21.0 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.9.11 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/endpoint-discovery v1.7.22 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.7.0 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/sso v1.9.0 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/sts v1.14.0 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.1.29 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/endpoint-discovery v1.7.28 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.9.28 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.14.3 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/kinesis v1.17.14 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/kms v1.22.2 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/lambda v1.37.0 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/rds v1.46.0 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/redshift v1.28.0 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/route53 v1.28.3 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/s3 v1.36.0 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/sfn v1.18.0 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/sns v1.20.13 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/sso v1.12.12 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.14.12 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/sts v1.19.2 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/wafv2 v1.35.1 // indirect
|
||||
github.com/aws/smithy-go v1.13.5 // indirect
|
||||
github.com/aymanbagabas/go-osc52 v1.0.3 // indirect
|
||||
github.com/containerd/console v1.0.3 // indirect
|
||||
github.com/davecgh/go-spew v1.1.1 // indirect
|
||||
github.com/gofrs/uuid v4.3.1+incompatible // indirect
|
||||
github.com/gofrs/uuid v4.4.0+incompatible // indirect
|
||||
github.com/golang/protobuf v1.5.2 // indirect
|
||||
github.com/golang/snappy v0.0.4 // indirect
|
||||
github.com/hashicorp/errwrap v1.0.0 // indirect
|
||||
github.com/hashicorp/go-multierror v1.1.1 // indirect
|
||||
github.com/jackc/pgpassfile v1.0.0 // indirect
|
||||
github.com/jackc/pgservicefile v0.0.0-20200714003250-2b9c44734f2b // indirect
|
||||
github.com/jackc/pgx/v5 v5.0.4 // indirect
|
||||
github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a // indirect
|
||||
github.com/jackc/pgx/v5 v5.4.1 // indirect
|
||||
github.com/jmespath/go-jmespath v0.4.0 // indirect
|
||||
github.com/juju/ansiterm v0.0.0-20210929141451-8b71cc96ebdc // indirect
|
||||
github.com/lucasb-eyer/go-colorful v1.2.0 // indirect
|
||||
github.com/lunixbochs/vtclean v1.0.0 // indirect
|
||||
github.com/mattn/go-isatty v0.0.16 // indirect
|
||||
github.com/mattn/go-isatty v0.0.17 // indirect
|
||||
github.com/mattn/go-localereader v0.0.1 // indirect
|
||||
github.com/muesli/cancelreader v0.2.2 // indirect
|
||||
github.com/muesli/termenv v0.13.0 // indirect
|
||||
github.com/pmezard/go-difflib v1.0.0 // indirect
|
||||
github.com/risor-io/risor v1.4.0 // indirect
|
||||
github.com/rivo/uniseg v0.4.2 // indirect
|
||||
github.com/sahilm/fuzzy v0.1.0 // indirect
|
||||
github.com/stretchr/objx v0.5.0 // indirect
|
||||
|
@ -76,13 +108,13 @@ require (
|
|||
github.com/vmihailenco/msgpack v4.0.4+incompatible // indirect
|
||||
github.com/wI2L/jsondiff v0.3.0 // indirect
|
||||
go.etcd.io/bbolt v1.3.6 // indirect
|
||||
golang.org/x/crypto v0.0.0-20220829220503-c86fa9a7ed90 // indirect
|
||||
golang.org/x/crypto v0.9.0 // indirect
|
||||
golang.org/x/exp/shiny v0.0.0-20230213192124-5e25df0256eb // indirect
|
||||
golang.org/x/image v0.0.0-20211028202545-6944b10bf410 // indirect
|
||||
golang.org/x/image v0.5.0 // indirect
|
||||
golang.org/x/mobile v0.0.0-20210716004757-34ab1303b554 // indirect
|
||||
golang.org/x/sys v0.1.0 // indirect
|
||||
golang.org/x/term v0.0.0-20220919170432-7a66f970e087 // indirect
|
||||
golang.org/x/text v0.3.8 // indirect
|
||||
golang.org/x/sys v0.8.0 // indirect
|
||||
golang.org/x/term v0.8.0 // indirect
|
||||
golang.org/x/text v0.9.0 // indirect
|
||||
google.golang.org/appengine v1.6.7 // indirect
|
||||
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||
)
|
||||
|
|
151
go.sum
151
go.sum
|
@ -1,3 +1,4 @@
|
|||
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
|
||||
github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo=
|
||||
github.com/DataDog/zstd v1.5.2 h1:vUG4lAyuPCXO0TLbXvPv7EB7cNK1QV/luu55UHLrrn8=
|
||||
github.com/DataDog/zstd v1.5.2/go.mod h1:g4AWEaM3yOg3HYfnJ3YIawPnVdXJh9QME85blwSAmyw=
|
||||
|
@ -7,6 +8,9 @@ github.com/alecthomas/assert/v2 v2.0.3 h1:WKqJODfOiQG0nEJKFKzDIG3E29CN2/4zR9XGJz
|
|||
github.com/alecthomas/participle/v2 v2.0.0-beta.5 h1:y6dsSYVb1G5eK6mgmy+BgI3Mw35a3WghArZ/Hbebrjo=
|
||||
github.com/alecthomas/participle/v2 v2.0.0-beta.5/go.mod h1:RC764t6n4L8D8ITAJv0qdokritYSNR3wV5cVwmIEaMM=
|
||||
github.com/alecthomas/repr v0.1.0 h1:ENn2e1+J3k09gyj2shc0dHr/yjaWSHRlrJ4DPMevDqE=
|
||||
github.com/anthonynsimon/bild v0.13.0 h1:mN3tMaNds1wBWi1BrJq0ipDBhpkooYfu7ZFSMhXt1C8=
|
||||
github.com/anthonynsimon/bild v0.13.0/go.mod h1:tpzzp0aYkAsMi1zmfhimaDyX1xjn2OUc1AJZK/TF0AE=
|
||||
github.com/armon/consul-api v0.0.0-20180202201655-eb2c6b5be1b6/go.mod h1:grANhF5doyWs3UAsr3K4I6qtAmlQcZDesFNEHPZAzj8=
|
||||
github.com/asdine/storm v2.1.2+incompatible h1:dczuIkyqwY2LrtXPz8ixMrU/OFgZp71kbKTHGrXYt/Q=
|
||||
github.com/asdine/storm v2.1.2+incompatible/go.mod h1:RarYDc9hq1UPLImuiXK3BIWPJLdIygvV3PsInK0FbVQ=
|
||||
github.com/atotto/clipboard v0.1.4 h1:EH0zSVneZPSuFR11BlR9YppQTVDbh5+16AmcJi4g1z4=
|
||||
|
@ -15,44 +19,126 @@ github.com/aws/aws-sdk-go-v2 v1.13.0/go.mod h1:L6+ZpqHaLbAaxsqV0L4cvxZY7QupWJB4f
|
|||
github.com/aws/aws-sdk-go-v2 v1.16.1/go.mod h1:ytwTPBG6fXTZLxxeeCCWj2/EMYp/xDUgX+OET6TLNNU=
|
||||
github.com/aws/aws-sdk-go-v2 v1.17.4 h1:wyC6p9Yfq6V2y98wfDsj6OnNQa4w2BLGCLIxzNhwOGY=
|
||||
github.com/aws/aws-sdk-go-v2 v1.17.4/go.mod h1:uzbQtefpm44goOPmdKyAlXSNcwlRgF3ePWVW6EtJvvw=
|
||||
github.com/aws/aws-sdk-go-v2 v1.18.1 h1:+tefE750oAb7ZQGzla6bLkOwfcQCEtC5y2RqoqCeqKo=
|
||||
github.com/aws/aws-sdk-go-v2 v1.18.1/go.mod h1:uzbQtefpm44goOPmdKyAlXSNcwlRgF3ePWVW6EtJvvw=
|
||||
github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.4.10 h1:dK82zF6kkPeCo8J1e+tGx4JdvDIQzj7ygIoLg8WMuGs=
|
||||
github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.4.10/go.mod h1:VeTZetY5KRJLuD/7fkQXMU6Mw7H5m/KP2J5Iy9osMno=
|
||||
github.com/aws/aws-sdk-go-v2/config v1.13.1 h1:yLv8bfNoT4r+UvUKQKqRtdnvuWGMK5a82l4ru9Jvnuo=
|
||||
github.com/aws/aws-sdk-go-v2/config v1.13.1/go.mod h1:Ba5Z4yL/UGbjQUzsiaN378YobhFo0MLfueXGiOsYtEs=
|
||||
github.com/aws/aws-sdk-go-v2/config v1.18.27 h1:Az9uLwmssTE6OGTpsFqOnaGpLnKDqNYOJzWuC6UAYzA=
|
||||
github.com/aws/aws-sdk-go-v2/config v1.18.27/go.mod h1:0My+YgmkGxeqjXZb5BYme5pc4drjTnM+x1GJ3zv42Nw=
|
||||
github.com/aws/aws-sdk-go-v2/credentials v1.8.0 h1:8Ow0WcyDesGNL0No11jcgb1JAtE+WtubqXjgxau+S0o=
|
||||
github.com/aws/aws-sdk-go-v2/credentials v1.8.0/go.mod h1:gnMo58Vwx3Mu7hj1wpcG8DI0s57c9o42UQ6wgTQT5to=
|
||||
github.com/aws/aws-sdk-go-v2/credentials v1.13.26 h1:qmU+yhKmOCyujmuPY7tf5MxR/RKyZrOPO3V4DobiTUk=
|
||||
github.com/aws/aws-sdk-go-v2/credentials v1.13.26/go.mod h1:GoXt2YC8jHUBbA4jr+W3JiemnIbkXOfxSXcisUsZ3os=
|
||||
github.com/aws/aws-sdk-go-v2/feature/dynamodb/attributevalue v1.10.12 h1:ama2cD4WaH6+8Gq/M/g+ZumPmmqCyanr+6Sm+iJVxfA=
|
||||
github.com/aws/aws-sdk-go-v2/feature/dynamodb/attributevalue v1.10.12/go.mod h1:tPnUO5mS3JThpwfq4Q8iPd745s7yh6fGPqDUEBw+Wv4=
|
||||
github.com/aws/aws-sdk-go-v2/feature/dynamodb/expression v1.4.39 h1:PhgfvgqwMFQKwOcxLV7V3lNDVnR3ZUWzoB6T9oCFpR4=
|
||||
github.com/aws/aws-sdk-go-v2/feature/dynamodb/expression v1.4.39/go.mod h1:/GkvC7uHpK50ilKkKx9I2gZiI/ieZbKjS2aah1rT9uE=
|
||||
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.10.0 h1:NITDuUZO34mqtOwFWZiXo7yAHj7kf+XPE+EiKuCBNUI=
|
||||
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.10.0/go.mod h1:I6/fHT/fH460v09eg2gVrd8B/IqskhNdpcLH0WNO3QI=
|
||||
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.13.4 h1:LxK/bitrAr4lnh9LnIS6i7zWbCOdMsfzKFBI6LUCS0I=
|
||||
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.13.4/go.mod h1:E1hLXN/BL2e6YizK1zFlYd8vsfi2GTjbjBazinMmeaM=
|
||||
github.com/aws/aws-sdk-go-v2/internal/configsources v1.1.4/go.mod h1:XHgQ7Hz2WY2GAn//UXHofLfPXWh+s62MbMOijrg12Lw=
|
||||
github.com/aws/aws-sdk-go-v2/internal/configsources v1.1.8/go.mod h1:LnTQMTqbKsbtt+UI5+wPsB7jedW+2ZgozoPG8k6cMxg=
|
||||
github.com/aws/aws-sdk-go-v2/internal/configsources v1.1.28 h1:r+XwaCLpIvCKjBIYy/HVZujQS9tsz5ohHG3ZIe0wKoE=
|
||||
github.com/aws/aws-sdk-go-v2/internal/configsources v1.1.28/go.mod h1:3lwChorpIM/BhImY/hy+Z6jekmN92cXGPI1QJasVPYY=
|
||||
github.com/aws/aws-sdk-go-v2/internal/configsources v1.1.34 h1:A5UqQEmPaCFpedKouS4v+dHCTUo2sKqhoKO9U5kxyWo=
|
||||
github.com/aws/aws-sdk-go-v2/internal/configsources v1.1.34/go.mod h1:wZpTEecJe0Btj3IYnDx/VlUzor9wm3fJHyvLpQF0VwY=
|
||||
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.2.0/go.mod h1:BsCSJHx5DnDXIrOcqB8KN1/B+hXLG/bi4Y6Vjcx/x9E=
|
||||
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.4.2/go.mod h1:1x4ZP3Z8odssdhuLI+/1Tqw6Pt/VAaP4Tr8EUxHvPXE=
|
||||
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.4.22 h1:7AwGYXDdqRQYsluvKFmWoqpcOQJ4bH634SkYf3FNj/A=
|
||||
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.4.22/go.mod h1:EqK7gVrIGAHyZItrD1D8B0ilgwMD1GiWAmbU4u/JHNk=
|
||||
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.4.28 h1:srIVS45eQuewqz6fKKu6ZGXaq6FuFg5NzgQBAM6g8Y4=
|
||||
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.4.28/go.mod h1:7VRpKQQedkfIEXb4k52I7swUnZP0wohVajJMRn3vsUw=
|
||||
github.com/aws/aws-sdk-go-v2/internal/ini v1.3.5 h1:ixotxbfTCFpqbuwFv/RcZwyzhkxPSYDYEMcj4niB5Uk=
|
||||
github.com/aws/aws-sdk-go-v2/internal/ini v1.3.5/go.mod h1:R3sWUqPcfXSiF/LSFJhjyJmpg9uV6yP2yv3YZZjldVI=
|
||||
github.com/aws/aws-sdk-go-v2/internal/ini v1.3.35 h1:LWA+3kDM8ly001vJ1X1waCuLJdtTl48gwkPKWy9sosI=
|
||||
github.com/aws/aws-sdk-go-v2/internal/ini v1.3.35/go.mod h1:0Eg1YjxE0Bhn56lx+SHJwCzhW+2JGtizsrx+lCqrfm0=
|
||||
github.com/aws/aws-sdk-go-v2/internal/v4a v1.0.26 h1:wscW+pnn3J1OYnanMnza5ZVYXLX4cKk5rAvUAl4Qu+c=
|
||||
github.com/aws/aws-sdk-go-v2/internal/v4a v1.0.26/go.mod h1:MtYiox5gvyB+OyP0Mr0Sm/yzbEAIPL9eijj/ouHAPw0=
|
||||
github.com/aws/aws-sdk-go-v2/service/cloudformation v1.30.0 h1:XbDkc4FLeg1RfnqeblfbJvaEabqq9ByZl4zqyPFkfSc=
|
||||
github.com/aws/aws-sdk-go-v2/service/cloudformation v1.30.0/go.mod h1:SwQFcCs9Rog8hSHm+81KBkAK+UKLXErA/1ChaEI8mLE=
|
||||
github.com/aws/aws-sdk-go-v2/service/cloudfront v1.26.8 h1:loRDtQ0vT0+JCB0hQBCfv95tttEzJ1rqSaTDy5cpy0A=
|
||||
github.com/aws/aws-sdk-go-v2/service/cloudfront v1.26.8/go.mod h1:YTd4wGn2beCF9wkSTpEcupk79zDFYJk2Ca76B8YyvJg=
|
||||
github.com/aws/aws-sdk-go-v2/service/cloudtrail v1.27.1 h1:Qw1G/M7eanpm6s/URkG1UuRLKEnRnpUvkUb7NMVvWb8=
|
||||
github.com/aws/aws-sdk-go-v2/service/cloudtrail v1.27.1/go.mod h1:oKRYqorIUkfAVmX03+lpv3tW5WelDpaliqzTwmCj/k8=
|
||||
github.com/aws/aws-sdk-go-v2/service/cloudwatch v1.26.2 h1:PWGu2JhCb/XJlJ7SSFJq76pxk4xWsN76nZxh7TzMHx0=
|
||||
github.com/aws/aws-sdk-go-v2/service/cloudwatch v1.26.2/go.mod h1:2KOZkkzMDZCo/aLzPhys06mHNkiU74u85aMJA3PLRvg=
|
||||
github.com/aws/aws-sdk-go-v2/service/dynamodb v1.18.3 h1:MxOpCZ+o9+AIeQHi2ocW7H4D7p0LhEkmetETVvDnkvg=
|
||||
github.com/aws/aws-sdk-go-v2/service/dynamodb v1.18.3/go.mod h1:nkpC9xkh+3vdxmhqN8Ac10pgV14DsJDLzUsV2CcS+44=
|
||||
github.com/aws/aws-sdk-go-v2/service/dynamodb v1.19.11 h1:tLTGNAsazbfjfjW1k/i43kyCcyTTTTFaD93H7JbSbbs=
|
||||
github.com/aws/aws-sdk-go-v2/service/dynamodb v1.19.11/go.mod h1:W1oiFegjVosgjIwb2Vv45jiCQT1ee8x85u8EyZRYLes=
|
||||
github.com/aws/aws-sdk-go-v2/service/dynamodbstreams v1.14.3 h1:B+bkmCnNJi194pu9aTtYUe8f4EPXafC+xfU+zciVxdg=
|
||||
github.com/aws/aws-sdk-go-v2/service/dynamodbstreams v1.14.3/go.mod h1:bRphLmXQD9Ux4jLcFEwyrWdmuPTj2Lh8VGl9wILuJII=
|
||||
github.com/aws/aws-sdk-go-v2/service/ebs v1.16.14 h1:DosI4CvEUo6/V21pDspzYkOa2X3Zwy5XS/cbPFiqDv0=
|
||||
github.com/aws/aws-sdk-go-v2/service/ebs v1.16.14/go.mod h1:yVTqVHjnrbAj6FvhTQfjNgwQbjPbDUUvA1x4IpXFmrE=
|
||||
github.com/aws/aws-sdk-go-v2/service/ec2 v1.102.0 h1:P4dyjm49F2kKws0FpouBC6fjVImACXKt752+CWa01lM=
|
||||
github.com/aws/aws-sdk-go-v2/service/ec2 v1.102.0/go.mod h1:tIctCeX9IbzsUTKHt53SVEcgyfxV2ElxJeEB+QUbc4M=
|
||||
github.com/aws/aws-sdk-go-v2/service/ecr v1.18.13 h1:hF7MUVNjubetjggZDtn3AmqCJzD7EUi//tSdxMYPm7U=
|
||||
github.com/aws/aws-sdk-go-v2/service/ecr v1.18.13/go.mod h1:XwEFO35g0uN/SftK0asWxh8Rk6DOx37R83TmWe2tzEE=
|
||||
github.com/aws/aws-sdk-go-v2/service/ecs v1.27.4 h1:F1N0Eh5EGRRY9QpF+tMTkx8Wb59DkQWE91Xza/9dk1c=
|
||||
github.com/aws/aws-sdk-go-v2/service/ecs v1.27.4/go.mod h1:0irnFofeEZwT7uTjSkNVcSQJbWRqZ9BRoxhKjt1BObM=
|
||||
github.com/aws/aws-sdk-go-v2/service/eks v1.27.14 h1:47HQVuJXgwvuoc4AT3rVdm77H0qGFbFnsuE4PRT+xX0=
|
||||
github.com/aws/aws-sdk-go-v2/service/eks v1.27.14/go.mod h1:QxuWcm9rlLkW3aEV8tiDzqZewnNSNUZfnqJvo1Nv9A0=
|
||||
github.com/aws/aws-sdk-go-v2/service/elasticache v1.27.2 h1:IC9XLGcT3yEkziTlX7PX54km7cHJnltlV7Ppwq2+7ik=
|
||||
github.com/aws/aws-sdk-go-v2/service/elasticache v1.27.2/go.mod h1:+oJhn/SIud310/2LLSVmlNZmExmlYPaGCLmUsnq5JZc=
|
||||
github.com/aws/aws-sdk-go-v2/service/elasticsearchservice v1.19.2 h1:Zam6yofBgdtP13laNoeA+DA9wlKJNooU8p3CWw6xLaI=
|
||||
github.com/aws/aws-sdk-go-v2/service/elasticsearchservice v1.19.2/go.mod h1:dehjpZ00q0RJcBUOUEysaj7zHK2rHSS4ePp89MsFiaI=
|
||||
github.com/aws/aws-sdk-go-v2/service/glue v1.52.0 h1:ukSf8ZdoZ6AygsUWIjj177wLOXljxBspBaNMgvx6fRA=
|
||||
github.com/aws/aws-sdk-go-v2/service/glue v1.52.0/go.mod h1:wMCE0B6l8eHb57l2DMYCGxt0rHIfcu3RvIY7SAfc+Fs=
|
||||
github.com/aws/aws-sdk-go-v2/service/iam v1.21.0 h1:8hEpu60CWlrp7iEBUFRZhgPoX6+gadaGL1sD4LoRYS0=
|
||||
github.com/aws/aws-sdk-go-v2/service/iam v1.21.0/go.mod h1:aQZ8BI+reeaY7RI/QQp7TKCSUHOesTdrzzylp3CW85c=
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.9.11 h1:y2+VQzC6Zh2ojtV2LoC0MNwHWc6qXv/j2vrQtlftkdA=
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.9.11/go.mod h1:iV4q2hsqtNECrfmlXyord9u4zyuFEJX9eLgLpSPzWA8=
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.1.29 h1:zZSLP3v3riMOP14H7b4XP0uyfREDQOYv2cqIrvTXDNQ=
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.1.29/go.mod h1:z7EjRjVwZ6pWcWdI2H64dKttvzaP99jRIj5hphW0M5U=
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/endpoint-discovery v1.7.22 h1:6zEryIiJOSk5/OcVHzkPDwzNBQ2atYCTShyA7TqkuxA=
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/endpoint-discovery v1.7.22/go.mod h1:moeOz5SKfY0p6pNIChdPIQdfaUfWI67+OVe0/r6+aGY=
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/endpoint-discovery v1.7.28 h1:/D994rtMQd1jQ2OY+7tvUlMlrv1L1c7Xtma/FhkbVtY=
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/endpoint-discovery v1.7.28/go.mod h1:3bJI2pLY3ilrqO5EclusI1GbjFJh1iXYrhOItf2sjKw=
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.7.0 h1:4QAOB3KrvI1ApJK14sliGr3Ie2pjyvNypn/lfzDHfUw=
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.7.0/go.mod h1:K/qPe6AP2TGYv4l6n7c88zh9jWBDf6nHhvg1fx/EWfU=
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.9.28 h1:bkRyG4a929RCnpVSTvLM2j/T4ls015ZhhYApbmYs15s=
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.9.28/go.mod h1:jj7znCIg05jXlaGBlFMGP8+7UN3VtCkRBG2spnmRQkU=
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.14.3 h1:dBL3StFxHtpBzJJ/mNEsjXVgfO+7jR0dAIEwLqMapEA=
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.14.3/go.mod h1:f1QyiAsvIv4B49DmCqrhlXqyaR+0IxMmyX+1P+AnzOM=
|
||||
github.com/aws/aws-sdk-go-v2/service/kinesis v1.17.14 h1:oSw0SQN9cKeYvCUYfPul7bH11b8E9I9BnoVUme3iSaU=
|
||||
github.com/aws/aws-sdk-go-v2/service/kinesis v1.17.14/go.mod h1:omXkSCk1T1difhE8wVaecXNeerY6jmpFFu49ngjEDQk=
|
||||
github.com/aws/aws-sdk-go-v2/service/kms v1.22.2 h1:jwmtdM1/l1DRNy5jQrrYpsQm8zwetkgeqhAqefDr1yI=
|
||||
github.com/aws/aws-sdk-go-v2/service/kms v1.22.2/go.mod h1:aNfh11Smy55o65PB3MyKbkM8BFyFUcZmj1k+4g8eNfg=
|
||||
github.com/aws/aws-sdk-go-v2/service/lambda v1.37.0 h1:xzyM5ZR9kZW0/Bkw5EiihOy6B+BYclp5K+yb6OHjc7s=
|
||||
github.com/aws/aws-sdk-go-v2/service/lambda v1.37.0/go.mod h1:Q8zQi5nZpjUF/H55dKEpKfEvFWJkgZzjjqvDb2AR5b4=
|
||||
github.com/aws/aws-sdk-go-v2/service/rds v1.46.0 h1:uv2LAciZRd5lEXzJo2u92tdZh/JxcVL7YLC51D4NLG4=
|
||||
github.com/aws/aws-sdk-go-v2/service/rds v1.46.0/go.mod h1:goBDR4OPrsnKpYyU0GHGcEnlTmL8O+eKGsWeyOAFJ5M=
|
||||
github.com/aws/aws-sdk-go-v2/service/redshift v1.28.0 h1:tmhg03t7nNVSFqhxb8YpHqq8H1wwwrfEQv/rL7NkTAE=
|
||||
github.com/aws/aws-sdk-go-v2/service/redshift v1.28.0/go.mod h1:x9am33DT5lVKUb0DH1UVbX+iFfpIqAKx6DAqB5Qu6jU=
|
||||
github.com/aws/aws-sdk-go-v2/service/route53 v1.28.3 h1:nJbE4+tHd8xpM1RB1ZF0/xTJnFd/ATz42ZC35lwXx0w=
|
||||
github.com/aws/aws-sdk-go-v2/service/route53 v1.28.3/go.mod h1:Cd4MnFoV+6fELBrgWXJ4Y09FrSkn/VjNPkOr1Yr1muU=
|
||||
github.com/aws/aws-sdk-go-v2/service/s3 v1.36.0 h1:lEmQ1XSD9qLk+NZXbgvLJI/IiTz7OIR2TYUTFH25EI4=
|
||||
github.com/aws/aws-sdk-go-v2/service/s3 v1.36.0/go.mod h1:aVbf0sko/TsLWHx30c/uVu7c62+0EAJ3vbxaJga0xCw=
|
||||
github.com/aws/aws-sdk-go-v2/service/sfn v1.18.0 h1:1AIwJvCywFO4nGtHj7ZtKb9mhLpB5hToyjtE5OO6o/I=
|
||||
github.com/aws/aws-sdk-go-v2/service/sfn v1.18.0/go.mod h1:41VgIwo6R/QE8DnFZ4RrP+f2w9xTzB77h3NRu/BzXyE=
|
||||
github.com/aws/aws-sdk-go-v2/service/sns v1.20.13 h1:+ADGcDhddHTKyu6Qp3oZKootryteS7D3ODo2ZPDBgjQ=
|
||||
github.com/aws/aws-sdk-go-v2/service/sns v1.20.13/go.mod h1:rWrvp9i8y/lX94lS7Kn/0iu9RY6vXzeKRqS/knVX8/c=
|
||||
github.com/aws/aws-sdk-go-v2/service/sqs v1.16.0 h1:dzWS4r8E9bA0TesHM40FSAtedwpTVCuTsLI8EziSqyk=
|
||||
github.com/aws/aws-sdk-go-v2/service/sqs v1.16.0/go.mod h1:IBTQMG8mtyj37OWg7vIXcg714Ntcb/LlYou/rZpvV1k=
|
||||
github.com/aws/aws-sdk-go-v2/service/sqs v1.23.2 h1:Y2vfLiY3HmaMisuwx6fS2kMRYbajRXXB+9vesGVPseY=
|
||||
github.com/aws/aws-sdk-go-v2/service/sqs v1.23.2/go.mod h1:TaV67b6JMD1988x/uMDop/JnMFK6v5d4Ru+sDmFg+ww=
|
||||
github.com/aws/aws-sdk-go-v2/service/ssm v1.24.0 h1:p22U2yL/AeRToERGcZv1R26Yci5VQnWIrpzcZdG54cg=
|
||||
github.com/aws/aws-sdk-go-v2/service/ssm v1.24.0/go.mod h1:chcyLYBEVRac/7rWJsD6cUHUR2osROwavvNqCplfwog=
|
||||
github.com/aws/aws-sdk-go-v2/service/sso v1.9.0 h1:1qLJeQGBmNQW3mBNzK2CFmrQNmoXWrscPqsrAaU1aTA=
|
||||
github.com/aws/aws-sdk-go-v2/service/sso v1.9.0/go.mod h1:vCV4glupK3tR7pw7ks7Y4jYRL86VvxS+g5qk04YeWrU=
|
||||
github.com/aws/aws-sdk-go-v2/service/sso v1.12.12 h1:nneMBM2p79PGWBQovYO/6Xnc2ryRMw3InnDJq1FHkSY=
|
||||
github.com/aws/aws-sdk-go-v2/service/sso v1.12.12/go.mod h1:HuCOxYsF21eKrerARYO6HapNeh9GBNq7fius2AcwodY=
|
||||
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.14.12 h1:2qTR7IFk7/0IN/adSFhYu9Xthr0zVFTgBrmPldILn80=
|
||||
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.14.12/go.mod h1:E4VrHCPzmVB/KFXtqBGKb3c8zpbNBgKe3fisDNLAW5w=
|
||||
github.com/aws/aws-sdk-go-v2/service/sts v1.14.0 h1:ksiDXhvNYg0D2/UFkLejsaz3LqpW5yjNQ8Nx9Sn2c0E=
|
||||
github.com/aws/aws-sdk-go-v2/service/sts v1.14.0/go.mod h1:u0xMJKDvvfocRjiozsoZglVNXRG19043xzp3r2ivLIk=
|
||||
github.com/aws/aws-sdk-go-v2/service/sts v1.19.2 h1:XFJ2Z6sNUUcAz9poj+245DMkrHE4h2j5I9/xD50RHfE=
|
||||
github.com/aws/aws-sdk-go-v2/service/sts v1.19.2/go.mod h1:dp0yLPsLBOi++WTxzCjA/oZqi6NPIhoR+uF7GeMU9eg=
|
||||
github.com/aws/aws-sdk-go-v2/service/wafv2 v1.35.1 h1:FtzLuTf9HPECIcKdBMtA16ZwZWOIj/r57Z3QuWuYfqc=
|
||||
github.com/aws/aws-sdk-go-v2/service/wafv2 v1.35.1/go.mod h1:RBpb9oTsEgAUfyaTAT2hFC83DxtLxj+SQpcbhaXiHnU=
|
||||
github.com/aws/smithy-go v1.10.0/go.mod h1:SObp3lf9smib00L/v3U2eAKG8FyQ7iLrJnQiAmR5n+E=
|
||||
github.com/aws/smithy-go v1.11.2/go.mod h1:3xHYmszWVx2c0kIwQeEVf9uSm4fYZt67FBJnwub1bgM=
|
||||
github.com/aws/smithy-go v1.13.5 h1:hgz0X/DX0dGqTYpGALqXJoRKRj5oQ7150i5FdTePzO8=
|
||||
|
@ -76,11 +162,18 @@ github.com/cloudcmds/tamarin v1.0.0 h1:PhrJ74FCUJo24/nIPXnQe9E3WVEIYo4aG58pICOMD
|
|||
github.com/cloudcmds/tamarin v1.0.0/go.mod h1:U1aHBoAFtJbI9jzgaj8TUo9C6vfzUKzn1OhWKIdigVM=
|
||||
github.com/containerd/console v1.0.3 h1:lIr7SlA5PxZyMV30bDW0MGbiOPXwc63yRuCP0ARubLw=
|
||||
github.com/containerd/console v1.0.3/go.mod h1:7LqA/THxQ86k76b8c/EMSiaJ3h1eZkMkXar0TQ1gf3U=
|
||||
github.com/coreos/etcd v3.3.10+incompatible/go.mod h1:uF7uidLiAD3TWHmW31ZFd/JWoc32PjwdhPthX9715RE=
|
||||
github.com/coreos/go-etcd v2.0.0+incompatible/go.mod h1:Jez6KQU2B/sWsbdaef3ED8NzMklzPG4d5KIOhIy30Tk=
|
||||
github.com/coreos/go-semver v0.2.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk=
|
||||
github.com/cpuguy83/go-md2man v1.0.10/go.mod h1:SmD6nW6nTyfqj6ABTjUi3V3JVMnlJmwcJI5acqYI6dE=
|
||||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=
|
||||
github.com/gofrs/uuid v4.3.1+incompatible h1:0/KbAdpx3UXAx1kEOWHJeOkpbgRFGHVgv+CFIY7dBJI=
|
||||
github.com/gofrs/uuid v4.3.1+incompatible/go.mod h1:b2aQJv3Z4Fp6yNu3cdSllBxTCLRxnplIgP/c0N/04lM=
|
||||
github.com/gofrs/uuid v4.4.0+incompatible h1:3qXRTX8/NbyulANqlc0lchS1gqAVxRgsuW1YrTJupqA=
|
||||
github.com/gofrs/uuid v4.4.0+incompatible/go.mod h1:b2aQJv3Z4Fp6yNu3cdSllBxTCLRxnplIgP/c0N/04lM=
|
||||
github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
|
||||
github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk=
|
||||
github.com/golang/protobuf v1.5.2 h1:ROPKBNFfQgOUMifHyP+KYbvpjbdoFNs+aK7DXlji0Tw=
|
||||
|
@ -97,13 +190,19 @@ github.com/hashicorp/errwrap v1.0.0 h1:hLrqtEDnRye3+sgx6z4qVLNuviH3MR5aQ0ykNJa/U
|
|||
github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4=
|
||||
github.com/hashicorp/go-multierror v1.1.1 h1:H5DkEtf6CXdFp0N0Em5UCwQpXMWke8IA0+lD48awMYo=
|
||||
github.com/hashicorp/go-multierror v1.1.1/go.mod h1:iw975J/qwKPdAO1clOe2L8331t/9/fmwbPZ6JB6eMoM=
|
||||
github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ=
|
||||
github.com/hexops/gotextdiff v1.0.3 h1:gitA9+qJrrTCsiCl7+kh75nPqQt1cx4ZkudSTLoUqJM=
|
||||
github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8=
|
||||
github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsIM=
|
||||
github.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg=
|
||||
github.com/jackc/pgservicefile v0.0.0-20200714003250-2b9c44734f2b h1:C8S2+VttkHFdOOCXJe+YGfa4vHYwlt4Zx+IVXQ97jYg=
|
||||
github.com/jackc/pgservicefile v0.0.0-20200714003250-2b9c44734f2b/go.mod h1:vsD4gTJCa9TptPL8sPkXrLZ+hDuNrZCnj29CQpr4X1E=
|
||||
github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a h1:bbPeKD0xmW/Y25WS6cokEszi5g+S0QxI/d45PkRi7Nk=
|
||||
github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a/go.mod h1:5TJZWKEWniPve33vlWYSoGYefn3gLQRzjfDlhSJ9ZKM=
|
||||
github.com/jackc/pgx/v5 v5.0.4 h1:r5O6y84qHX/z/HZV40JBdx2obsHz7/uRj5b+CcYEdeY=
|
||||
github.com/jackc/pgx/v5 v5.0.4/go.mod h1:U0ynklHtgg43fue9Ly30w3OCSTDPlXjig9ghrNGaguQ=
|
||||
github.com/jackc/pgx/v5 v5.4.1 h1:oKfB/FhuVtit1bBM3zNRRsZ925ZkMN3HXL+LgLUM9lE=
|
||||
github.com/jackc/pgx/v5 v5.4.1/go.mod h1:q6iHT8uDNXWiFNOlRqJzBTaSH3+2xCXkokxHZC5qWFY=
|
||||
github.com/jmespath/go-jmespath v0.4.0 h1:BEgLn5cpjn8UN1mAw4NjwDrS35OdebyEtFe+9YPoQUg=
|
||||
github.com/jmespath/go-jmespath v0.4.0/go.mod h1:T8mJZnbsbmF+m6zOOFylbeCJqk5+pHWvzYPziyZiYoo=
|
||||
github.com/jmespath/go-jmespath/internal/testify v1.5.1 h1:shLQSRRSCCPj3f2gpwzGwWFoC7ycTf1rcQZHOlsJ6N8=
|
||||
|
@ -112,6 +211,7 @@ github.com/juju/ansiterm v0.0.0-20210929141451-8b71cc96ebdc h1:ZQrgZFsLzkw7o3CoD
|
|||
github.com/juju/ansiterm v0.0.0-20210929141451-8b71cc96ebdc/go.mod h1:PyXUpnI3olx3bsPcHt98FGPX/KCFZ1Fi+hw1XLI6384=
|
||||
github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI=
|
||||
github.com/kr/pretty v0.3.0 h1:WgNl7dwNpEZ6jJ9k1snq4pZsg7DOEN8hP9Xw0Tsjwk0=
|
||||
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
|
||||
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
|
||||
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
|
||||
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
|
||||
|
@ -129,11 +229,14 @@ github.com/lucasb-eyer/go-colorful v1.2.0 h1:1nnpGOrhyZZuNyfu1QjKiUICQ74+3FNCN69
|
|||
github.com/lucasb-eyer/go-colorful v1.2.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0=
|
||||
github.com/lunixbochs/vtclean v1.0.0 h1:xu2sLAri4lGiovBDQKxl5mrXyESr3gUr5m5SM5+LVb8=
|
||||
github.com/lunixbochs/vtclean v1.0.0/go.mod h1:pHhQNgMf3btfWnGBVipUOjRYhoOsdGqdm/+2c2E2WMI=
|
||||
github.com/magiconair/properties v1.8.0/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ=
|
||||
github.com/mattn/go-colorable v0.1.10/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc=
|
||||
github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU=
|
||||
github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94=
|
||||
github.com/mattn/go-isatty v0.0.16 h1:bq3VjFmv/sOjHtdEhmkEV4x1AJtvUvOJ2PFAZ5+peKQ=
|
||||
github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
|
||||
github.com/mattn/go-isatty v0.0.17 h1:BTarxUcIeDqL27Mc+vyvdWYSL28zpIhv3RoTdsLMPng=
|
||||
github.com/mattn/go-isatty v0.0.17/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
|
||||
github.com/mattn/go-localereader v0.0.1 h1:ygSAOl7ZXTx4RdPYinUpg6W99U8jWvWi9Ye2JC/oIi4=
|
||||
github.com/mattn/go-localereader v0.0.1/go.mod h1:8fBrzywKY7BI3czFoHkuzRoWE9C+EiG4R1k4Cjx5p88=
|
||||
github.com/mattn/go-runewidth v0.0.10/go.mod h1:RAqKPSqVFrSLVXbA8x7dzmKdmGzieGRCM46jaSJTDAk=
|
||||
|
@ -141,6 +244,8 @@ github.com/mattn/go-runewidth v0.0.12/go.mod h1:RAqKPSqVFrSLVXbA8x7dzmKdmGzieGRC
|
|||
github.com/mattn/go-runewidth v0.0.13/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w=
|
||||
github.com/mattn/go-runewidth v0.0.14 h1:+xnbZSEeDbOIg5/mE6JF0w6n9duR1l3/WmbinWVwUuU=
|
||||
github.com/mattn/go-runewidth v0.0.14/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w=
|
||||
github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0=
|
||||
github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y=
|
||||
github.com/muesli/ansi v0.0.0-20211018074035-2e021307bc4b/go.mod h1:fQuZ0gauxyBcmsdE3ZT4NasjaRdxmbCS0jRHsrWu3Ho=
|
||||
github.com/muesli/ansi v0.0.0-20211031195517-c9f0611b6c70 h1:kMlmsLSbjkikxQJ1IPwaM+7LJ9ltFu/fi8CRzvSnQmA=
|
||||
github.com/muesli/ansi v0.0.0-20211031195517-c9f0611b6c70/go.mod h1:fQuZ0gauxyBcmsdE3ZT4NasjaRdxmbCS0jRHsrWu3Ho=
|
||||
|
@ -154,27 +259,47 @@ github.com/muesli/termenv v0.11.1-0.20220204035834-5ac8409525e0/go.mod h1:Bd5NYQ
|
|||
github.com/muesli/termenv v0.11.1-0.20220212125758-44cd13922739/go.mod h1:Bd5NYQ7pd+SrtBSrSNoBBmXlcY8+Xj4BMJgh8qcZrvs=
|
||||
github.com/muesli/termenv v0.13.0 h1:wK20DRpJdDX8b7Ek2QfhvqhRQFZ237RGRO0RQ/Iqdy0=
|
||||
github.com/muesli/termenv v0.13.0/go.mod h1:sP1+uffeLaEYpyOTb8pLCUctGcGLnoFjSn4YJK5e2bc=
|
||||
github.com/pelletier/go-toml v1.2.0/go.mod h1:5z9KED0ma1S8pY6P1sdut58dfprrGBbd/94hg7ilaic=
|
||||
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
|
||||
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/risor-io/risor v0.8.0 h1:G0fpHMGztvocKVu8egkKNbvLy4Rsjkuk+0zReu2JSn8=
|
||||
github.com/risor-io/risor v0.8.0/go.mod h1:lvatEIYxs6HL+X/Bm0R+Mq4Z9a5Y036mniw6DwUnqs0=
|
||||
github.com/risor-io/risor v1.1.1 h1:J8rIZX/0HXhg/t2+QygksvP65XCWhg5QxRZrwZabhxE=
|
||||
github.com/risor-io/risor v1.1.1/go.mod h1:0UMw7ZMbUKSPFgQyuHCFe7UuBUewBKX4K3By4ba1CBA=
|
||||
github.com/risor-io/risor v1.4.0 h1:G17pWgq+N06jWvnaJVwos89tC5C4VMjqwGYRrTWleRM=
|
||||
github.com/risor-io/risor v1.4.0/go.mod h1:+s/FeK0CdsTCCNZsHSp8EJa3u3mMrhqtNGLCv/GcW8Y=
|
||||
github.com/rivo/uniseg v0.1.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
|
||||
github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
|
||||
github.com/rivo/uniseg v0.4.2 h1:YwD0ulJSJytLpiaWua0sBDusfsCZohxjxzVTYjwxfV8=
|
||||
github.com/rivo/uniseg v0.4.2/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88=
|
||||
github.com/rogpeppe/go-internal v1.9.0 h1:73kH8U+JUqXU8lRuOHeVHaa/SZPifC7BkcraZVejAe8=
|
||||
github.com/russross/blackfriday v1.5.2/go.mod h1:JO/DiYxRf+HjHt06OyowR9PTA263kcR/rfWxYHBV53g=
|
||||
github.com/sahilm/fuzzy v0.1.0 h1:FzWGaw2Opqyu+794ZQ9SYifWv2EIXpwP4q8dY1kDAwI=
|
||||
github.com/sahilm/fuzzy v0.1.0/go.mod h1:VFvziUEIMCrT6A6tw2RFIXPXXmzXbOsSHF0DOI8ZK9Y=
|
||||
github.com/spf13/afero v1.1.2/go.mod h1:j4pytiNVoe2o6bmDsKpLACNPDBIoEAkihy7loJ1B0CQ=
|
||||
github.com/spf13/cast v1.3.0/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE=
|
||||
github.com/spf13/cobra v0.0.5/go.mod h1:3K3wKZymM7VvHMDS9+Akkh4K60UwM26emMESw8tLCHU=
|
||||
github.com/spf13/jwalterweatherman v1.0.0/go.mod h1:cQK4TGJAtQXfYWX+Ddv3mKDzgVb68N+wFjFa4jdeBTo=
|
||||
github.com/spf13/pflag v1.0.3/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4=
|
||||
github.com/spf13/viper v1.3.2/go.mod h1:ZiWeW+zYFKm7srdB9IoDzzZXaJaI5eL9QjNiN/DMA2s=
|
||||
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
|
||||
github.com/stretchr/objx v0.5.0 h1:1zr/of2m5FGMsad5YfcqgdqdWrIhu+EBEJRhR1U7z/c=
|
||||
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
|
||||
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
|
||||
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
|
||||
github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA=
|
||||
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
|
||||
github.com/stretchr/testify v1.8.1 h1:w7B6lhMri9wdJUVmEZPGGhZzrYTPvgJArz7wNPgYKsk=
|
||||
github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
|
||||
github.com/stretchr/testify v1.8.3 h1:RP3t2pwF7cMEbC1dqtB6poj3niw/9gnV4Cjg5oW5gtY=
|
||||
github.com/stretchr/testify v1.8.3/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
|
||||
github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk=
|
||||
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
|
||||
github.com/tidwall/gjson v1.14.3 h1:9jvXn7olKEHU1S9vwoMGliaT8jq1vJ7IH/n9zD9Dnlw=
|
||||
github.com/tidwall/gjson v1.14.3/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk=
|
||||
github.com/tidwall/match v1.1.1 h1:+Ho715JplO36QYgwN9PGYNhgZvoUSc9X2c80KVTi+GA=
|
||||
|
@ -182,42 +307,58 @@ github.com/tidwall/match v1.1.1/go.mod h1:eRSPERbgtNPcGhD8UCthc6PmLEQXEWd3PRB5JT
|
|||
github.com/tidwall/pretty v1.2.0/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU=
|
||||
github.com/tidwall/pretty v1.2.1 h1:qjsOFOWWQl+N3RsoF5/ssm1pHmJJwhjlSbZ51I6wMl4=
|
||||
github.com/tidwall/pretty v1.2.1/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU=
|
||||
github.com/ugorji/go/codec v0.0.0-20181204163529-d75b2dcb6bc8/go.mod h1:VFNgLljTbGfSG7qAOspJ7OScBnGdDN/yBr0sguwnwf0=
|
||||
github.com/vmihailenco/msgpack v4.0.4+incompatible h1:dSLoQfGFAo3F6OoNhwUmLwVgaUXK79GlxNBwueZn0xI=
|
||||
github.com/vmihailenco/msgpack v4.0.4+incompatible/go.mod h1:fy3FlTQTDXWkZ7Bh6AcGMlsjHatGryHQYUTf1ShIgkk=
|
||||
github.com/wI2L/jsondiff v0.3.0 h1:iTzQ9u/d86GE9RsBzVHX88f2EA1vQUboHwLhSQFc1s4=
|
||||
github.com/wI2L/jsondiff v0.3.0/go.mod h1:y1IMzNNjlSsk3IUoJdRJO7VRBtzMvRgyo4Vu0LdHpTc=
|
||||
github.com/xordataexchange/crypt v0.0.3-0.20170626215501-b2862e3d0a77/go.mod h1:aYKd//L2LvnjZzWKhF00oedf4jCCReLcmhLdhm1A27Q=
|
||||
github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k=
|
||||
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
|
||||
go.etcd.io/bbolt v1.3.6 h1:/ecaJf0sk1l4l6V4awd65v2C3ILy7MSj+s/x1ADCIMU=
|
||||
go.etcd.io/bbolt v1.3.6/go.mod h1:qXsaaIqmgQH0T+OPdb99Bf+PKfBBQVAdyD6TY9G8XM4=
|
||||
golang.design/x/clipboard v0.6.2 h1:a3Np4qfKnLWwfFJQhUWU3IDeRfmVuqWl+QPtP4CSYGw=
|
||||
golang.design/x/clipboard v0.6.2/go.mod h1:kqBSweBP0/im4SZGGjLrppH0D400Hnfo5WbFKSNK8N4=
|
||||
golang.org/x/crypto v0.0.0-20181203042331-505ab145d0a9/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
|
||||
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
||||
golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
||||
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
||||
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
|
||||
golang.org/x/crypto v0.0.0-20220829220503-c86fa9a7ed90 h1:Y/gsMcFOcR+6S6f3YeMKl5g+dZMEWqcz5Czj/GWYbkM=
|
||||
golang.org/x/crypto v0.0.0-20220829220503-c86fa9a7ed90/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
|
||||
golang.org/x/crypto v0.9.0 h1:LF6fAI+IutBocDJ2OT0Q1g8plpYljMZ4+lty+dsqw3g=
|
||||
golang.org/x/crypto v0.9.0/go.mod h1:yrmDGqONDYtNj3tH8X9dzUun2m2lzPa9ngI6/RUPGR0=
|
||||
golang.org/x/exp v0.0.0-20190731235908-ec7cb31e5a56/go.mod h1:JhuoJpWY28nO4Vef9tZUw9qufEGTyX1+7lmHxV5q5G4=
|
||||
golang.org/x/exp v0.0.0-20230108222341-4b8118a2686a h1:tlXy25amD5A7gOfbXdqCGN5k8ESEed/Ee1E5RcrYnqU=
|
||||
golang.org/x/exp v0.0.0-20230108222341-4b8118a2686a/go.mod h1:CxIveKay+FTh1D0yPZemJVgC/95VzuuOLq5Qi4xnoYc=
|
||||
golang.org/x/exp/shiny v0.0.0-20230213192124-5e25df0256eb h1:gdeQX7xJSkTNF+Sw7++XNIOo4pGL0CjQv3N2Vm1Erxk=
|
||||
golang.org/x/exp/shiny v0.0.0-20230213192124-5e25df0256eb/go.mod h1:UH99kUObWAZkDnWqppdQe5ZhPYESUw8I0zVV1uWBR+0=
|
||||
golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js=
|
||||
golang.org/x/image v0.0.0-20190703141733-d6a02ce849c9/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0=
|
||||
golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0=
|
||||
golang.org/x/image v0.0.0-20211028202545-6944b10bf410 h1:hTftEOvwiOq2+O8k2D5/Q7COC7k5Qcrgc2TFURJYnvQ=
|
||||
golang.org/x/image v0.0.0-20211028202545-6944b10bf410/go.mod h1:023OzeP/+EPmXeapQh35lcL3II3LrY8Ic+EFFKVhULM=
|
||||
golang.org/x/image v0.5.0 h1:5JMiNunQeQw++mMOz48/ISeNu3Iweh/JaZU8ZLqHRrI=
|
||||
golang.org/x/image v0.5.0/go.mod h1:FVC7BI/5Ym8R25iw5OLsgshdUBbT1h5jZTpA+mvAdZ4=
|
||||
golang.org/x/mobile v0.0.0-20190312151609-d3739f865fa6/go.mod h1:z+o9i4GpDbdi3rU15maQ/Ox0txvL9dWGYEHz965HBQE=
|
||||
golang.org/x/mobile v0.0.0-20210716004757-34ab1303b554 h1:3In5TnfvnuXTF/uflgpYxSCEGP2NdYT37KsPh3VjZYU=
|
||||
golang.org/x/mobile v0.0.0-20210716004757-34ab1303b554/go.mod h1:jFTmtFYCV0MFtXBU+J5V/+5AUeVS0ON/0WkE/KSrl6E=
|
||||
golang.org/x/mod v0.1.0/go.mod h1:0QHyrYULN0/3qlju5TqG8bIK38QM8yzMo5ekMj3DlcY=
|
||||
golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
|
||||
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
|
||||
golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||
golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks=
|
||||
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
|
||||
golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM=
|
||||
golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2 h1:CIJ76btIcR3eFI5EgSo6k1qKw9KJexJuRLI9G7Hp5wE=
|
||||
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
|
||||
golang.org/x/net v0.10.0 h1:X2//UzNDwYmtCLn7To6G58Wr6f5ahEAQgKNzv9Y951M=
|
||||
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sys v0.0.0-20181205085412-a5c9d58dba9a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
|
@ -231,13 +372,19 @@ golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBc
|
|||
golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220204135822-1c1b9b1eba6a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220209214540-3681064d5158/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.1.0 h1:kunALQeHf1/185U1i0GOB/fy1IPRDDpuoOOqRReG57U=
|
||||
golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.8.0 h1:EBmGv8NaZBZTWvrbjNoL6HVt+IVy3QDQpJs7VRIw3tU=
|
||||
golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
||||
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
|
||||
golang.org/x/term v0.0.0-20220919170432-7a66f970e087 h1:tPwmk4vmvVCMdr98VgL4JH+qZxPL8fqlUOHnyOM8N3w=
|
||||
golang.org/x/term v0.0.0-20220919170432-7a66f970e087/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
|
||||
golang.org/x/term v0.8.0 h1:n5xxQn2i3PC0yLAbjTpNT85q/Kgzcr2gIoX9OrJUols=
|
||||
golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo=
|
||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
|
||||
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||
|
@ -245,10 +392,14 @@ golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
|||
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
|
||||
golang.org/x/text v0.3.8 h1:nAL+RVCQ9uMn3vJZbV+MRnydTJFPf8qqY42YiA6MrqY=
|
||||
golang.org/x/text v0.3.8/go.mod h1:E6s5w1FMmriuDzIBO73fBruAKo1PCIq6d2Q6DHfQ8WQ=
|
||||
golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
|
||||
golang.org/x/text v0.9.0 h1:2sjJmO8cDvYveuX97RDLsxlyUxLl+GHoLxBiRdHllBE=
|
||||
golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
|
||||
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
golang.org/x/tools v0.0.0-20190312151545-0bb0c0a6e846/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
|
||||
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||
golang.org/x/tools v0.1.2/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk=
|
||||
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
|
||||
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
|
|
|
@ -8,6 +8,16 @@ func Values[K comparable, T any](ts map[K]T) []T {
|
|||
return values
|
||||
}
|
||||
|
||||
func MapValues[K comparable, T, U any](ts map[K]T, fn func(t T) U) map[K]U {
|
||||
us := make(map[K]U)
|
||||
|
||||
for k, t := range ts {
|
||||
us[k] = fn(t)
|
||||
}
|
||||
|
||||
return us
|
||||
}
|
||||
|
||||
func MapValuesWithError[K comparable, T, U any](ts map[K]T, fn func(t T) (U, error)) (map[K]U, error) {
|
||||
us := make(map[K]U)
|
||||
|
||||
|
|
|
@ -9,6 +9,14 @@ func All[T any](ts []T, predicate func(t T) bool) bool {
|
|||
return true
|
||||
}
|
||||
|
||||
func Generate[U any](from, to int, fn func(t int) U) []U {
|
||||
us := make([]U, to-from+1)
|
||||
for i := from; i <= to; i++ {
|
||||
us[i-from] = fn(i)
|
||||
}
|
||||
return us
|
||||
}
|
||||
|
||||
func Map[T, U any](ts []T, fn func(t T) U) []U {
|
||||
us := make([]U, len(ts))
|
||||
for i, t := range ts {
|
||||
|
@ -39,3 +47,22 @@ func Filter[T any](ts []T, fn func(t T) bool) []T {
|
|||
}
|
||||
return us
|
||||
}
|
||||
|
||||
func FindFirst[T any](ts []T, fn func(t T) bool) (returnedT T, found bool) {
|
||||
for _, t := range ts {
|
||||
if fn(t) {
|
||||
return t, true
|
||||
}
|
||||
}
|
||||
return returnedT, false
|
||||
}
|
||||
|
||||
func FindLast[T any](ts []T, fn func(t T) bool) (returnedT T, found bool) {
|
||||
for i := len(ts) - 1; i >= 0; i-- {
|
||||
t := ts[i]
|
||||
if fn(t) {
|
||||
return t, true
|
||||
}
|
||||
}
|
||||
return returnedT, false
|
||||
}
|
||||
|
|
|
@ -11,16 +11,17 @@ import (
|
|||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
"github.com/lmika/audax/internal/common/ui/events"
|
||||
"github.com/lmika/dynamo-browse/internal/common/ui/events"
|
||||
"github.com/lmika/shellwords"
|
||||
)
|
||||
|
||||
const commandsCategory = "commands"
|
||||
|
||||
type CommandController struct {
|
||||
historyProvider IterProvider
|
||||
commandList *CommandList
|
||||
lookupExtensions []CommandLookupExtension
|
||||
historyProvider IterProvider
|
||||
commandList *CommandList
|
||||
lookupExtensions []CommandLookupExtension
|
||||
completionProvider CommandCompletionProvider
|
||||
}
|
||||
|
||||
func NewCommandController(historyProvider IterProvider) *CommandController {
|
||||
|
@ -40,6 +41,10 @@ func (c *CommandController) AddCommandLookupExtension(ext CommandLookupExtension
|
|||
c.lookupExtensions = append(c.lookupExtensions, ext)
|
||||
}
|
||||
|
||||
func (c *CommandController) SetCommandCompletionProvider(provider CommandCompletionProvider) {
|
||||
c.completionProvider = provider
|
||||
}
|
||||
|
||||
func (c *CommandController) Prompt() tea.Msg {
|
||||
return events.PromptForInputMsg{
|
||||
Prompt: ":",
|
||||
|
@ -47,6 +52,24 @@ func (c *CommandController) Prompt() tea.Msg {
|
|||
OnDone: func(value string) tea.Msg {
|
||||
return c.Execute(value)
|
||||
},
|
||||
// TEMP
|
||||
OnTabComplete: func(value string) (string, bool) {
|
||||
if c.completionProvider == nil {
|
||||
return "", false
|
||||
}
|
||||
|
||||
if strings.HasPrefix(value, "sa ") || strings.HasPrefix(value, "da ") {
|
||||
tokens := shellwords.Split(strings.TrimSpace(value))
|
||||
lastToken := tokens[len(tokens)-1]
|
||||
|
||||
options := c.completionProvider.AttributesWithPrefix(lastToken)
|
||||
if len(options) == 1 {
|
||||
return value[:len(value)-len(lastToken)] + options[0], true
|
||||
}
|
||||
}
|
||||
return "", false
|
||||
},
|
||||
// END TEMP
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -2,11 +2,11 @@ package commandctrl_test
|
|||
|
||||
import (
|
||||
"context"
|
||||
"github.com/lmika/audax/internal/common/ui/events"
|
||||
"github.com/lmika/audax/internal/dynamo-browse/services"
|
||||
"github.com/lmika/dynamo-browse/internal/common/ui/events"
|
||||
"github.com/lmika/dynamo-browse/internal/dynamo-browse/services"
|
||||
"testing"
|
||||
|
||||
"github.com/lmika/audax/internal/common/ui/commandctrl"
|
||||
"github.com/lmika/dynamo-browse/internal/common/ui/commandctrl"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
|
|
|
@ -2,7 +2,7 @@ package commandctrl
|
|||
|
||||
import (
|
||||
"context"
|
||||
"github.com/lmika/audax/internal/dynamo-browse/services"
|
||||
"github.com/lmika/dynamo-browse/internal/dynamo-browse/services"
|
||||
)
|
||||
|
||||
type IterProvider interface {
|
||||
|
|
|
@ -19,3 +19,7 @@ type CommandList struct {
|
|||
type CommandLookupExtension interface {
|
||||
LookupCommand(name string) Command
|
||||
}
|
||||
|
||||
type CommandCompletionProvider interface {
|
||||
AttributesWithPrefix(prefix string) []string
|
||||
}
|
||||
|
|
|
@ -2,7 +2,7 @@ package dispatcher
|
|||
|
||||
import (
|
||||
tea "github.com/charmbracelet/bubbletea"
|
||||
"github.com/lmika/audax/internal/common/ui/uimodels"
|
||||
"github.com/lmika/dynamo-browse/internal/common/ui/uimodels"
|
||||
)
|
||||
|
||||
type DispatcherContext struct {
|
||||
|
|
|
@ -4,8 +4,8 @@ import (
|
|||
"context"
|
||||
"sync"
|
||||
|
||||
"github.com/lmika/audax/internal/common/ui/events"
|
||||
"github.com/lmika/audax/internal/common/ui/uimodels"
|
||||
"github.com/lmika/dynamo-browse/internal/common/ui/events"
|
||||
"github.com/lmika/dynamo-browse/internal/common/ui/uimodels"
|
||||
"github.com/pkg/errors"
|
||||
)
|
||||
|
||||
|
|
|
@ -2,7 +2,7 @@ package events
|
|||
|
||||
import (
|
||||
tea "github.com/charmbracelet/bubbletea"
|
||||
"github.com/lmika/audax/internal/dynamo-browse/services"
|
||||
"github.com/lmika/dynamo-browse/internal/dynamo-browse/services"
|
||||
"log"
|
||||
)
|
||||
|
||||
|
@ -54,3 +54,8 @@ type MessageWithMode interface {
|
|||
MessageWithStatus
|
||||
ModeMessage() string
|
||||
}
|
||||
|
||||
type MessageWithRightMode interface {
|
||||
MessageWithStatus
|
||||
RightModeMessage() string
|
||||
}
|
||||
|
|
|
@ -2,7 +2,7 @@ package events
|
|||
|
||||
import (
|
||||
tea "github.com/charmbracelet/bubbletea"
|
||||
"github.com/lmika/audax/internal/dynamo-browse/services"
|
||||
"github.com/lmika/dynamo-browse/internal/dynamo-browse/services"
|
||||
)
|
||||
|
||||
// Error indicates that an error occurred
|
||||
|
@ -21,8 +21,9 @@ type ModeMessage string
|
|||
|
||||
// PromptForInput indicates that the context is requesting a line of input
|
||||
type PromptForInputMsg struct {
|
||||
Prompt string
|
||||
History services.HistoryProvider
|
||||
OnDone func(value string) tea.Msg
|
||||
OnCancel func() tea.Msg
|
||||
Prompt string
|
||||
History services.HistoryProvider
|
||||
OnDone func(value string) tea.Msg
|
||||
OnCancel func() tea.Msg
|
||||
OnTabComplete func(value string) (string, bool)
|
||||
}
|
||||
|
|
|
@ -2,21 +2,25 @@ package controllers
|
|||
|
||||
import (
|
||||
tea "github.com/charmbracelet/bubbletea"
|
||||
"github.com/lmika/audax/internal/common/ui/events"
|
||||
"github.com/lmika/audax/internal/dynamo-browse/models"
|
||||
"github.com/lmika/audax/internal/dynamo-browse/models/columns"
|
||||
"github.com/lmika/audax/internal/dynamo-browse/models/queryexpr"
|
||||
"github.com/lmika/dynamo-browse/internal/common/ui/events"
|
||||
"github.com/lmika/dynamo-browse/internal/dynamo-browse/models"
|
||||
"github.com/lmika/dynamo-browse/internal/dynamo-browse/models/columns"
|
||||
"github.com/lmika/dynamo-browse/internal/dynamo-browse/models/evaluators"
|
||||
"github.com/lmika/dynamo-browse/internal/dynamo-browse/models/queryexpr"
|
||||
bus "github.com/lmika/events"
|
||||
"strings"
|
||||
)
|
||||
|
||||
type ColumnsController struct {
|
||||
tr *TableReadController
|
||||
|
||||
// State
|
||||
colModel *columns.Columns
|
||||
resultSet *models.ResultSet
|
||||
}
|
||||
|
||||
func NewColumnsController(eventBus *bus.Bus) *ColumnsController {
|
||||
cc := &ColumnsController{}
|
||||
func NewColumnsController(tr *TableReadController, eventBus *bus.Bus) *ColumnsController {
|
||||
cc := &ColumnsController{tr: tr}
|
||||
|
||||
eventBus.On(newResultSetEvent, cc.onNewResultSet)
|
||||
return cc
|
||||
|
@ -79,7 +83,7 @@ func (cc *ColumnsController) AddColumn(afterIndex int) tea.Msg {
|
|||
|
||||
newCol := columns.Column{
|
||||
Name: colExpr.String(),
|
||||
Evaluator: columns.ExprFieldValueEvaluator{Expr: colExpr},
|
||||
Evaluator: queryexpr.ExprFieldValueEvaluator{Expr: colExpr},
|
||||
}
|
||||
|
||||
if afterIndex >= len(cc.colModel.Columns)-1 {
|
||||
|
@ -115,3 +119,44 @@ func (cc *ColumnsController) DeleteColumn(afterIndex int) tea.Msg {
|
|||
|
||||
return ColumnsUpdated{}
|
||||
}
|
||||
|
||||
func (cc *ColumnsController) SortByColumn(index int) tea.Msg {
|
||||
if index >= len(cc.colModel.Columns) {
|
||||
return nil
|
||||
}
|
||||
|
||||
column := cc.colModel.Columns[index]
|
||||
newCriteria := models.SortCriteria{
|
||||
Fields: []models.SortField{
|
||||
{Field: column.Evaluator, Asc: true},
|
||||
},
|
||||
}
|
||||
if ff := cc.SortCriteria().FirstField(); evaluators.Equals(ff.Field, column.Evaluator) {
|
||||
newCriteria.Fields[0].Asc = !ff.Asc
|
||||
}
|
||||
|
||||
cc.SetSortCriteria(newCriteria)
|
||||
return ColumnsUpdated{}
|
||||
}
|
||||
|
||||
func (c *ColumnsController) AttributesWithPrefix(prefix string) []string {
|
||||
options := make([]string, 0)
|
||||
for _, col := range c.resultSet.Columns() {
|
||||
if strings.HasPrefix(col, prefix) {
|
||||
options = append(options, col)
|
||||
}
|
||||
}
|
||||
return options
|
||||
}
|
||||
|
||||
func (cc *ColumnsController) SortCriteria() models.SortCriteria {
|
||||
if cc.resultSet == nil {
|
||||
return models.SortCriteria{}
|
||||
}
|
||||
|
||||
return cc.resultSet.SortCriteria()
|
||||
}
|
||||
|
||||
func (cc *ColumnsController) SetSortCriteria(criteria models.SortCriteria) {
|
||||
cc.tr.SortResultSet(criteria)
|
||||
}
|
||||
|
|
|
@ -2,7 +2,7 @@ package controllers
|
|||
|
||||
import (
|
||||
tea "github.com/charmbracelet/bubbletea"
|
||||
"github.com/lmika/audax/internal/common/ui/events"
|
||||
"github.com/lmika/dynamo-browse/internal/common/ui/events"
|
||||
)
|
||||
|
||||
type promptSequence struct {
|
||||
|
|
|
@ -2,8 +2,12 @@ package controllers
|
|||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
tea "github.com/charmbracelet/bubbletea"
|
||||
"github.com/lmika/audax/internal/dynamo-browse/models"
|
||||
"github.com/lmika/dynamo-browse/internal/dynamo-browse/models"
|
||||
"github.com/lmika/dynamo-browse/internal/dynamo-browse/models/relitems"
|
||||
)
|
||||
|
||||
type SetTableItemView struct {
|
||||
|
@ -42,6 +46,24 @@ func (rs NewResultSet) ModeMessage() string {
|
|||
return modeLine
|
||||
}
|
||||
|
||||
func (rs NewResultSet) RightModeMessage() string {
|
||||
var sb strings.Builder
|
||||
|
||||
itemCountStr := applyToN("", len(rs.ResultSet.Items()), "item", "items", "")
|
||||
if rs.currentFilter != "" {
|
||||
sb.WriteString(fmt.Sprintf("%d of %v", rs.filteredCount, itemCountStr))
|
||||
} else {
|
||||
sb.WriteString(itemCountStr)
|
||||
}
|
||||
|
||||
if !rs.ResultSet.Created.IsZero() {
|
||||
sb.WriteString(" • ")
|
||||
sb.WriteString(rs.ResultSet.Created.Format(time.Kitchen))
|
||||
}
|
||||
|
||||
return sb.String()
|
||||
}
|
||||
|
||||
func (rs NewResultSet) StatusMessage() string {
|
||||
if rs.statusMessage != "" {
|
||||
return rs.statusMessage
|
||||
|
@ -69,3 +91,9 @@ func (rs ResultSetUpdated) StatusMessage() string {
|
|||
|
||||
type ShowColumnOverlay struct{}
|
||||
type HideColumnOverlay struct{}
|
||||
|
||||
type ShowRelatedItemsOverlay struct {
|
||||
Items []relitems.RelatedItem
|
||||
OnSelected func(item relitems.RelatedItem) tea.Msg
|
||||
}
|
||||
type HideRelatedItemsOverlay struct{}
|
||||
|
|
|
@ -1,57 +1,146 @@
|
|||
package controllers
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/csv"
|
||||
tea "github.com/charmbracelet/bubbletea"
|
||||
"github.com/lmika/audax/internal/common/ui/events"
|
||||
"github.com/lmika/audax/internal/dynamo-browse/models/attrutils"
|
||||
"github.com/pkg/errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
|
||||
tea "github.com/charmbracelet/bubbletea"
|
||||
"github.com/lmika/dynamo-browse/internal/common/ui/events"
|
||||
"github.com/lmika/dynamo-browse/internal/dynamo-browse/models"
|
||||
"github.com/lmika/dynamo-browse/internal/dynamo-browse/models/attrutils"
|
||||
"github.com/lmika/dynamo-browse/internal/dynamo-browse/models/columns"
|
||||
"github.com/lmika/dynamo-browse/internal/dynamo-browse/services"
|
||||
"github.com/lmika/dynamo-browse/internal/dynamo-browse/services/jobs"
|
||||
"github.com/pkg/errors"
|
||||
)
|
||||
|
||||
type ExportController struct {
|
||||
state *State
|
||||
columns *ColumnsController
|
||||
state *State
|
||||
tableService TableReadService
|
||||
jobController *JobsController
|
||||
columns *ColumnsController
|
||||
pasteboardProvider services.PasteboardProvider
|
||||
}
|
||||
|
||||
func NewExportController(state *State, columns *ColumnsController) *ExportController {
|
||||
return &ExportController{state, columns}
|
||||
func NewExportController(
|
||||
state *State,
|
||||
tableService TableReadService,
|
||||
jobsController *JobsController,
|
||||
columns *ColumnsController,
|
||||
pasteboardProvider services.PasteboardProvider,
|
||||
) *ExportController {
|
||||
return &ExportController{state, tableService, jobsController, columns, pasteboardProvider}
|
||||
}
|
||||
|
||||
func (c *ExportController) ExportCSV(filename string) tea.Msg {
|
||||
func (c *ExportController) ExportCSV(filename string, opts ExportOptions) tea.Msg {
|
||||
resultSet := c.state.ResultSet()
|
||||
if resultSet == nil {
|
||||
return events.Error(errors.New("no result set"))
|
||||
}
|
||||
|
||||
f, err := os.Create(filename)
|
||||
if err != nil {
|
||||
return events.Error(errors.Wrapf(err, "cannot export to '%v'", filename))
|
||||
}
|
||||
defer f.Close()
|
||||
return NewJob(c.jobController, fmt.Sprintf("Exporting to %v…", filename), func(ctx context.Context) (int, error) {
|
||||
f, err := os.Create(filename)
|
||||
if err != nil {
|
||||
return 0, errors.Wrapf(err, "cannot export to '%v'", filename)
|
||||
}
|
||||
defer f.Close()
|
||||
|
||||
cw := csv.NewWriter(f)
|
||||
cw := csv.NewWriter(f)
|
||||
defer cw.Flush()
|
||||
|
||||
columns := c.columns.Columns().VisibleColumns()
|
||||
|
||||
colNames := make([]string, len(columns))
|
||||
for i, c := range columns {
|
||||
colNames[i] = c.Name
|
||||
}
|
||||
if err := cw.Write(colNames); err != nil {
|
||||
return 0, errors.Wrapf(err, "cannot export to '%v'", filename)
|
||||
}
|
||||
|
||||
totalRows := 0
|
||||
row := make([]string, len(columns))
|
||||
for {
|
||||
for _, item := range resultSet.Items() {
|
||||
for i, col := range columns {
|
||||
row[i], _ = attrutils.AttributeToString(col.Evaluator.EvaluateForItem(item))
|
||||
}
|
||||
if err := cw.Write(row); err != nil {
|
||||
return 0, errors.Wrapf(err, "cannot export to '%v'", filename)
|
||||
}
|
||||
}
|
||||
totalRows += len(resultSet.Items())
|
||||
|
||||
if !opts.AllResults || !resultSet.HasNextPage() {
|
||||
break
|
||||
}
|
||||
|
||||
jobs.PostUpdate(ctx, fmt.Sprintf("exported %d items", totalRows))
|
||||
resultSet, err = c.tableService.NextPage(ctx, resultSet)
|
||||
if err != nil {
|
||||
return 0, errors.Wrapf(err, "cannot get next page while exporting to '%v'", filename)
|
||||
}
|
||||
}
|
||||
|
||||
return totalRows, nil
|
||||
}).OnDone(func(rows int) tea.Msg {
|
||||
return events.StatusMsg(applyToN("Exported ", rows, "item", "items", " to "+filename))
|
||||
}).Submit()
|
||||
}
|
||||
|
||||
func (c *ExportController) ExportCSVToClipboard() tea.Msg {
|
||||
var bts bytes.Buffer
|
||||
|
||||
resultSet := c.state.ResultSet()
|
||||
if resultSet == nil {
|
||||
return errors.New("no result set")
|
||||
}
|
||||
|
||||
if err := c.exportCSV(&bts, c.columns.Columns().VisibleColumns(), resultSet); err != nil {
|
||||
return events.Error(err)
|
||||
}
|
||||
|
||||
if err := c.pasteboardProvider.WriteText(bts.Bytes()); err != nil {
|
||||
return events.Error(err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// TODO: this really needs to be a service!
|
||||
func (c *ExportController) ExportToWriter(w io.Writer, resultSet *models.ResultSet) error {
|
||||
return c.exportCSV(w, columns.NewColumnsFromResultSet(resultSet).Columns, resultSet)
|
||||
}
|
||||
|
||||
func (c *ExportController) exportCSV(w io.Writer, cols []columns.Column, resultSet *models.ResultSet) error {
|
||||
cw := csv.NewWriter(w)
|
||||
defer cw.Flush()
|
||||
|
||||
columns := c.columns.Columns().VisibleColumns()
|
||||
|
||||
colNames := make([]string, len(columns))
|
||||
for i, c := range columns {
|
||||
colNames := make([]string, len(cols))
|
||||
for i, c := range cols {
|
||||
colNames[i] = c.Name
|
||||
}
|
||||
if err := cw.Write(colNames); err != nil {
|
||||
return events.Error(errors.Wrapf(err, "cannot export to '%v'", filename))
|
||||
return errors.Wrap(err, "cannot export to clipboard")
|
||||
}
|
||||
|
||||
row := make([]string, len(columns))
|
||||
row := make([]string, len(cols))
|
||||
for _, item := range resultSet.Items() {
|
||||
for i, col := range columns {
|
||||
for i, col := range cols {
|
||||
row[i], _ = attrutils.AttributeToString(col.Evaluator.EvaluateForItem(item))
|
||||
}
|
||||
if err := cw.Write(row); err != nil {
|
||||
return events.Error(errors.Wrapf(err, "cannot export to '%v'", filename))
|
||||
return errors.Wrap(err, "cannot export to clipboard")
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
type ExportOptions struct {
|
||||
// AllResults returns all results from the table
|
||||
AllResults bool
|
||||
}
|
||||
|
|
|
@ -1,6 +1,9 @@
|
|||
package controllers_test
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"github.com/lmika/dynamo-browse/internal/common/sliceutils"
|
||||
"github.com/lmika/dynamo-browse/internal/dynamo-browse/controllers"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"os"
|
||||
"strings"
|
||||
|
@ -14,7 +17,7 @@ func TestExportController_ExportCSV(t *testing.T) {
|
|||
tempFile := tempFile(t)
|
||||
|
||||
invokeCommand(t, srv.readController.Init())
|
||||
invokeCommand(t, srv.exportController.ExportCSV(tempFile))
|
||||
invokeCommand(t, srv.exportController.ExportCSV(tempFile, controllers.ExportOptions{}))
|
||||
|
||||
bts, err := os.ReadFile(tempFile)
|
||||
assert.NoError(t, err)
|
||||
|
@ -27,13 +30,66 @@ func TestExportController_ExportCSV(t *testing.T) {
|
|||
}, ""))
|
||||
})
|
||||
|
||||
t.Run("should export all pages of the results", func(t *testing.T) {
|
||||
pageLimits := []int{5, 10, 50}
|
||||
|
||||
for _, pageLimit := range pageLimits {
|
||||
t.Run(fmt.Sprintf("page size %d", pageLimit), func(t *testing.T) {
|
||||
t.Run("all results", func(t *testing.T) {
|
||||
srv := newService(t, serviceConfig{tableName: "count-to-30", defaultLimit: 5})
|
||||
|
||||
tempFile := tempFile(t)
|
||||
|
||||
expected := append([]string{
|
||||
"pk,sk,num\n",
|
||||
}, sliceutils.Generate(1, 30, func(i int) string {
|
||||
return fmt.Sprintf("NUM,NUM#%02d,%d\n", i, i)
|
||||
})...)
|
||||
|
||||
invokeCommand(t, srv.readController.Init())
|
||||
invokeCommand(t, srv.exportController.ExportCSV(tempFile, controllers.ExportOptions{
|
||||
AllResults: true,
|
||||
}))
|
||||
|
||||
bts, err := os.ReadFile(tempFile)
|
||||
assert.NoError(t, err)
|
||||
|
||||
assert.Equal(t, strings.Join(expected, ""), string(bts))
|
||||
})
|
||||
|
||||
t.Run("with query", func(t *testing.T) {
|
||||
srv := newService(t, serviceConfig{tableName: "count-to-30", defaultLimit: 5})
|
||||
|
||||
tempFile := tempFile(t)
|
||||
|
||||
expected := append([]string{
|
||||
"pk,sk,num\n",
|
||||
}, sliceutils.Generate(1, 15, func(i int) string {
|
||||
return fmt.Sprintf("NUM,NUM#%02d,%d\n", i, i)
|
||||
})...)
|
||||
|
||||
invokeCommand(t, srv.readController.Init())
|
||||
invokeCommandWithPrompt(t, srv.readController.PromptForQuery(), "num<=15")
|
||||
invokeCommand(t, srv.exportController.ExportCSV(tempFile, controllers.ExportOptions{
|
||||
AllResults: true,
|
||||
}))
|
||||
|
||||
bts, err := os.ReadFile(tempFile)
|
||||
assert.NoError(t, err)
|
||||
|
||||
assert.Equal(t, strings.Join(expected, ""), string(bts))
|
||||
})
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("should return error if result set is not set", func(t *testing.T) {
|
||||
srv := newService(t, serviceConfig{tableName: "non-existant-table"})
|
||||
|
||||
tempFile := tempFile(t)
|
||||
|
||||
invokeCommandExpectingError(t, srv.readController.Init())
|
||||
invokeCommandExpectingError(t, srv.exportController.ExportCSV(tempFile))
|
||||
invokeCommandExpectingError(t, srv.exportController.ExportCSV(tempFile, controllers.ExportOptions{}))
|
||||
})
|
||||
|
||||
t.Run("should honour new columns in CSV file", func(t *testing.T) {
|
||||
|
@ -48,7 +104,7 @@ func TestExportController_ExportCSV(t *testing.T) {
|
|||
invokeCommandWithPrompt(t, srv.columnsController.AddColumn(1), "address.street")
|
||||
invokeCommand(t, srv.columnsController.ShiftColumnLeft(1))
|
||||
|
||||
invokeCommand(t, srv.exportController.ExportCSV(tempFile))
|
||||
invokeCommand(t, srv.exportController.ExportCSV(tempFile, controllers.ExportOptions{}))
|
||||
|
||||
bts, err := os.ReadFile(tempFile)
|
||||
assert.NoError(t, err)
|
||||
|
@ -71,7 +127,7 @@ func TestExportController_ExportCSV(t *testing.T) {
|
|||
invokeCommand(t, srv.columnsController.ToggleVisible(1))
|
||||
invokeCommand(t, srv.columnsController.ToggleVisible(2))
|
||||
|
||||
invokeCommand(t, srv.exportController.ExportCSV(tempFile))
|
||||
invokeCommand(t, srv.exportController.ExportCSV(tempFile, controllers.ExportOptions{}))
|
||||
|
||||
bts, err := os.ReadFile(tempFile)
|
||||
assert.NoError(t, err)
|
||||
|
|
|
@ -2,10 +2,12 @@ package controllers
|
|||
|
||||
import (
|
||||
"context"
|
||||
"io/fs"
|
||||
|
||||
"github.com/aws/aws-sdk-go-v2/service/dynamodb/types"
|
||||
tea "github.com/charmbracelet/bubbletea"
|
||||
"github.com/lmika/audax/internal/dynamo-browse/models"
|
||||
"io/fs"
|
||||
"github.com/lmika/dynamo-browse/internal/dynamo-browse/models"
|
||||
"github.com/lmika/dynamo-browse/internal/dynamo-browse/models/relitems"
|
||||
)
|
||||
|
||||
type TableReadService interface {
|
||||
|
@ -33,3 +35,7 @@ type CustomKeyBindingSource interface {
|
|||
UnbindKey(key string)
|
||||
Rebind(bindingName string, newKey string) error
|
||||
}
|
||||
|
||||
type RelatedItemSupplier interface {
|
||||
RelatedItemOfItem(context.Context, *models.ResultSet, int) ([]relitems.RelatedItem, error)
|
||||
}
|
||||
|
|
|
@ -3,8 +3,8 @@ package controllers
|
|||
import (
|
||||
"context"
|
||||
tea "github.com/charmbracelet/bubbletea"
|
||||
"github.com/lmika/audax/internal/common/ui/events"
|
||||
"github.com/lmika/audax/internal/dynamo-browse/services/jobs"
|
||||
"github.com/lmika/dynamo-browse/internal/common/ui/events"
|
||||
"github.com/lmika/dynamo-browse/internal/dynamo-browse/services/jobs"
|
||||
)
|
||||
|
||||
func NewJob[T any](jc *JobsController, description string, job func(ctx context.Context) (T, error)) JobBuilder[T] {
|
||||
|
@ -51,6 +51,9 @@ func (jb JobBuilder[T]) executeJob(ctx context.Context) tea.Msg {
|
|||
if jb.onEither != nil {
|
||||
return jb.onEither(res, err)
|
||||
} else if err == nil {
|
||||
if jb.onDone == nil {
|
||||
return nil
|
||||
}
|
||||
return jb.onDone(res)
|
||||
} else {
|
||||
if jb.onErr != nil {
|
||||
|
|
|
@ -2,8 +2,8 @@ package controllers
|
|||
|
||||
import (
|
||||
tea "github.com/charmbracelet/bubbletea"
|
||||
"github.com/lmika/audax/internal/common/ui/events"
|
||||
"github.com/lmika/audax/internal/dynamo-browse/services/jobs"
|
||||
"github.com/lmika/dynamo-browse/internal/common/ui/events"
|
||||
"github.com/lmika/dynamo-browse/internal/dynamo-browse/services/jobs"
|
||||
bus "github.com/lmika/events"
|
||||
"log"
|
||||
)
|
||||
|
|
|
@ -3,8 +3,8 @@ package controllers
|
|||
import (
|
||||
"fmt"
|
||||
tea "github.com/charmbracelet/bubbletea"
|
||||
"github.com/lmika/audax/internal/common/ui/events"
|
||||
"github.com/lmika/audax/internal/dynamo-browse/services/keybindings"
|
||||
"github.com/lmika/dynamo-browse/internal/common/ui/events"
|
||||
"github.com/lmika/dynamo-browse/internal/dynamo-browse/services/keybindings"
|
||||
"github.com/pkg/errors"
|
||||
)
|
||||
|
||||
|
|
|
@ -3,21 +3,24 @@ package controllers
|
|||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
tea "github.com/charmbracelet/bubbletea"
|
||||
"github.com/lmika/audax/internal/common/ui/commandctrl"
|
||||
"github.com/lmika/audax/internal/common/ui/events"
|
||||
"github.com/lmika/audax/internal/dynamo-browse/models"
|
||||
"github.com/lmika/audax/internal/dynamo-browse/models/queryexpr"
|
||||
"github.com/lmika/audax/internal/dynamo-browse/services/scriptmanager"
|
||||
bus "github.com/lmika/events"
|
||||
"github.com/pkg/errors"
|
||||
"log"
|
||||
"strings"
|
||||
|
||||
tea "github.com/charmbracelet/bubbletea"
|
||||
"github.com/lmika/dynamo-browse/internal/common/ui/commandctrl"
|
||||
"github.com/lmika/dynamo-browse/internal/common/ui/events"
|
||||
"github.com/lmika/dynamo-browse/internal/dynamo-browse/models"
|
||||
"github.com/lmika/dynamo-browse/internal/dynamo-browse/models/queryexpr"
|
||||
"github.com/lmika/dynamo-browse/internal/dynamo-browse/models/relitems"
|
||||
"github.com/lmika/dynamo-browse/internal/dynamo-browse/services/scriptmanager"
|
||||
bus "github.com/lmika/events"
|
||||
"github.com/pkg/errors"
|
||||
)
|
||||
|
||||
type ScriptController struct {
|
||||
scriptManager *scriptmanager.Service
|
||||
tableReadController *TableReadController
|
||||
jobController *JobsController
|
||||
settingsController *SettingsController
|
||||
eventBus *bus.Bus
|
||||
sendMsg func(msg tea.Msg)
|
||||
|
@ -26,12 +29,14 @@ type ScriptController struct {
|
|||
func NewScriptController(
|
||||
scriptManager *scriptmanager.Service,
|
||||
tableReadController *TableReadController,
|
||||
jobController *JobsController,
|
||||
settingsController *SettingsController,
|
||||
eventBus *bus.Bus,
|
||||
) *ScriptController {
|
||||
sc := &ScriptController{
|
||||
scriptManager: scriptManager,
|
||||
tableReadController: tableReadController,
|
||||
jobController: jobController,
|
||||
settingsController: settingsController,
|
||||
eventBus: eventBus,
|
||||
}
|
||||
|
@ -61,13 +66,6 @@ func (sc *ScriptController) Init() {
|
|||
} else {
|
||||
log.Printf("warn: script lookup paths are invalid: %v", err)
|
||||
}
|
||||
sc.scriptManager.SetDefaultOptions(scriptmanager.Options{
|
||||
OSExecShell: "/bin/bash",
|
||||
Permissions: scriptmanager.Permissions{
|
||||
AllowShellCommands: true,
|
||||
AllowEnv: true,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
func (sc *ScriptController) SetMessageSender(sendMsg func(msg tea.Msg)) {
|
||||
|
@ -169,7 +167,6 @@ func (s *sessionImpl) SetResultSet(ctx context.Context, newResultSet *models.Res
|
|||
}
|
||||
|
||||
func (s *sessionImpl) Query(ctx context.Context, query string, opts scriptmanager.QueryOptions) (*models.ResultSet, error) {
|
||||
|
||||
// Parse the query
|
||||
expr, err := queryexpr.Parse(query)
|
||||
if err != nil {
|
||||
|
@ -182,12 +179,22 @@ func (s *sessionImpl) Query(ctx context.Context, query string, opts scriptmanage
|
|||
if opts.ValuePlaceholders != nil {
|
||||
expr = expr.WithValueParams(opts.ValuePlaceholders)
|
||||
}
|
||||
if opts.IndexName != "" {
|
||||
expr = expr.WithIndex(opts.IndexName)
|
||||
}
|
||||
|
||||
return s.sc.doQuery(ctx, expr, opts)
|
||||
}
|
||||
|
||||
func (s *ScriptController) doQuery(ctx context.Context, expr *queryexpr.QueryExpr, opts scriptmanager.QueryOptions) (*models.ResultSet, error) {
|
||||
// Get the table info
|
||||
var tableInfo *models.TableInfo
|
||||
var (
|
||||
tableInfo *models.TableInfo
|
||||
err error
|
||||
)
|
||||
|
||||
tableName := opts.TableName
|
||||
currentResultSet := s.sc.tableReadController.state.ResultSet()
|
||||
currentResultSet := s.tableReadController.state.ResultSet()
|
||||
|
||||
if tableName != "" {
|
||||
// Table specified. If it's the same as the current table, then use the existing table info
|
||||
|
@ -196,7 +203,7 @@ func (s *sessionImpl) Query(ctx context.Context, query string, opts scriptmanage
|
|||
}
|
||||
|
||||
// Otherwise, describe the table
|
||||
tableInfo, err = s.sc.tableReadController.tableService.Describe(ctx, tableName)
|
||||
tableInfo, err = s.tableReadController.tableService.Describe(ctx, tableName)
|
||||
if err != nil {
|
||||
return nil, errors.Wrapf(err, "cannot describe table '%v'", tableName)
|
||||
}
|
||||
|
@ -208,7 +215,7 @@ func (s *sessionImpl) Query(ctx context.Context, query string, opts scriptmanage
|
|||
tableInfo = currentResultSet.TableInfo
|
||||
}
|
||||
|
||||
newResultSet, err := s.sc.tableReadController.tableService.ScanOrQuery(ctx, tableInfo, expr, nil)
|
||||
newResultSet, err := s.tableReadController.tableService.ScanOrQuery(ctx, tableInfo, expr, nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
@ -244,3 +251,31 @@ func (sc *ScriptController) LookupBinding(theKey string) string {
|
|||
func (sc *ScriptController) UnbindKey(key string) {
|
||||
sc.scriptManager.UnbindKey(key)
|
||||
}
|
||||
|
||||
func (c *ScriptController) LookupRelatedItems(idx int) (res tea.Msg) {
|
||||
rs := c.tableReadController.state.ResultSet()
|
||||
|
||||
relItems, err := c.scriptManager.RelatedItemOfItem(context.Background(), rs, idx)
|
||||
if err != nil {
|
||||
return events.Error(err)
|
||||
} else if len(relItems) == 0 {
|
||||
return events.StatusMsg("No related items available")
|
||||
}
|
||||
|
||||
return ShowRelatedItemsOverlay{
|
||||
Items: relItems,
|
||||
OnSelected: func(item relitems.RelatedItem) tea.Msg {
|
||||
if item.OnSelect != nil {
|
||||
return item.OnSelect()
|
||||
}
|
||||
|
||||
return NewJob(c.jobController, "Running query…", func(ctx context.Context) (*models.ResultSet, error) {
|
||||
return c.doQuery(ctx, item.Query, scriptmanager.QueryOptions{
|
||||
TableName: item.Table,
|
||||
})
|
||||
}).OnDone(func(rs *models.ResultSet) tea.Msg {
|
||||
return c.tableReadController.setResultSetAndFilter(rs, "", true, resultSetUpdateQuery)
|
||||
}).Submit()
|
||||
},
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,12 +1,13 @@
|
|||
package controllers_test
|
||||
|
||||
import (
|
||||
"github.com/aws/aws-sdk-go-v2/service/dynamodb/types"
|
||||
"github.com/lmika/audax/internal/common/ui/events"
|
||||
"github.com/lmika/audax/internal/dynamo-browse/controllers"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/aws/aws-sdk-go-v2/service/dynamodb/types"
|
||||
"github.com/lmika/dynamo-browse/internal/common/ui/events"
|
||||
"github.com/lmika/dynamo-browse/internal/dynamo-browse/controllers"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestScriptController_RunScript(t *testing.T) {
|
||||
|
@ -53,7 +54,7 @@ func TestScriptController_RunScript(t *testing.T) {
|
|||
srv := newService(t, serviceConfig{
|
||||
tableName: "alpha-table",
|
||||
scriptFS: testScriptFile(t, "test.tm", `
|
||||
rs := session.query('pk="abc"').unwrap()
|
||||
rs := session.query('pk="abc"')
|
||||
ui.print(rs.length)
|
||||
`),
|
||||
})
|
||||
|
@ -72,7 +73,7 @@ func TestScriptController_RunScript(t *testing.T) {
|
|||
srv := newService(t, serviceConfig{
|
||||
tableName: "alpha-table",
|
||||
scriptFS: testScriptFile(t, "test.tm", `
|
||||
rs := session.query('pk!="abc"', { table: "count-to-30" }).unwrap()
|
||||
rs := session.query('pk!="abc"', { table: "count-to-30" })
|
||||
ui.print(rs.length)
|
||||
`),
|
||||
})
|
||||
|
@ -93,7 +94,7 @@ func TestScriptController_RunScript(t *testing.T) {
|
|||
srv := newService(t, serviceConfig{
|
||||
tableName: "alpha-table",
|
||||
scriptFS: testScriptFile(t, "test.tm", `
|
||||
rs := session.query('pk="abc"').unwrap()
|
||||
rs := session.query('pk="abc"')
|
||||
session.set_result_set(rs)
|
||||
`),
|
||||
})
|
||||
|
@ -112,7 +113,7 @@ func TestScriptController_RunScript(t *testing.T) {
|
|||
srv := newService(t, serviceConfig{
|
||||
tableName: "alpha-table",
|
||||
scriptFS: testScriptFile(t, "test.tm", `
|
||||
rs := session.query('pk="abc"').unwrap()
|
||||
rs := session.query('pk="abc"')
|
||||
rs[0].set_attr("pk", "131")
|
||||
session.set_result_set(rs)
|
||||
`),
|
||||
|
@ -135,22 +136,35 @@ func TestScriptController_RunScript(t *testing.T) {
|
|||
|
||||
func TestScriptController_LookupCommand(t *testing.T) {
|
||||
t.Run("should schedule the script on a separate go-routine", func(t *testing.T) {
|
||||
srv := newService(t, serviceConfig{
|
||||
tableName: "alpha-table",
|
||||
scriptFS: testScriptFile(t, "test.tm", `
|
||||
ext.command("mycommand", func(name) {
|
||||
ui.print("Hello, ", name)
|
||||
scenarios := []struct {
|
||||
descr string
|
||||
command string
|
||||
expectedOutput string
|
||||
}{
|
||||
{descr: "command with arg", command: "mycommand \"test name\"", expectedOutput: "Hello, test name"},
|
||||
{descr: "command no arg", command: "mycommand", expectedOutput: "Hello, nil value"},
|
||||
}
|
||||
|
||||
for _, scenario := range scenarios {
|
||||
t.Run(scenario.descr, func(t *testing.T) {
|
||||
srv := newService(t, serviceConfig{
|
||||
tableName: "alpha-table",
|
||||
scriptFS: testScriptFile(t, "test.tm", `
|
||||
ext.command("mycommand", func(name = "nil value") {
|
||||
ui.print(sprintf("Hello, %v", name))
|
||||
})
|
||||
`),
|
||||
})
|
||||
`),
|
||||
})
|
||||
|
||||
invokeCommand(t, srv.scriptController.LoadScript("test.tm"))
|
||||
invokeCommand(t, srv.commandController.Execute(`mycommand "test name"`))
|
||||
invokeCommand(t, srv.scriptController.LoadScript("test.tm"))
|
||||
invokeCommand(t, srv.commandController.Execute(scenario.command))
|
||||
|
||||
srv.msgSender.waitForAtLeastOneMessages(t, 5*time.Second)
|
||||
srv.msgSender.waitForAtLeastOneMessages(t, 5*time.Second)
|
||||
|
||||
assert.Len(t, srv.msgSender.msgs, 1)
|
||||
assert.Equal(t, events.StatusMsg("Hello, test name"), srv.msgSender.msgs[0])
|
||||
assert.Len(t, srv.msgSender.msgs, 1)
|
||||
assert.Equal(t, events.StatusMsg(scenario.expectedOutput), srv.msgSender.msgs[0])
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("should only allow one script to run at a time", func(t *testing.T) {
|
||||
|
|
|
@ -3,7 +3,7 @@ package controllers
|
|||
import (
|
||||
"fmt"
|
||||
tea "github.com/charmbracelet/bubbletea"
|
||||
"github.com/lmika/audax/internal/common/ui/events"
|
||||
"github.com/lmika/dynamo-browse/internal/common/ui/events"
|
||||
bus "github.com/lmika/events"
|
||||
"github.com/pkg/errors"
|
||||
"log"
|
||||
|
|
|
@ -1,8 +1,8 @@
|
|||
package controllers_test
|
||||
|
||||
import (
|
||||
"github.com/lmika/audax/internal/common/ui/events"
|
||||
"github.com/lmika/audax/internal/dynamo-browse/controllers"
|
||||
"github.com/lmika/dynamo-browse/internal/common/ui/events"
|
||||
"github.com/lmika/dynamo-browse/internal/dynamo-browse/controllers"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"testing"
|
||||
)
|
||||
|
|
|
@ -1,9 +1,8 @@
|
|||
package controllers
|
||||
|
||||
import (
|
||||
"github.com/lmika/dynamo-browse/internal/dynamo-browse/models"
|
||||
"sync"
|
||||
|
||||
"github.com/lmika/audax/internal/dynamo-browse/models"
|
||||
)
|
||||
|
||||
type State struct {
|
||||
|
|
|
@ -4,22 +4,24 @@ import (
|
|||
"bytes"
|
||||
"context"
|
||||
"fmt"
|
||||
"github.com/aws/aws-sdk-go-v2/service/dynamodb/types"
|
||||
tea "github.com/charmbracelet/bubbletea"
|
||||
"github.com/lmika/audax/internal/common/ui/events"
|
||||
"github.com/lmika/audax/internal/dynamo-browse/models"
|
||||
"github.com/lmika/audax/internal/dynamo-browse/models/attrcodec"
|
||||
"github.com/lmika/audax/internal/dynamo-browse/models/queryexpr"
|
||||
"github.com/lmika/audax/internal/dynamo-browse/models/serialisable"
|
||||
"github.com/lmika/audax/internal/dynamo-browse/services/inputhistory"
|
||||
"github.com/lmika/audax/internal/dynamo-browse/services/itemrenderer"
|
||||
"github.com/lmika/audax/internal/dynamo-browse/services/viewsnapshot"
|
||||
bus "github.com/lmika/events"
|
||||
"github.com/pkg/errors"
|
||||
"golang.design/x/clipboard"
|
||||
"log"
|
||||
"strings"
|
||||
"sync"
|
||||
|
||||
"github.com/aws/aws-sdk-go-v2/service/dynamodb/types"
|
||||
tea "github.com/charmbracelet/bubbletea"
|
||||
"github.com/lmika/dynamo-browse/internal/common/ui/events"
|
||||
"github.com/lmika/dynamo-browse/internal/dynamo-browse/models"
|
||||
"github.com/lmika/dynamo-browse/internal/dynamo-browse/models/attrcodec"
|
||||
"github.com/lmika/dynamo-browse/internal/dynamo-browse/models/attrutils"
|
||||
"github.com/lmika/dynamo-browse/internal/dynamo-browse/models/queryexpr"
|
||||
"github.com/lmika/dynamo-browse/internal/dynamo-browse/models/serialisable"
|
||||
"github.com/lmika/dynamo-browse/internal/dynamo-browse/services"
|
||||
"github.com/lmika/dynamo-browse/internal/dynamo-browse/services/inputhistory"
|
||||
"github.com/lmika/dynamo-browse/internal/dynamo-browse/services/itemrenderer"
|
||||
"github.com/lmika/dynamo-browse/internal/dynamo-browse/services/viewsnapshot"
|
||||
bus "github.com/lmika/events"
|
||||
"github.com/pkg/errors"
|
||||
)
|
||||
|
||||
type resultSetUpdateOp int
|
||||
|
@ -33,6 +35,7 @@ const (
|
|||
resultSetUpdateTouch
|
||||
resultSetUpdateNextPage
|
||||
resultSetUpdateScript
|
||||
resultSetUpdateResort
|
||||
)
|
||||
|
||||
type MarkOp int
|
||||
|
@ -57,11 +60,12 @@ type TableReadController struct {
|
|||
eventBus *bus.Bus
|
||||
tableName string
|
||||
loadFromLastView bool
|
||||
pasteboardProvider services.PasteboardProvider
|
||||
relatedItemSupplier RelatedItemSupplier
|
||||
|
||||
// state
|
||||
mutex *sync.Mutex
|
||||
state *State
|
||||
clipboardInit bool
|
||||
mutex *sync.Mutex
|
||||
state *State
|
||||
}
|
||||
|
||||
func NewTableReadController(
|
||||
|
@ -72,6 +76,8 @@ func NewTableReadController(
|
|||
jobController *JobsController,
|
||||
inputHistoryService *inputhistory.Service,
|
||||
eventBus *bus.Bus,
|
||||
pasteboardProvider services.PasteboardProvider,
|
||||
relatedItemSupplier RelatedItemSupplier,
|
||||
tableName string,
|
||||
) *TableReadController {
|
||||
return &TableReadController{
|
||||
|
@ -83,6 +89,8 @@ func NewTableReadController(
|
|||
inputHistoryService: inputHistoryService,
|
||||
eventBus: eventBus,
|
||||
tableName: tableName,
|
||||
pasteboardProvider: pasteboardProvider,
|
||||
relatedItemSupplier: relatedItemSupplier,
|
||||
mutex: new(sync.Mutex),
|
||||
}
|
||||
}
|
||||
|
@ -143,6 +151,13 @@ func (c *TableReadController) ScanTable(name string) tea.Msg {
|
|||
}).OnEither(c.handleResultSetFromJobResult(c.state.Filter(), true, false, resultSetUpdateInit)).Submit()
|
||||
}
|
||||
|
||||
func (c *TableReadController) SortResultSet(newCriteria models.SortCriteria) {
|
||||
c.state.withResultSet(func(rs *models.ResultSet) {
|
||||
rs.Sort(newCriteria.Append(models.PKSKSortFilter(rs.TableInfo)))
|
||||
})
|
||||
c.eventBus.Fire(newResultSetEvent, c.state.resultSet, resultSetUpdateResort)
|
||||
}
|
||||
|
||||
func (c *TableReadController) PromptForQuery() tea.Msg {
|
||||
return events.PromptForInputMsg{
|
||||
Prompt: "query: ",
|
||||
|
@ -276,13 +291,35 @@ func (c *TableReadController) setResultSetAndFilter(resultSet *models.ResultSet,
|
|||
return c.state.buildNewResultSetMessage("")
|
||||
}
|
||||
|
||||
func (c *TableReadController) Mark(op MarkOp) tea.Msg {
|
||||
c.state.withResultSet(func(resultSet *models.ResultSet) {
|
||||
for i := range resultSet.Items() {
|
||||
func (c *TableReadController) Mark(op MarkOp, where string) tea.Msg {
|
||||
var (
|
||||
whereExpr *queryexpr.QueryExpr
|
||||
err error
|
||||
)
|
||||
|
||||
if where != "" {
|
||||
whereExpr, err = queryexpr.Parse(where)
|
||||
if err != nil {
|
||||
return events.Error(err)
|
||||
}
|
||||
}
|
||||
|
||||
if err := c.state.withResultSetReturningError(func(resultSet *models.ResultSet) error {
|
||||
for i, item := range resultSet.Items() {
|
||||
if resultSet.Hidden(i) {
|
||||
continue
|
||||
}
|
||||
|
||||
if whereExpr != nil {
|
||||
res, err := whereExpr.EvalItem(item)
|
||||
if err != nil {
|
||||
return errors.Wrapf(err, "item %d", i)
|
||||
}
|
||||
if !attrutils.Truthy(res) {
|
||||
continue
|
||||
}
|
||||
}
|
||||
|
||||
switch op {
|
||||
case MarkOpMark:
|
||||
resultSet.SetMark(i, true)
|
||||
|
@ -292,7 +329,10 @@ func (c *TableReadController) Mark(op MarkOp) tea.Msg {
|
|||
resultSet.SetMark(i, !resultSet.Marked(i))
|
||||
}
|
||||
}
|
||||
})
|
||||
return nil
|
||||
}); err != nil {
|
||||
return events.Error(err)
|
||||
}
|
||||
return ResultSetUpdated{}
|
||||
}
|
||||
|
||||
|
@ -377,7 +417,7 @@ func (c *TableReadController) NextPage() tea.Msg {
|
|||
resultSet := c.state.ResultSet()
|
||||
if resultSet == nil {
|
||||
return events.StatusMsg("Result-set is nil")
|
||||
} else if resultSet.LastEvaluatedKey == nil {
|
||||
} else if !resultSet.HasNextPage() {
|
||||
return events.StatusMsg("No more results")
|
||||
}
|
||||
currentFilter := c.state.filter
|
||||
|
@ -446,12 +486,8 @@ func (c *TableReadController) updateViewToSnapshot(viewSnapshot *serialisable.Vi
|
|||
}
|
||||
|
||||
func (c *TableReadController) CopyItemToClipboard(idx int) tea.Msg {
|
||||
if err := c.initClipboard(); err != nil {
|
||||
return events.Error(err)
|
||||
}
|
||||
|
||||
itemCount := 0
|
||||
c.state.withResultSet(func(resultSet *models.ResultSet) {
|
||||
if err := c.state.withResultSetReturningError(func(resultSet *models.ResultSet) error {
|
||||
sb := new(strings.Builder)
|
||||
_ = applyToMarkedItems(resultSet, idx, func(idx int, item models.Item) error {
|
||||
if sb.Len() > 0 {
|
||||
|
@ -461,23 +497,14 @@ func (c *TableReadController) CopyItemToClipboard(idx int) tea.Msg {
|
|||
itemCount += 1
|
||||
return nil
|
||||
})
|
||||
clipboard.Write(clipboard.FmtText, []byte(sb.String()))
|
||||
})
|
||||
|
||||
if err := c.pasteboardProvider.WriteText([]byte(sb.String())); err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}); err != nil {
|
||||
return events.Error(err)
|
||||
}
|
||||
|
||||
return events.StatusMsg(applyToN("", itemCount, "item", "items", " copied to clipboard"))
|
||||
}
|
||||
|
||||
func (c *TableReadController) initClipboard() error {
|
||||
c.mutex.Lock()
|
||||
defer c.mutex.Unlock()
|
||||
|
||||
if c.clipboardInit {
|
||||
return nil
|
||||
}
|
||||
|
||||
if err := clipboard.Init(); err != nil {
|
||||
return errors.Wrap(err, "unable to enable clipboard")
|
||||
}
|
||||
c.clipboardInit = true
|
||||
return nil
|
||||
}
|
||||
|
|
|
@ -4,9 +4,9 @@ import (
|
|||
"fmt"
|
||||
"github.com/aws/aws-sdk-go-v2/service/dynamodb/types"
|
||||
tea "github.com/charmbracelet/bubbletea"
|
||||
"github.com/lmika/audax/internal/common/ui/events"
|
||||
"github.com/lmika/audax/internal/dynamo-browse/controllers"
|
||||
"github.com/lmika/audax/test/testdynamo"
|
||||
"github.com/lmika/dynamo-browse/internal/common/ui/events"
|
||||
"github.com/lmika/dynamo-browse/internal/dynamo-browse/controllers"
|
||||
"github.com/lmika/dynamo-browse/test/testdynamo"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"os"
|
||||
"strings"
|
||||
|
@ -89,7 +89,7 @@ func TestTableReadController_Query(t *testing.T) {
|
|||
|
||||
invokeCommand(t, srv.readController.Init())
|
||||
invokeCommandWithPrompts(t, srv.readController.PromptForQuery(), `pk ^= "abc"`)
|
||||
invokeCommand(t, srv.exportController.ExportCSV(tempFile))
|
||||
invokeCommand(t, srv.exportController.ExportCSV(tempFile, controllers.ExportOptions{}))
|
||||
|
||||
bts, err := os.ReadFile(tempFile)
|
||||
assert.NoError(t, err)
|
||||
|
@ -107,7 +107,7 @@ func TestTableReadController_Query(t *testing.T) {
|
|||
|
||||
invokeCommand(t, srv.readController.Init())
|
||||
invokeCommandWithPrompts(t, srv.readController.PromptForQuery(), `alpha = "This is some value"`)
|
||||
invokeCommand(t, srv.exportController.ExportCSV(tempFile))
|
||||
invokeCommand(t, srv.exportController.ExportCSV(tempFile, controllers.ExportOptions{}))
|
||||
|
||||
bts, err := os.ReadFile(tempFile)
|
||||
assert.NoError(t, err)
|
||||
|
@ -124,7 +124,7 @@ func TestTableReadController_Query(t *testing.T) {
|
|||
tempFile := tempFile(t)
|
||||
|
||||
invokeCommandExpectingError(t, srv.readController.Init())
|
||||
invokeCommandExpectingError(t, srv.exportController.ExportCSV(tempFile))
|
||||
invokeCommandExpectingError(t, srv.exportController.ExportCSV(tempFile, controllers.ExportOptions{}))
|
||||
})
|
||||
}
|
||||
|
||||
|
|
|
@ -5,11 +5,11 @@ import (
|
|||
"fmt"
|
||||
"github.com/aws/aws-sdk-go-v2/service/dynamodb/types"
|
||||
tea "github.com/charmbracelet/bubbletea"
|
||||
"github.com/lmika/audax/internal/common/sliceutils"
|
||||
"github.com/lmika/audax/internal/common/ui/events"
|
||||
"github.com/lmika/audax/internal/dynamo-browse/models"
|
||||
"github.com/lmika/audax/internal/dynamo-browse/models/queryexpr"
|
||||
"github.com/lmika/audax/internal/dynamo-browse/services/tables"
|
||||
"github.com/lmika/dynamo-browse/internal/common/sliceutils"
|
||||
"github.com/lmika/dynamo-browse/internal/common/ui/events"
|
||||
"github.com/lmika/dynamo-browse/internal/dynamo-browse/models"
|
||||
"github.com/lmika/dynamo-browse/internal/dynamo-browse/models/queryexpr"
|
||||
"github.com/lmika/dynamo-browse/internal/dynamo-browse/services/tables"
|
||||
"github.com/pkg/errors"
|
||||
"log"
|
||||
"strconv"
|
||||
|
@ -122,6 +122,8 @@ func (twc *TableWriteController) SetAttributeValue(idx int, itemType models.Item
|
|||
return twc.setBoolValue(idx, path)
|
||||
case models.NullItemType:
|
||||
return twc.setNullValue(idx, path)
|
||||
case models.ExprValueItemType:
|
||||
return twc.setToExpressionValue(idx, path)
|
||||
default:
|
||||
return events.Error(errors.New("unsupported attribute type"))
|
||||
}
|
||||
|
@ -151,6 +153,39 @@ func (twc *TableWriteController) setStringValue(idx int, attr *queryexpr.QueryEx
|
|||
}
|
||||
}
|
||||
|
||||
func (twc *TableWriteController) setToExpressionValue(idx int, attr *queryexpr.QueryExpr) tea.Msg {
|
||||
return events.PromptForInputMsg{
|
||||
Prompt: "expr value: ",
|
||||
OnDone: func(value string) tea.Msg {
|
||||
valueExpr, err := queryexpr.Parse(value)
|
||||
if err != nil {
|
||||
return events.Error(err)
|
||||
}
|
||||
|
||||
if err := twc.state.withResultSetReturningError(func(set *models.ResultSet) error {
|
||||
if err := applyToMarkedItems(set, idx, func(idx int, item models.Item) error {
|
||||
newValue, err := valueExpr.EvalItem(item)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if err := attr.SetEvalItem(item, newValue); err != nil {
|
||||
return err
|
||||
}
|
||||
set.SetDirty(idx, true)
|
||||
return nil
|
||||
}); err != nil {
|
||||
return err
|
||||
}
|
||||
set.RefreshColumns()
|
||||
return nil
|
||||
}); err != nil {
|
||||
return events.Error(err)
|
||||
}
|
||||
return ResultSetUpdated{}
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func (twc *TableWriteController) setNumberValue(idx int, attr *queryexpr.QueryExpr) tea.Msg {
|
||||
return events.PromptForInputMsg{
|
||||
Prompt: "number value: ",
|
||||
|
@ -239,12 +274,17 @@ func (twc *TableWriteController) DeleteAttribute(idx int, key string) tea.Msg {
|
|||
}
|
||||
|
||||
if err := twc.state.withResultSetReturningError(func(set *models.ResultSet) error {
|
||||
err := path.DeleteAttribute(set.Items()[idx])
|
||||
if err != nil {
|
||||
if err := applyToMarkedItems(set, idx, func(idx int, item models.Item) error {
|
||||
if err := path.DeleteAttribute(set.Items()[idx]); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
set.SetDirty(idx, true)
|
||||
return nil
|
||||
}); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
set.SetDirty(idx, true)
|
||||
set.RefreshColumns()
|
||||
return nil
|
||||
}); err != nil {
|
||||
|
@ -418,6 +458,44 @@ func (twc *TableWriteController) assertReadWrite() error {
|
|||
return nil
|
||||
}
|
||||
|
||||
func (twc *TableWriteController) CloneItem(idx int) tea.Msg {
|
||||
if err := twc.assertReadWrite(); err != nil {
|
||||
return events.Error(err)
|
||||
}
|
||||
|
||||
// Work out which keys we need to prompt for
|
||||
rs := twc.state.ResultSet()
|
||||
|
||||
keyPrompts := &promptSequence{
|
||||
prompts: []string{rs.TableInfo.Keys.PartitionKey + ": "},
|
||||
}
|
||||
if rs.TableInfo.Keys.SortKey != "" {
|
||||
keyPrompts.prompts = append(keyPrompts.prompts, rs.TableInfo.Keys.SortKey+": ")
|
||||
}
|
||||
keyPrompts.onAllDone = func(values []string) tea.Msg {
|
||||
twc.state.withResultSet(func(set *models.ResultSet) {
|
||||
applyToMarkedItems(set, idx, func(idx int, item models.Item) error {
|
||||
// TODO: should be a deep clone
|
||||
clonedItem := item.Clone()
|
||||
|
||||
clonedItem[rs.TableInfo.Keys.PartitionKey] = &types.AttributeValueMemberS{Value: values[0]}
|
||||
if len(values) == 2 {
|
||||
clonedItem[rs.TableInfo.Keys.SortKey] = &types.AttributeValueMemberS{Value: values[1]}
|
||||
}
|
||||
|
||||
set.AddNewItem(clonedItem, models.ItemAttribute{
|
||||
New: true,
|
||||
Dirty: true,
|
||||
})
|
||||
return nil
|
||||
})
|
||||
})
|
||||
return twc.state.buildNewResultSetMessage("New item cloned")
|
||||
}
|
||||
|
||||
return keyPrompts.next()
|
||||
}
|
||||
|
||||
func applyToN(prefix string, n int, singular, plural, suffix string) string {
|
||||
if n == 1 {
|
||||
return fmt.Sprintf("%v%v %v%v", prefix, n, singular, suffix)
|
||||
|
|
|
@ -4,21 +4,22 @@ import (
|
|||
"fmt"
|
||||
"github.com/aws/aws-sdk-go-v2/service/dynamodb/types"
|
||||
tea "github.com/charmbracelet/bubbletea"
|
||||
"github.com/lmika/audax/internal/common/ui/commandctrl"
|
||||
"github.com/lmika/audax/internal/dynamo-browse/controllers"
|
||||
"github.com/lmika/audax/internal/dynamo-browse/models"
|
||||
"github.com/lmika/audax/internal/dynamo-browse/providers/dynamo"
|
||||
"github.com/lmika/audax/internal/dynamo-browse/providers/inputhistorystore"
|
||||
"github.com/lmika/audax/internal/dynamo-browse/providers/settingstore"
|
||||
"github.com/lmika/audax/internal/dynamo-browse/providers/workspacestore"
|
||||
"github.com/lmika/audax/internal/dynamo-browse/services/inputhistory"
|
||||
"github.com/lmika/audax/internal/dynamo-browse/services/itemrenderer"
|
||||
"github.com/lmika/audax/internal/dynamo-browse/services/jobs"
|
||||
"github.com/lmika/audax/internal/dynamo-browse/services/scriptmanager"
|
||||
"github.com/lmika/audax/internal/dynamo-browse/services/tables"
|
||||
"github.com/lmika/audax/internal/dynamo-browse/services/viewsnapshot"
|
||||
"github.com/lmika/audax/test/testdynamo"
|
||||
"github.com/lmika/audax/test/testworkspace"
|
||||
"github.com/lmika/dynamo-browse/internal/common/ui/commandctrl"
|
||||
"github.com/lmika/dynamo-browse/internal/dynamo-browse/controllers"
|
||||
"github.com/lmika/dynamo-browse/internal/dynamo-browse/models"
|
||||
"github.com/lmika/dynamo-browse/internal/dynamo-browse/providers/dynamo"
|
||||
"github.com/lmika/dynamo-browse/internal/dynamo-browse/providers/inputhistorystore"
|
||||
"github.com/lmika/dynamo-browse/internal/dynamo-browse/providers/pasteboardprovider"
|
||||
"github.com/lmika/dynamo-browse/internal/dynamo-browse/providers/settingstore"
|
||||
"github.com/lmika/dynamo-browse/internal/dynamo-browse/providers/workspacestore"
|
||||
"github.com/lmika/dynamo-browse/internal/dynamo-browse/services/inputhistory"
|
||||
"github.com/lmika/dynamo-browse/internal/dynamo-browse/services/itemrenderer"
|
||||
"github.com/lmika/dynamo-browse/internal/dynamo-browse/services/jobs"
|
||||
"github.com/lmika/dynamo-browse/internal/dynamo-browse/services/scriptmanager"
|
||||
"github.com/lmika/dynamo-browse/internal/dynamo-browse/services/tables"
|
||||
"github.com/lmika/dynamo-browse/internal/dynamo-browse/services/viewsnapshot"
|
||||
"github.com/lmika/dynamo-browse/test/testdynamo"
|
||||
"github.com/lmika/dynamo-browse/test/testworkspace"
|
||||
bus "github.com/lmika/events"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"io/fs"
|
||||
|
@ -617,12 +618,23 @@ func newService(t *testing.T, cfg serviceConfig) *services {
|
|||
|
||||
state := controllers.NewState()
|
||||
jobsController := controllers.NewJobsController(jobs.NewService(eventBus), eventBus, true)
|
||||
readController := controllers.NewTableReadController(state, service, workspaceService, itemRendererService, jobsController, inputHistoryService, eventBus, cfg.tableName)
|
||||
readController := controllers.NewTableReadController(
|
||||
state,
|
||||
service,
|
||||
workspaceService,
|
||||
itemRendererService,
|
||||
jobsController,
|
||||
inputHistoryService,
|
||||
eventBus,
|
||||
pasteboardprovider.NilProvider{},
|
||||
nil,
|
||||
cfg.tableName,
|
||||
)
|
||||
writeController := controllers.NewTableWriteController(state, service, jobsController, readController, settingStore)
|
||||
settingsController := controllers.NewSettingsController(settingStore, eventBus)
|
||||
columnsController := controllers.NewColumnsController(eventBus)
|
||||
exportController := controllers.NewExportController(state, columnsController)
|
||||
scriptController := controllers.NewScriptController(scriptService, readController, settingsController, eventBus)
|
||||
columnsController := controllers.NewColumnsController(readController, eventBus)
|
||||
exportController := controllers.NewExportController(state, service, jobsController, columnsController, pasteboardprovider.NilProvider{})
|
||||
scriptController := controllers.NewScriptController(scriptService, readController, jobsController, settingsController, eventBus)
|
||||
|
||||
commandController := commandctrl.NewCommandController(inputHistoryService)
|
||||
commandController.AddCommandLookupExtension(scriptController)
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
package controllers
|
||||
|
||||
import "github.com/lmika/audax/internal/dynamo-browse/models"
|
||||
import "github.com/lmika/dynamo-browse/internal/dynamo-browse/models"
|
||||
|
||||
func applyToMarkedItems(rs *models.ResultSet, selectedIndex int, applyFn func(idx int, item models.Item) error) error {
|
||||
if markedItems := rs.MarkedItems(); len(markedItems) > 0 {
|
||||
|
|
|
@ -3,7 +3,7 @@ package attrcodec_test
|
|||
import (
|
||||
"bytes"
|
||||
"github.com/aws/aws-sdk-go-v2/service/dynamodb/types"
|
||||
"github.com/lmika/audax/internal/dynamo-browse/models/attrcodec"
|
||||
"github.com/lmika/dynamo-browse/internal/dynamo-browse/models/attrcodec"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"strings"
|
||||
"testing"
|
||||
|
|
32
internal/dynamo-browse/models/attrutils/truthy.go
Normal file
32
internal/dynamo-browse/models/attrutils/truthy.go
Normal 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
|
||||
}
|
|
@ -1,9 +1,7 @@
|
|||
package columns
|
||||
|
||||
import (
|
||||
"github.com/aws/aws-sdk-go-v2/service/dynamodb/types"
|
||||
"github.com/lmika/audax/internal/dynamo-browse/models"
|
||||
"github.com/lmika/audax/internal/dynamo-browse/models/queryexpr"
|
||||
"github.com/lmika/dynamo-browse/internal/dynamo-browse/models"
|
||||
)
|
||||
|
||||
type Columns struct {
|
||||
|
@ -19,7 +17,7 @@ func NewColumnsFromResultSet(rs *models.ResultSet) *Columns {
|
|||
for i, c := range rsCols {
|
||||
cols[i] = Column{
|
||||
Name: c,
|
||||
Evaluator: SimpleFieldValueEvaluator(c),
|
||||
Evaluator: models.SimpleFieldValueEvaluator(c),
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -44,7 +42,7 @@ func (cols *Columns) AddMissingColumns(rs *models.ResultSet) {
|
|||
if _, hasCol := existingColumns[c]; !hasCol {
|
||||
newCols = append(newCols, Column{
|
||||
Name: c,
|
||||
Evaluator: SimpleFieldValueEvaluator(c),
|
||||
Evaluator: models.SimpleFieldValueEvaluator(c),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
@ -56,7 +54,7 @@ func (cols *Columns) AddMissingColumns(rs *models.ResultSet) {
|
|||
} else {
|
||||
newCols[i] = Column{
|
||||
Name: c,
|
||||
Evaluator: SimpleFieldValueEvaluator(c),
|
||||
Evaluator: models.SimpleFieldValueEvaluator(c),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -82,25 +80,6 @@ func (cols *Columns) VisibleColumns() []Column {
|
|||
|
||||
type Column struct {
|
||||
Name string
|
||||
Evaluator FieldValueEvaluator
|
||||
Evaluator models.FieldValueEvaluator
|
||||
Hidden bool
|
||||
}
|
||||
|
||||
type FieldValueEvaluator interface {
|
||||
EvaluateForItem(item models.Item) types.AttributeValue
|
||||
}
|
||||
|
||||
type SimpleFieldValueEvaluator string
|
||||
|
||||
func (sfve SimpleFieldValueEvaluator) EvaluateForItem(item models.Item) types.AttributeValue {
|
||||
return item[string(sfve)]
|
||||
}
|
||||
|
||||
type ExprFieldValueEvaluator struct {
|
||||
Expr *queryexpr.QueryExpr
|
||||
}
|
||||
|
||||
func (sfve ExprFieldValueEvaluator) EvaluateForItem(item models.Item) types.AttributeValue {
|
||||
val, _ := sfve.Expr.EvalItem(item)
|
||||
return val
|
||||
}
|
||||
|
|
15
internal/dynamo-browse/models/evaluators.go
Normal file
15
internal/dynamo-browse/models/evaluators.go
Normal 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)]
|
||||
}
|
25
internal/dynamo-browse/models/evaluators/equals.go
Normal file
25
internal/dynamo-browse/models/evaluators/equals.go
Normal 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
|
||||
}
|
|
@ -2,7 +2,7 @@ package models
|
|||
|
||||
import (
|
||||
"github.com/aws/aws-sdk-go-v2/service/dynamodb/types"
|
||||
"github.com/lmika/audax/internal/dynamo-browse/models/attrutils"
|
||||
"github.com/lmika/dynamo-browse/internal/dynamo-browse/models/attrutils"
|
||||
)
|
||||
|
||||
type ItemIndex struct {
|
||||
|
@ -16,7 +16,7 @@ type Item map[string]types.AttributeValue
|
|||
func (i Item) Clone() Item {
|
||||
newItem := Item{}
|
||||
|
||||
// TODO: should be a deep clone?
|
||||
// TODO: should be a deep clone? YES!!
|
||||
for k, v := range i {
|
||||
newItem[k] = v
|
||||
}
|
||||
|
@ -33,6 +33,14 @@ func (i Item) KeyValue(info *TableInfo) map[string]types.AttributeValue {
|
|||
return itemKey
|
||||
}
|
||||
|
||||
func (i Item) PKSK(info *TableInfo) (pk types.AttributeValue, sk types.AttributeValue) {
|
||||
pk = i[info.Keys.PartitionKey]
|
||||
if info.Keys.SortKey != "" {
|
||||
sk = i[info.Keys.SortKey]
|
||||
}
|
||||
return pk, sk
|
||||
}
|
||||
|
||||
func (i Item) AttributeValueAsString(key string) (string, bool) {
|
||||
return attrutils.AttributeToString(i[key])
|
||||
}
|
||||
|
|
|
@ -8,4 +8,6 @@ const (
|
|||
NumberItemType ItemType = "N"
|
||||
BoolItemType ItemType = "BOOL"
|
||||
NullItemType ItemType = "NULL"
|
||||
|
||||
ExprValueItemType ItemType = "exprvalue"
|
||||
)
|
||||
|
|
|
@ -3,12 +3,14 @@ package models
|
|||
import (
|
||||
"github.com/aws/aws-sdk-go-v2/service/dynamodb/types"
|
||||
"sort"
|
||||
"time"
|
||||
)
|
||||
|
||||
type ResultSet struct {
|
||||
// Query information
|
||||
TableInfo *TableInfo
|
||||
Query Queryable
|
||||
Created time.Time
|
||||
ExclusiveStartKey map[string]types.AttributeValue
|
||||
|
||||
// Result information
|
||||
|
@ -16,7 +18,8 @@ type ResultSet struct {
|
|||
items []Item
|
||||
attributes []ItemAttribute
|
||||
|
||||
columns []string
|
||||
columns []string
|
||||
sortCriteria SortCriteria
|
||||
}
|
||||
|
||||
type Queryable interface {
|
||||
|
@ -46,6 +49,10 @@ func (rs *ResultSet) SetItems(items []Item) {
|
|||
rs.attributes = make([]ItemAttribute, len(items))
|
||||
}
|
||||
|
||||
func (rs *ResultSet) SortCriteria() SortCriteria {
|
||||
return rs.sortCriteria
|
||||
}
|
||||
|
||||
func (rs *ResultSet) AddNewItem(item Item, attrs ItemAttribute) {
|
||||
rs.items = append(rs.items, item)
|
||||
rs.attributes = append(rs.attributes, attrs)
|
||||
|
@ -135,3 +142,12 @@ func (rs *ResultSet) RefreshColumns() {
|
|||
|
||||
rs.columns = columns
|
||||
}
|
||||
|
||||
func (rs *ResultSet) HasNextPage() bool {
|
||||
return rs.LastEvaluatedKey != nil
|
||||
}
|
||||
|
||||
func (rs *ResultSet) Sort(criteria SortCriteria) {
|
||||
rs.sortCriteria = criteria
|
||||
Sort(rs.items, criteria)
|
||||
}
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
package modexpr
|
||||
|
||||
import "github.com/lmika/audax/internal/dynamo-browse/models"
|
||||
import "github.com/lmika/dynamo-browse/internal/dynamo-browse/models"
|
||||
|
||||
func (a *astExpr) calcPatchMods(item models.Item) ([]patchMod, error) {
|
||||
patchMods := make([]patchMod, 0)
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
package modexpr
|
||||
|
||||
import "github.com/lmika/audax/internal/dynamo-browse/models"
|
||||
import "github.com/lmika/dynamo-browse/internal/dynamo-browse/models"
|
||||
|
||||
type ModExpr struct {
|
||||
ast *astExpr
|
||||
|
|
|
@ -4,8 +4,8 @@ import (
|
|||
"testing"
|
||||
|
||||
"github.com/aws/aws-sdk-go-v2/service/dynamodb/types"
|
||||
"github.com/lmika/audax/internal/dynamo-browse/models"
|
||||
"github.com/lmika/audax/internal/dynamo-browse/models/modexpr"
|
||||
"github.com/lmika/dynamo-browse/internal/dynamo-browse/models"
|
||||
"github.com/lmika/dynamo-browse/internal/dynamo-browse/models/modexpr"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
|
|
|
@ -2,7 +2,7 @@ package modexpr
|
|||
|
||||
import (
|
||||
"github.com/aws/aws-sdk-go-v2/service/dynamodb/types"
|
||||
"github.com/lmika/audax/internal/dynamo-browse/models"
|
||||
"github.com/lmika/dynamo-browse/internal/dynamo-browse/models"
|
||||
)
|
||||
|
||||
type patchMod interface {
|
||||
|
|
|
@ -3,7 +3,7 @@ package models
|
|||
import (
|
||||
"github.com/aws/aws-sdk-go-v2/aws"
|
||||
"github.com/aws/aws-sdk-go-v2/feature/dynamodb/expression"
|
||||
"github.com/lmika/audax/internal/dynamo-browse/models/itemrender"
|
||||
"github.com/lmika/dynamo-browse/internal/dynamo-browse/models/itemrender"
|
||||
)
|
||||
|
||||
type QueryExecutionPlan struct {
|
||||
|
|
|
@ -4,17 +4,23 @@ import (
|
|||
"github.com/alecthomas/participle/v2"
|
||||
"github.com/alecthomas/participle/v2/lexer"
|
||||
"github.com/aws/aws-sdk-go-v2/feature/dynamodb/expression"
|
||||
"github.com/aws/aws-sdk-go-v2/service/dynamodb/types"
|
||||
"github.com/lmika/audax/internal/common/sliceutils"
|
||||
"github.com/lmika/audax/internal/dynamo-browse/models"
|
||||
"github.com/lmika/dynamo-browse/internal/common/sliceutils"
|
||||
"github.com/lmika/dynamo-browse/internal/dynamo-browse/models"
|
||||
"github.com/pkg/errors"
|
||||
"strconv"
|
||||
)
|
||||
|
||||
// Modelled on the expression language here
|
||||
// https://docs.aws.amazon.com/amazondynamodb/latest/developerguide/Expressions.OperatorsAndFunctions.html
|
||||
|
||||
type astExpr struct {
|
||||
Root *astDisjunction `parser:"@@"`
|
||||
Root *astDisjunction `parser:"@@"`
|
||||
Options *astOptions `parser:"( 'using' @@ )?"`
|
||||
}
|
||||
|
||||
type astOptions struct {
|
||||
Scan bool `parser:"@'scan'"`
|
||||
Index string `parser:" | 'index' '(' @String ')'"`
|
||||
}
|
||||
|
||||
type astDisjunction struct {
|
||||
|
@ -38,9 +44,15 @@ type astIn struct {
|
|||
}
|
||||
|
||||
type astComparisonOp struct {
|
||||
Ref *astEqualityOp `parser:"@@"`
|
||||
Op string `parser:"( @('<' | '<=' | '>' | '>=')"`
|
||||
Value *astEqualityOp `parser:"@@ )?"`
|
||||
Ref *astBetweenOp `parser:"@@"`
|
||||
Op string `parser:"( @('<' | '<=' | '>' | '>=')"`
|
||||
Value *astBetweenOp `parser:"@@ )?"`
|
||||
}
|
||||
|
||||
type astBetweenOp struct {
|
||||
Ref *astEqualityOp `parser:"@@"`
|
||||
From *astEqualityOp `parser:"( 'between' @@ "`
|
||||
To *astEqualityOp `parser:" 'and' @@ )?"`
|
||||
}
|
||||
|
||||
type astEqualityOp struct {
|
||||
|
@ -58,7 +70,6 @@ type astIsOp struct {
|
|||
type astSubRef struct {
|
||||
Ref *astFunctionCall `parser:"@@"`
|
||||
SubRefs []*astSubRefType `parser:"@@*"`
|
||||
//Quals []string `parser:"('.' @Ident)*"`
|
||||
}
|
||||
|
||||
type astSubRefType struct {
|
||||
|
@ -121,7 +132,58 @@ func Parse(expr string) (*QueryExpr, error) {
|
|||
return &QueryExpr{ast: ast}, nil
|
||||
}
|
||||
|
||||
func (a *astExpr) calcQuery(ctx *evalContext, info *models.TableInfo) (*models.QueryExecutionPlan, error) {
|
||||
func (a *astExpr) calcQuery(ctx *evalContext, info *models.TableInfo, preferredIndex string) (*models.QueryExecutionPlan, error) {
|
||||
plans, err := a.determinePlausibleExecutionPlans(ctx, info)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
scanPlan, _ := sliceutils.FindLast(plans, func(p *models.QueryExecutionPlan) bool {
|
||||
return !p.CanQuery
|
||||
})
|
||||
queryPlans := sliceutils.Filter(plans, func(p *models.QueryExecutionPlan) bool {
|
||||
if !p.CanQuery {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
})
|
||||
|
||||
if len(queryPlans) == 0 || (a.Options != nil && a.Options.Scan) {
|
||||
if preferredIndex != "" {
|
||||
return nil, NoPlausiblePlanWithIndexError{
|
||||
PreferredIndex: preferredIndex,
|
||||
PossibleIndices: sliceutils.Map(queryPlans, func(p *models.QueryExecutionPlan) string { return p.IndexName }),
|
||||
}
|
||||
}
|
||||
return scanPlan, nil
|
||||
}
|
||||
|
||||
if preferredIndex == "" && (a.Options != nil && a.Options.Index != "") {
|
||||
preferredIndex, err = strconv.Unquote(a.Options.Index)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
if preferredIndex != "" {
|
||||
queryPlans = sliceutils.Filter(queryPlans, func(p *models.QueryExecutionPlan) bool { return p.IndexName == preferredIndex })
|
||||
}
|
||||
if len(queryPlans) == 1 {
|
||||
return queryPlans[0], nil
|
||||
} else if len(queryPlans) == 0 {
|
||||
return nil, NoPlausiblePlanWithIndexError{
|
||||
PreferredIndex: preferredIndex,
|
||||
}
|
||||
}
|
||||
|
||||
return nil, MultiplePlansWithIndexError{
|
||||
PossibleIndices: sliceutils.Map(queryPlans, func(p *models.QueryExecutionPlan) string { return p.IndexName }),
|
||||
}
|
||||
}
|
||||
|
||||
func (a *astExpr) determinePlausibleExecutionPlans(ctx *evalContext, info *models.TableInfo) ([]*models.QueryExecutionPlan, error) {
|
||||
plans := make([]*models.QueryExecutionPlan, 0)
|
||||
|
||||
type queryTestAttempt struct {
|
||||
index string
|
||||
keysUnderTest models.KeyAttribute
|
||||
|
@ -153,11 +215,11 @@ func (a *astExpr) calcQuery(ctx *evalContext, info *models.TableInfo) (*models.Q
|
|||
return nil, err
|
||||
}
|
||||
|
||||
return &models.QueryExecutionPlan{
|
||||
plans = append(plans, &models.QueryExecutionPlan{
|
||||
CanQuery: true,
|
||||
IndexName: attempt.index,
|
||||
Expression: expr,
|
||||
}, nil
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -174,21 +236,22 @@ func (a *astExpr) calcQuery(ctx *evalContext, info *models.TableInfo) (*models.Q
|
|||
return nil, err
|
||||
}
|
||||
|
||||
return &models.QueryExecutionPlan{
|
||||
plans = append(plans, &models.QueryExecutionPlan{
|
||||
CanQuery: false,
|
||||
Expression: expr,
|
||||
}, nil
|
||||
})
|
||||
return plans, nil
|
||||
}
|
||||
|
||||
func (a *astExpr) evalToIR(ctx *evalContext, tableInfo *models.TableInfo) (irAtom, error) {
|
||||
return a.Root.evalToIR(ctx, tableInfo)
|
||||
}
|
||||
|
||||
func (a *astExpr) evalItem(ctx *evalContext, item models.Item) (types.AttributeValue, error) {
|
||||
func (a *astExpr) evalItem(ctx *evalContext, item models.Item) (exprValue, error) {
|
||||
return a.Root.evalItem(ctx, item)
|
||||
}
|
||||
|
||||
func (a *astExpr) setEvalItem(ctx *evalContext, item models.Item, value types.AttributeValue) error {
|
||||
func (a *astExpr) setEvalItem(ctx *evalContext, item models.Item, value exprValue) error {
|
||||
return a.Root.setEvalItem(ctx, item, value)
|
||||
}
|
||||
|
||||
|
|
|
@ -1,8 +1,7 @@
|
|||
package queryexpr
|
||||
|
||||
import (
|
||||
"github.com/aws/aws-sdk-go-v2/service/dynamodb/types"
|
||||
"github.com/lmika/audax/internal/dynamo-browse/models"
|
||||
"github.com/lmika/dynamo-browse/internal/dynamo-browse/models"
|
||||
"github.com/pkg/errors"
|
||||
)
|
||||
|
||||
|
@ -21,15 +20,6 @@ func (a *astAtom) evalToIR(ctx *evalContext, info *models.TableInfo) (irAtom, er
|
|||
return nil, errors.New("unhandled atom case")
|
||||
}
|
||||
|
||||
func (a *astAtom) rightOperandDynamoValue() (types.AttributeValue, error) {
|
||||
switch {
|
||||
case a.Literal != nil:
|
||||
return a.Literal.dynamoValue()
|
||||
}
|
||||
|
||||
return nil, errors.New("unhandled atom case")
|
||||
}
|
||||
|
||||
func (a *astAtom) unqualifiedName() (string, bool) {
|
||||
switch {
|
||||
case a.Ref != nil:
|
||||
|
@ -39,12 +29,12 @@ func (a *astAtom) unqualifiedName() (string, bool) {
|
|||
return "", false
|
||||
}
|
||||
|
||||
func (a *astAtom) evalItem(ctx *evalContext, item models.Item) (types.AttributeValue, error) {
|
||||
func (a *astAtom) evalItem(ctx *evalContext, item models.Item) (exprValue, error) {
|
||||
switch {
|
||||
case a.Ref != nil:
|
||||
return a.Ref.evalItem(ctx, item)
|
||||
case a.Literal != nil:
|
||||
return a.Literal.dynamoValue()
|
||||
return a.Literal.exprValue()
|
||||
case a.Placeholder != nil:
|
||||
return a.Placeholder.evalItem(ctx, item)
|
||||
case a.Paren != nil:
|
||||
|
@ -66,7 +56,7 @@ func (a *astAtom) canModifyItem(ctx *evalContext, item models.Item) bool {
|
|||
return false
|
||||
}
|
||||
|
||||
func (a *astAtom) setEvalItem(ctx *evalContext, item models.Item, value types.AttributeValue) error {
|
||||
func (a *astAtom) setEvalItem(ctx *evalContext, item models.Item, value exprValue) error {
|
||||
switch {
|
||||
case a.Ref != nil:
|
||||
return a.Ref.setEvalItem(ctx, item, value)
|
||||
|
|
155
internal/dynamo-browse/models/queryexpr/between.go
Normal file
155
internal/dynamo-browse/models/queryexpr/between.go
Normal 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
|
||||
}
|
|
@ -2,8 +2,7 @@ package queryexpr
|
|||
|
||||
import (
|
||||
"github.com/aws/aws-sdk-go-v2/feature/dynamodb/expression"
|
||||
"github.com/aws/aws-sdk-go-v2/service/dynamodb/types"
|
||||
"github.com/lmika/audax/internal/dynamo-browse/models"
|
||||
"github.com/lmika/dynamo-browse/internal/dynamo-browse/models"
|
||||
"strings"
|
||||
)
|
||||
|
||||
|
@ -20,7 +19,7 @@ func (a *astBooleanNot) evalToIR(ctx *evalContext, tableInfo *models.TableInfo)
|
|||
return &irBoolNot{atom: irNode}, nil
|
||||
}
|
||||
|
||||
func (a *astBooleanNot) evalItem(ctx *evalContext, item models.Item) (types.AttributeValue, error) {
|
||||
func (a *astBooleanNot) evalItem(ctx *evalContext, item models.Item) (exprValue, error) {
|
||||
val, err := a.Operand.evalItem(ctx, item)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
|
@ -30,7 +29,7 @@ func (a *astBooleanNot) evalItem(ctx *evalContext, item models.Item) (types.Attr
|
|||
return val, nil
|
||||
}
|
||||
|
||||
return &types.AttributeValueMemberBOOL{Value: !isAttributeTrue(val)}, nil
|
||||
return boolExprValue(!isAttributeTrue(val)), nil
|
||||
}
|
||||
|
||||
func (a *astBooleanNot) canModifyItem(ctx *evalContext, item models.Item) bool {
|
||||
|
@ -40,7 +39,7 @@ func (a *astBooleanNot) canModifyItem(ctx *evalContext, item models.Item) bool {
|
|||
return a.Operand.canModifyItem(ctx, item)
|
||||
}
|
||||
|
||||
func (a *astBooleanNot) setEvalItem(ctx *evalContext, item models.Item, value types.AttributeValue) error {
|
||||
func (a *astBooleanNot) setEvalItem(ctx *evalContext, item models.Item, value exprValue) error {
|
||||
if a.HasNot {
|
||||
return PathNotSettableError{}
|
||||
}
|
||||
|
|
|
@ -2,38 +2,127 @@ package queryexpr
|
|||
|
||||
import (
|
||||
"context"
|
||||
"github.com/aws/aws-sdk-go-v2/service/dynamodb/types"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
"strconv"
|
||||
)
|
||||
|
||||
type nativeFunc func(ctx context.Context, args []types.AttributeValue) (types.AttributeValue, error)
|
||||
type nativeFunc func(ctx context.Context, args []exprValue) (exprValue, error)
|
||||
|
||||
var nativeFuncs = map[string]nativeFunc{
|
||||
"size": func(ctx context.Context, args []types.AttributeValue) (types.AttributeValue, error) {
|
||||
"size": func(ctx context.Context, args []exprValue) (exprValue, error) {
|
||||
if len(args) != 1 {
|
||||
return nil, InvalidArgumentNumberError{Name: "size", Expected: 1, Actual: len(args)}
|
||||
}
|
||||
|
||||
var l int
|
||||
switch t := args[0].(type) {
|
||||
case *types.AttributeValueMemberB:
|
||||
l = len(t.Value)
|
||||
case *types.AttributeValueMemberS:
|
||||
l = len(t.Value)
|
||||
case *types.AttributeValueMemberL:
|
||||
l = len(t.Value)
|
||||
case *types.AttributeValueMemberM:
|
||||
l = len(t.Value)
|
||||
case *types.AttributeValueMemberSS:
|
||||
l = len(t.Value)
|
||||
case *types.AttributeValueMemberNS:
|
||||
l = len(t.Value)
|
||||
case *types.AttributeValueMemberBS:
|
||||
l = len(t.Value)
|
||||
case stringExprValue:
|
||||
l = len(t)
|
||||
case mappableExprValue:
|
||||
l = t.len()
|
||||
case slicableExprValue:
|
||||
l = t.len()
|
||||
default:
|
||||
return nil, errors.New("cannot take size of arg")
|
||||
}
|
||||
return &types.AttributeValueMemberN{Value: strconv.Itoa(l)}, nil
|
||||
return int64ExprValue(l), nil
|
||||
},
|
||||
|
||||
"range": func(ctx context.Context, args []exprValue) (exprValue, error) {
|
||||
if len(args) != 2 {
|
||||
return nil, InvalidArgumentNumberError{Name: "range", Expected: 2, Actual: len(args)}
|
||||
}
|
||||
|
||||
xVal, isXNum := args[0].(numberableExprValue)
|
||||
if !isXNum {
|
||||
return nil, InvalidArgumentTypeError{Name: "range", ArgIndex: 0, Expected: "N"}
|
||||
}
|
||||
yVal, isYNum := args[1].(numberableExprValue)
|
||||
if !isYNum {
|
||||
return nil, InvalidArgumentTypeError{Name: "range", ArgIndex: 1, Expected: "N"}
|
||||
}
|
||||
|
||||
xInt, _ := xVal.asBigFloat().Int64()
|
||||
yInt, _ := yVal.asBigFloat().Int64()
|
||||
xs := make([]exprValue, 0)
|
||||
for x := xInt; x <= yInt; x++ {
|
||||
xs = append(xs, int64ExprValue(x))
|
||||
}
|
||||
return listExprValue(xs), nil
|
||||
},
|
||||
|
||||
"marked": func(ctx context.Context, args []exprValue) (exprValue, error) {
|
||||
if len(args) != 1 {
|
||||
return nil, InvalidArgumentNumberError{Name: "marked", Expected: 1, Actual: len(args)}
|
||||
}
|
||||
|
||||
fieldName, ok := args[0].(stringableExprValue)
|
||||
if !ok {
|
||||
return nil, InvalidArgumentTypeError{Name: "marked", ArgIndex: 0, Expected: "S"}
|
||||
}
|
||||
|
||||
rs := currentResultSetFromContext(ctx)
|
||||
if rs == nil {
|
||||
return listExprValue{}, nil
|
||||
}
|
||||
|
||||
var items = []exprValue{}
|
||||
for i, itm := range rs.Items() {
|
||||
if !rs.Marked(i) {
|
||||
continue
|
||||
}
|
||||
|
||||
attr, hasAttr := itm[fieldName.asString()]
|
||||
if !hasAttr {
|
||||
continue
|
||||
}
|
||||
|
||||
exprAttrValue, err := newExprValueFromAttributeValue(attr)
|
||||
if err != nil {
|
||||
return nil, errors.Wrapf(err, "marked(): item %d, attr %v", i, fieldName.asString())
|
||||
}
|
||||
|
||||
items = append(items, exprAttrValue)
|
||||
}
|
||||
return listExprValue(items), nil
|
||||
},
|
||||
|
||||
"_x_now": func(ctx context.Context, args []exprValue) (exprValue, error) {
|
||||
now := timeSourceFromContext(ctx).now().Unix()
|
||||
return int64ExprValue(now), nil
|
||||
},
|
||||
|
||||
"_x_add": func(ctx context.Context, args []exprValue) (exprValue, error) {
|
||||
if len(args) != 2 {
|
||||
return nil, InvalidArgumentNumberError{Name: "_x_add", Expected: 2, Actual: len(args)}
|
||||
}
|
||||
|
||||
xVal, isXNum := args[0].(numberableExprValue)
|
||||
if !isXNum {
|
||||
return nil, InvalidArgumentTypeError{Name: "_x_add", ArgIndex: 0, Expected: "N"}
|
||||
}
|
||||
yVal, isYNum := args[1].(numberableExprValue)
|
||||
if !isYNum {
|
||||
return nil, InvalidArgumentTypeError{Name: "_x_add", ArgIndex: 1, Expected: "N"}
|
||||
}
|
||||
|
||||
return bigNumExprValue{num: xVal.asBigFloat().Add(xVal.asBigFloat(), yVal.asBigFloat())}, nil
|
||||
},
|
||||
|
||||
"_x_concat": func(ctx context.Context, args []exprValue) (exprValue, error) {
|
||||
if len(args) != 2 {
|
||||
return nil, InvalidArgumentNumberError{Name: "_x_concat", Expected: 2, Actual: len(args)}
|
||||
}
|
||||
|
||||
xVal, isXNum := args[0].(stringableExprValue)
|
||||
if !isXNum {
|
||||
return nil, InvalidArgumentTypeError{Name: "_x_concat", ArgIndex: 0, Expected: "S"}
|
||||
}
|
||||
yVal, isYNum := args[1].(stringableExprValue)
|
||||
if !isYNum {
|
||||
return nil, InvalidArgumentTypeError{Name: "_x_concat", ArgIndex: 1, Expected: "S"}
|
||||
}
|
||||
|
||||
return stringExprValue(xVal.asString() + yVal.asString()), nil
|
||||
},
|
||||
}
|
||||
|
|
|
@ -2,9 +2,8 @@ package queryexpr
|
|||
|
||||
import (
|
||||
"github.com/aws/aws-sdk-go-v2/feature/dynamodb/expression"
|
||||
"github.com/aws/aws-sdk-go-v2/service/dynamodb/types"
|
||||
"github.com/lmika/audax/internal/dynamo-browse/models"
|
||||
"github.com/lmika/audax/internal/dynamo-browse/models/attrutils"
|
||||
"github.com/lmika/dynamo-browse/internal/dynamo-browse/models"
|
||||
"github.com/lmika/dynamo-browse/internal/dynamo-browse/models/attrutils"
|
||||
"github.com/pkg/errors"
|
||||
)
|
||||
|
||||
|
@ -47,7 +46,7 @@ func (a *astComparisonOp) evalToIR(ctx *evalContext, info *models.TableInfo) (ir
|
|||
return irGenericCmp{leftOpr, rightOpr, cmpType}, nil
|
||||
}
|
||||
|
||||
func (a *astComparisonOp) evalItem(ctx *evalContext, item models.Item) (types.AttributeValue, error) {
|
||||
func (a *astComparisonOp) evalItem(ctx *evalContext, item models.Item) (exprValue, error) {
|
||||
left, err := a.Ref.evalItem(ctx, item)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
|
@ -61,20 +60,21 @@ func (a *astComparisonOp) evalItem(ctx *evalContext, item models.Item) (types.At
|
|||
return nil, err
|
||||
}
|
||||
|
||||
cmp, isComparable := attrutils.CompareScalarAttributes(left, right)
|
||||
// TODO: use expr value here
|
||||
cmp, isComparable := attrutils.CompareScalarAttributes(left.asAttributeValue(), right.asAttributeValue())
|
||||
if !isComparable {
|
||||
return nil, ValuesNotComparable{Left: left, Right: right}
|
||||
return nil, ValuesNotComparable{Left: left.asAttributeValue(), Right: right.asAttributeValue()}
|
||||
}
|
||||
|
||||
switch opToCmdType[a.Op] {
|
||||
case cmpTypeLt:
|
||||
return &types.AttributeValueMemberBOOL{Value: cmp < 0}, nil
|
||||
return boolExprValue(cmp < 0), nil
|
||||
case cmpTypeLe:
|
||||
return &types.AttributeValueMemberBOOL{Value: cmp <= 0}, nil
|
||||
return boolExprValue(cmp <= 0), nil
|
||||
case cmpTypeGt:
|
||||
return &types.AttributeValueMemberBOOL{Value: cmp > 0}, nil
|
||||
return boolExprValue(cmp > 0), nil
|
||||
case cmpTypeGe:
|
||||
return &types.AttributeValueMemberBOOL{Value: cmp >= 0}, nil
|
||||
return boolExprValue(cmp >= 0), nil
|
||||
}
|
||||
return nil, errors.Errorf("unrecognised operator: %v", a.Op)
|
||||
}
|
||||
|
@ -86,7 +86,7 @@ func (a *astComparisonOp) canModifyItem(ctx *evalContext, item models.Item) bool
|
|||
return a.Ref.canModifyItem(ctx, item)
|
||||
}
|
||||
|
||||
func (a *astComparisonOp) setEvalItem(ctx *evalContext, item models.Item, value types.AttributeValue) error {
|
||||
func (a *astComparisonOp) setEvalItem(ctx *evalContext, item models.Item, value exprValue) error {
|
||||
if a.Op != "" {
|
||||
return PathNotSettableError{}
|
||||
}
|
||||
|
@ -143,34 +143,34 @@ func (a irKeyFieldCmp) canBeExecutedAsQuery(qci *queryCalcInfo) bool {
|
|||
|
||||
func (a irKeyFieldCmp) calcQueryForScan(info *models.TableInfo) (expression.ConditionBuilder, error) {
|
||||
nb := a.name.calcName(info)
|
||||
vb := a.value.goValue()
|
||||
vb := a.value.exprValue()
|
||||
|
||||
switch a.cmpType {
|
||||
case cmpTypeLt:
|
||||
return nb.LessThan(expression.Value(vb)), nil
|
||||
return nb.LessThan(buildExpressionFromValue(vb)), nil
|
||||
case cmpTypeLe:
|
||||
return nb.LessThanEqual(expression.Value(vb)), nil
|
||||
return nb.LessThanEqual(buildExpressionFromValue(vb)), nil
|
||||
case cmpTypeGt:
|
||||
return nb.GreaterThan(expression.Value(vb)), nil
|
||||
return nb.GreaterThan(buildExpressionFromValue(vb)), nil
|
||||
case cmpTypeGe:
|
||||
return nb.GreaterThanEqual(expression.Value(vb)), nil
|
||||
return nb.GreaterThanEqual(buildExpressionFromValue(vb)), nil
|
||||
}
|
||||
return expression.ConditionBuilder{}, errors.New("unsupported cmp type")
|
||||
}
|
||||
|
||||
func (a irKeyFieldCmp) calcQueryForQuery() (expression.KeyConditionBuilder, error) {
|
||||
keyName := a.name.keyName()
|
||||
vb := a.value.goValue()
|
||||
vb := a.value.exprValue()
|
||||
|
||||
switch a.cmpType {
|
||||
case cmpTypeLt:
|
||||
return expression.Key(keyName).LessThan(expression.Value(vb)), nil
|
||||
return expression.Key(keyName).LessThan(buildExpressionFromValue(vb)), nil
|
||||
case cmpTypeLe:
|
||||
return expression.Key(keyName).LessThanEqual(expression.Value(vb)), nil
|
||||
return expression.Key(keyName).LessThanEqual(buildExpressionFromValue(vb)), nil
|
||||
case cmpTypeGt:
|
||||
return expression.Key(keyName).GreaterThan(expression.Value(vb)), nil
|
||||
return expression.Key(keyName).GreaterThan(buildExpressionFromValue(vb)), nil
|
||||
case cmpTypeGe:
|
||||
return expression.Key(keyName).GreaterThanEqual(expression.Value(vb)), nil
|
||||
return expression.Key(keyName).GreaterThanEqual(buildExpressionFromValue(vb)), nil
|
||||
}
|
||||
return expression.KeyConditionBuilder{}, errors.New("unsupported cmp type")
|
||||
}
|
||||
|
|
|
@ -2,8 +2,8 @@ package queryexpr
|
|||
|
||||
import (
|
||||
"github.com/aws/aws-sdk-go-v2/feature/dynamodb/expression"
|
||||
"github.com/aws/aws-sdk-go-v2/service/dynamodb/types"
|
||||
"github.com/lmika/audax/internal/dynamo-browse/models"
|
||||
"github.com/lmika/dynamo-browse/internal/dynamo-browse/models"
|
||||
"math/big"
|
||||
"strings"
|
||||
)
|
||||
|
||||
|
@ -36,7 +36,7 @@ func (a *astConjunction) evalToIR(ctx *evalContext, tableInfo *models.TableInfo)
|
|||
return &irMultiConjunction{atoms: atoms}, nil
|
||||
}
|
||||
|
||||
func (a *astConjunction) evalItem(ctx *evalContext, item models.Item) (types.AttributeValue, error) {
|
||||
func (a *astConjunction) evalItem(ctx *evalContext, item models.Item) (exprValue, error) {
|
||||
val, err := a.Operands[0].evalItem(ctx, item)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
|
@ -47,7 +47,7 @@ func (a *astConjunction) evalItem(ctx *evalContext, item models.Item) (types.Att
|
|||
|
||||
for _, opr := range a.Operands[1:] {
|
||||
if !isAttributeTrue(val) {
|
||||
return &types.AttributeValueMemberBOOL{Value: false}, nil
|
||||
return boolExprValue(false), nil
|
||||
}
|
||||
|
||||
val, err = opr.evalItem(ctx, item)
|
||||
|
@ -56,7 +56,7 @@ func (a *astConjunction) evalItem(ctx *evalContext, item models.Item) (types.Att
|
|||
}
|
||||
}
|
||||
|
||||
return &types.AttributeValueMemberBOOL{Value: isAttributeTrue(val)}, nil
|
||||
return boolExprValue(isAttributeTrue(val)), nil
|
||||
}
|
||||
|
||||
func (a *astConjunction) canModifyItem(ctx *evalContext, item models.Item) bool {
|
||||
|
@ -67,7 +67,7 @@ func (a *astConjunction) canModifyItem(ctx *evalContext, item models.Item) bool
|
|||
return false
|
||||
}
|
||||
|
||||
func (a *astConjunction) setEvalItem(ctx *evalContext, item models.Item, value types.AttributeValue) error {
|
||||
func (a *astConjunction) setEvalItem(ctx *evalContext, item models.Item, value exprValue) error {
|
||||
if len(a.Operands) == 1 {
|
||||
return a.Operands[0].setEvalItem(ctx, item, value)
|
||||
}
|
||||
|
@ -168,16 +168,16 @@ func (d *irMultiConjunction) calcQueryForScan(info *models.TableInfo) (expressio
|
|||
return conjExpr, nil
|
||||
}
|
||||
|
||||
func isAttributeTrue(attr types.AttributeValue) bool {
|
||||
func isAttributeTrue(attr exprValue) bool {
|
||||
switch val := attr.(type) {
|
||||
case *types.AttributeValueMemberS:
|
||||
return val.Value != ""
|
||||
case *types.AttributeValueMemberN:
|
||||
return val.Value != "0"
|
||||
case *types.AttributeValueMemberBOOL:
|
||||
return val.Value
|
||||
case *types.AttributeValueMemberNULL:
|
||||
case nullExprValue:
|
||||
return false
|
||||
case boolExprValue:
|
||||
return bool(val)
|
||||
case stringableExprValue:
|
||||
return val.asString() != ""
|
||||
case numberableExprValue:
|
||||
return val.asBigFloat().Cmp(&big.Float{}) != 0
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
|
40
internal/dynamo-browse/models/queryexpr/context.go
Normal file
40
internal/dynamo-browse/models/queryexpr/context.go
Normal 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
|
||||
}
|
|
@ -2,8 +2,7 @@ package queryexpr
|
|||
|
||||
import (
|
||||
"github.com/aws/aws-sdk-go-v2/feature/dynamodb/expression"
|
||||
"github.com/aws/aws-sdk-go-v2/service/dynamodb/types"
|
||||
"github.com/lmika/audax/internal/dynamo-browse/models"
|
||||
"github.com/lmika/dynamo-browse/internal/dynamo-browse/models"
|
||||
"strings"
|
||||
)
|
||||
|
||||
|
@ -24,7 +23,7 @@ func (a *astDisjunction) evalToIR(ctx *evalContext, tableInfo *models.TableInfo)
|
|||
return &irDisjunction{conj: conj}, nil
|
||||
}
|
||||
|
||||
func (a *astDisjunction) evalItem(ctx *evalContext, item models.Item) (types.AttributeValue, error) {
|
||||
func (a *astDisjunction) evalItem(ctx *evalContext, item models.Item) (exprValue, error) {
|
||||
val, err := a.Operands[0].evalItem(ctx, item)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
|
@ -35,7 +34,7 @@ func (a *astDisjunction) evalItem(ctx *evalContext, item models.Item) (types.Att
|
|||
|
||||
for _, opr := range a.Operands[1:] {
|
||||
if isAttributeTrue(val) {
|
||||
return &types.AttributeValueMemberBOOL{Value: true}, nil
|
||||
return boolExprValue(true), nil
|
||||
}
|
||||
|
||||
val, err = opr.evalItem(ctx, item)
|
||||
|
@ -44,7 +43,7 @@ func (a *astDisjunction) evalItem(ctx *evalContext, item models.Item) (types.Att
|
|||
}
|
||||
}
|
||||
|
||||
return &types.AttributeValueMemberBOOL{Value: isAttributeTrue(val)}, nil
|
||||
return boolExprValue(isAttributeTrue(val)), nil
|
||||
}
|
||||
|
||||
func (a *astDisjunction) canModifyItem(ctx *evalContext, item models.Item) bool {
|
||||
|
@ -55,7 +54,7 @@ func (a *astDisjunction) canModifyItem(ctx *evalContext, item models.Item) bool
|
|||
return false
|
||||
}
|
||||
|
||||
func (a *astDisjunction) setEvalItem(ctx *evalContext, item models.Item, value types.AttributeValue) error {
|
||||
func (a *astDisjunction) setEvalItem(ctx *evalContext, item models.Item, value exprValue) error {
|
||||
if len(a.Operands) == 1 {
|
||||
return a.Operands[0].setEvalItem(ctx, item, value)
|
||||
}
|
||||
|
|
|
@ -3,8 +3,7 @@ package queryexpr
|
|||
import (
|
||||
"fmt"
|
||||
"github.com/aws/aws-sdk-go-v2/feature/dynamodb/expression"
|
||||
"github.com/aws/aws-sdk-go-v2/service/dynamodb/types"
|
||||
"github.com/lmika/audax/internal/dynamo-browse/models"
|
||||
"github.com/lmika/dynamo-browse/internal/dynamo-browse/models"
|
||||
"strings"
|
||||
)
|
||||
|
||||
|
@ -16,21 +15,21 @@ func (dt *astRef) unqualifiedName() (string, bool) {
|
|||
return dt.Name, true
|
||||
}
|
||||
|
||||
func (dt *astRef) evalItem(ctx *evalContext, item models.Item) (types.AttributeValue, error) {
|
||||
func (dt *astRef) evalItem(ctx *evalContext, item models.Item) (exprValue, error) {
|
||||
res, hasV := item[dt.Name]
|
||||
if !hasV {
|
||||
return nil, nil
|
||||
return undefinedExprValue{}, nil
|
||||
}
|
||||
|
||||
return res, nil
|
||||
return newExprValueFromAttributeValue(res)
|
||||
}
|
||||
|
||||
func (dt *astRef) canModifyItem(ctx *evalContext, item models.Item) bool {
|
||||
return true
|
||||
}
|
||||
|
||||
func (dt *astRef) setEvalItem(ctx *evalContext, item models.Item, value types.AttributeValue) error {
|
||||
item[dt.Name] = value
|
||||
func (dt *astRef) setEvalItem(ctx *evalContext, item models.Item, value exprValue) error {
|
||||
item[dt.Name] = value.asAttributeValue()
|
||||
return nil
|
||||
}
|
||||
|
||||
|
@ -71,7 +70,7 @@ func (i irNamePath) calcName(info *models.TableInfo) expression.NameBuilder {
|
|||
switch v := qual.(type) {
|
||||
case string:
|
||||
fullName.WriteString("." + v)
|
||||
case int:
|
||||
case int64:
|
||||
fullName.WriteString(fmt.Sprintf("[%v]", qual))
|
||||
}
|
||||
}
|
||||
|
|
|
@ -2,9 +2,8 @@ package queryexpr
|
|||
|
||||
import (
|
||||
"github.com/aws/aws-sdk-go-v2/feature/dynamodb/expression"
|
||||
"github.com/aws/aws-sdk-go-v2/service/dynamodb/types"
|
||||
"github.com/lmika/audax/internal/dynamo-browse/models"
|
||||
"github.com/lmika/audax/internal/dynamo-browse/models/attrutils"
|
||||
"github.com/lmika/dynamo-browse/internal/dynamo-browse/models"
|
||||
"github.com/lmika/dynamo-browse/internal/dynamo-browse/models/attrutils"
|
||||
"github.com/pkg/errors"
|
||||
"strings"
|
||||
)
|
||||
|
@ -59,7 +58,7 @@ func (a *astEqualityOp) evalToIR(ctx *evalContext, info *models.TableInfo) (irAt
|
|||
return nil, errors.Errorf("unrecognised operator: %v", a.Op)
|
||||
}
|
||||
|
||||
func (a *astEqualityOp) evalItem(ctx *evalContext, item models.Item) (types.AttributeValue, error) {
|
||||
func (a *astEqualityOp) evalItem(ctx *evalContext, item models.Item) (exprValue, error) {
|
||||
left, err := a.Ref.evalItem(ctx, item)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
|
@ -76,28 +75,28 @@ func (a *astEqualityOp) evalItem(ctx *evalContext, item models.Item) (types.Attr
|
|||
|
||||
switch a.Op {
|
||||
case "=":
|
||||
cmp, isComparable := attrutils.CompareScalarAttributes(left, right)
|
||||
cmp, isComparable := attrutils.CompareScalarAttributes(left.asAttributeValue(), right.asAttributeValue())
|
||||
if !isComparable {
|
||||
return nil, ValuesNotComparable{Left: left, Right: right}
|
||||
return nil, ValuesNotComparable{Left: left.asAttributeValue(), Right: right.asAttributeValue()}
|
||||
}
|
||||
return &types.AttributeValueMemberBOOL{Value: cmp == 0}, nil
|
||||
return boolExprValue(cmp == 0), nil
|
||||
case "!=":
|
||||
cmp, isComparable := attrutils.CompareScalarAttributes(left, right)
|
||||
cmp, isComparable := attrutils.CompareScalarAttributes(left.asAttributeValue(), right.asAttributeValue())
|
||||
if !isComparable {
|
||||
return nil, ValuesNotComparable{Left: left, Right: right}
|
||||
return nil, ValuesNotComparable{Left: left.asAttributeValue(), Right: right.asAttributeValue()}
|
||||
}
|
||||
return &types.AttributeValueMemberBOOL{Value: cmp != 0}, nil
|
||||
return boolExprValue(cmp != 0), nil
|
||||
case "^=":
|
||||
strValue, isStrValue := right.(*types.AttributeValueMemberS)
|
||||
strValue, isStrValue := right.(stringableExprValue)
|
||||
if !isStrValue {
|
||||
return nil, errors.New("operand '^=' must be string")
|
||||
}
|
||||
|
||||
leftAsStr, canBeString := attrutils.AttributeToString(left)
|
||||
leftAsStr, canBeString := left.(stringableExprValue)
|
||||
if !canBeString {
|
||||
return nil, ValueNotConvertableToString{Val: left}
|
||||
return nil, ValueNotConvertableToString{Val: left.asAttributeValue()}
|
||||
}
|
||||
return &types.AttributeValueMemberBOOL{Value: strings.HasPrefix(leftAsStr, strValue.Value)}, nil
|
||||
return boolExprValue(strings.HasPrefix(leftAsStr.asString(), strValue.asString())), nil
|
||||
}
|
||||
|
||||
return nil, errors.Errorf("unrecognised operator: %v", a.Op)
|
||||
|
@ -110,7 +109,7 @@ func (a *astEqualityOp) canModifyItem(ctx *evalContext, item models.Item) bool {
|
|||
return a.Ref.canModifyItem(ctx, item)
|
||||
}
|
||||
|
||||
func (a *astEqualityOp) setEvalItem(ctx *evalContext, item models.Item, value types.AttributeValue) error {
|
||||
func (a *astEqualityOp) setEvalItem(ctx *evalContext, item models.Item, value exprValue) error {
|
||||
if a.Op != "" {
|
||||
return PathNotSettableError{}
|
||||
}
|
||||
|
@ -157,8 +156,8 @@ func (a irKeyFieldEq) calcQueryForScan(info *models.TableInfo) (expression.Condi
|
|||
}
|
||||
|
||||
func (a irKeyFieldEq) calcQueryForQuery() (expression.KeyConditionBuilder, error) {
|
||||
vb := a.value.goValue()
|
||||
return expression.Key(a.name.keyName()).Equal(expression.Value(vb)), nil
|
||||
vb := a.value.exprValue()
|
||||
return expression.Key(a.name.keyName()).Equal(buildExpressionFromValue(vb)), nil
|
||||
}
|
||||
|
||||
type irGenericEq struct {
|
||||
|
@ -203,21 +202,21 @@ func (a irFieldBeginsWith) canBeExecutedAsQuery(qci *queryCalcInfo) bool {
|
|||
|
||||
func (a irFieldBeginsWith) calcQueryForScan(info *models.TableInfo) (expression.ConditionBuilder, error) {
|
||||
nb := a.name.calcName(info)
|
||||
vb := a.value.goValue()
|
||||
strValue, isStrValue := vb.(string)
|
||||
vb := a.value.exprValue()
|
||||
strValue, isStrValue := vb.(stringableExprValue)
|
||||
if !isStrValue {
|
||||
return expression.ConditionBuilder{}, errors.New("operand '^=' must be string")
|
||||
}
|
||||
|
||||
return nb.BeginsWith(strValue), nil
|
||||
return nb.BeginsWith(strValue.asString()), nil
|
||||
}
|
||||
|
||||
func (a irFieldBeginsWith) calcQueryForQuery() (expression.KeyConditionBuilder, error) {
|
||||
vb := a.value.goValue()
|
||||
strValue, isStrValue := vb.(string)
|
||||
vb := a.value.exprValue()
|
||||
strValue, isStrValue := vb.(stringableExprValue)
|
||||
if !isStrValue {
|
||||
return expression.KeyConditionBuilder{}, errors.New("operand '^=' must be string")
|
||||
}
|
||||
|
||||
return expression.Key(a.name.keyName()).BeginsWith(strValue), nil
|
||||
return expression.Key(a.name.keyName()).BeginsWith(strValue.asString()), nil
|
||||
}
|
||||
|
|
|
@ -3,8 +3,8 @@ package queryexpr
|
|||
import (
|
||||
"fmt"
|
||||
"github.com/aws/aws-sdk-go-v2/service/dynamodb/types"
|
||||
"github.com/lmika/audax/internal/dynamo-browse/models/attrutils"
|
||||
"github.com/lmika/audax/internal/dynamo-browse/models/itemrender"
|
||||
"github.com/lmika/dynamo-browse/internal/dynamo-browse/models/attrutils"
|
||||
"github.com/lmika/dynamo-browse/internal/dynamo-browse/models/itemrender"
|
||||
"strings"
|
||||
)
|
||||
|
||||
|
@ -69,6 +69,10 @@ type ValueNotConvertableToString struct {
|
|||
|
||||
func (n ValueNotConvertableToString) Error() string {
|
||||
render := itemrender.ToRenderer(n.Val)
|
||||
if render == nil {
|
||||
return "nil value is not convertable to string"
|
||||
}
|
||||
|
||||
return fmt.Sprintf("values '%v', type %v, is not convertable to string", render.StringValue(), render.TypeName())
|
||||
}
|
||||
|
||||
|
@ -98,6 +102,14 @@ func (n InvalidTypeForIsError) Error() string {
|
|||
return "invalid type for 'is': " + n.TypeName
|
||||
}
|
||||
|
||||
type InvalidTypeForBetweenError struct {
|
||||
TypeName string
|
||||
}
|
||||
|
||||
func (n InvalidTypeForBetweenError) Error() string {
|
||||
return "invalid type for 'between': " + n.TypeName
|
||||
}
|
||||
|
||||
type InvalidArgumentNumberError struct {
|
||||
Name string
|
||||
Expected int
|
||||
|
@ -108,6 +120,16 @@ func (e InvalidArgumentNumberError) Error() string {
|
|||
return fmt.Sprintf("function '%v' expected %v args but received %v", e.Name, e.Expected, e.Actual)
|
||||
}
|
||||
|
||||
type InvalidArgumentTypeError struct {
|
||||
Name string
|
||||
ArgIndex int
|
||||
Expected string
|
||||
}
|
||||
|
||||
func (e InvalidArgumentTypeError) Error() string {
|
||||
return fmt.Sprintf("function '%v' expected arg %v to be of type %v", e.Name, e.ArgIndex, e.Expected)
|
||||
}
|
||||
|
||||
type UnrecognisedFunctionError struct {
|
||||
Name string
|
||||
}
|
||||
|
@ -137,3 +159,20 @@ type ValueNotUsableAsASubref struct {
|
|||
func (e ValueNotUsableAsASubref) Error() string {
|
||||
return "value cannot be used as a subref"
|
||||
}
|
||||
|
||||
type MultiplePlansWithIndexError struct {
|
||||
PossibleIndices []string
|
||||
}
|
||||
|
||||
func (e MultiplePlansWithIndexError) Error() string {
|
||||
return fmt.Sprintf("multiple plans with index found. Specify index or scan with 'using' clause: possible indices are %v", e.PossibleIndices)
|
||||
}
|
||||
|
||||
type NoPlausiblePlanWithIndexError struct {
|
||||
PreferredIndex string
|
||||
PossibleIndices []string
|
||||
}
|
||||
|
||||
func (e NoPlausiblePlanWithIndexError) Error() string {
|
||||
return fmt.Sprintf("no plan with index '%v' found: possible indices are %v", e.PreferredIndex, e.PossibleIndices)
|
||||
}
|
||||
|
|
|
@ -3,25 +3,32 @@ package queryexpr
|
|||
import (
|
||||
"bytes"
|
||||
"encoding/gob"
|
||||
"hash/fnv"
|
||||
"io"
|
||||
|
||||
"github.com/aws/aws-sdk-go-v2/service/dynamodb/types"
|
||||
"github.com/lmika/audax/internal/dynamo-browse/models"
|
||||
"github.com/lmika/audax/internal/dynamo-browse/models/attrcodec"
|
||||
"github.com/lmika/audax/internal/dynamo-browse/models/attrutils"
|
||||
"github.com/lmika/dynamo-browse/internal/dynamo-browse/models"
|
||||
"github.com/lmika/dynamo-browse/internal/dynamo-browse/models/attrcodec"
|
||||
"github.com/lmika/dynamo-browse/internal/dynamo-browse/models/attrutils"
|
||||
"github.com/pkg/errors"
|
||||
"golang.org/x/exp/maps"
|
||||
"golang.org/x/exp/slices"
|
||||
"hash/fnv"
|
||||
"io"
|
||||
)
|
||||
|
||||
type QueryExpr struct {
|
||||
ast *astExpr
|
||||
names map[string]string
|
||||
values map[string]types.AttributeValue
|
||||
ast *astExpr
|
||||
index string
|
||||
names map[string]string
|
||||
values map[string]types.AttributeValue
|
||||
currentResultSet *models.ResultSet
|
||||
|
||||
// tests fields only
|
||||
timeSource timeSource
|
||||
}
|
||||
|
||||
type serializedExpr struct {
|
||||
Expr string
|
||||
Index string
|
||||
Names map[string]string
|
||||
Values []byte
|
||||
}
|
||||
|
@ -39,6 +46,7 @@ func DeserializeFrom(r io.Reader) (*QueryExpr, error) {
|
|||
}
|
||||
|
||||
qe.names = se.Names
|
||||
qe.index = se.Index
|
||||
|
||||
if len(se.Values) > 0 {
|
||||
vals, err := attrcodec.NewDecoder(bytes.NewReader(se.Values)).Decode()
|
||||
|
@ -56,7 +64,7 @@ func DeserializeFrom(r io.Reader) (*QueryExpr, error) {
|
|||
}
|
||||
|
||||
func (md *QueryExpr) SerializeTo(w io.Writer) error {
|
||||
se := serializedExpr{Expr: md.String(), Names: md.names}
|
||||
se := serializedExpr{Expr: md.String(), Index: md.index, Names: md.names}
|
||||
if md.values != nil {
|
||||
var bts bytes.Buffer
|
||||
if err := attrcodec.NewEncoder(&bts).Encode(&types.AttributeValueMemberM{Value: md.values}); err != nil {
|
||||
|
@ -90,6 +98,7 @@ func (md *QueryExpr) Equal(other *QueryExpr) bool {
|
|||
}
|
||||
|
||||
return md.ast.String() == other.ast.String() &&
|
||||
md.index == other.index &&
|
||||
maps.Equal(md.names, other.names) &&
|
||||
maps.EqualFunc(md.values, md.values, attrutils.Equals)
|
||||
}
|
||||
|
@ -104,6 +113,7 @@ func (md *QueryExpr) HashCode() uint64 {
|
|||
|
||||
h := fnv.New64a()
|
||||
h.Write([]byte(md.ast.String()))
|
||||
h.Write([]byte(md.index))
|
||||
|
||||
// the names must be in sorted order to maintain consistant key ordering
|
||||
if len(md.names) > 0 {
|
||||
|
@ -133,9 +143,11 @@ func (md *QueryExpr) HashCode() uint64 {
|
|||
|
||||
func (md *QueryExpr) WithNameParams(value map[string]string) *QueryExpr {
|
||||
return &QueryExpr{
|
||||
ast: md.ast,
|
||||
names: value,
|
||||
values: md.values,
|
||||
ast: md.ast,
|
||||
index: md.index,
|
||||
names: value,
|
||||
values: md.values,
|
||||
currentResultSet: md.currentResultSet,
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -157,18 +169,47 @@ func (md *QueryExpr) ValueParamOrNil(name string) types.AttributeValue {
|
|||
|
||||
func (md *QueryExpr) WithValueParams(value map[string]types.AttributeValue) *QueryExpr {
|
||||
return &QueryExpr{
|
||||
ast: md.ast,
|
||||
names: md.names,
|
||||
values: value,
|
||||
ast: md.ast,
|
||||
index: md.index,
|
||||
names: md.names,
|
||||
values: value,
|
||||
currentResultSet: md.currentResultSet,
|
||||
}
|
||||
}
|
||||
|
||||
func (md *QueryExpr) WithIndex(index string) *QueryExpr {
|
||||
return &QueryExpr{
|
||||
ast: md.ast,
|
||||
index: index,
|
||||
names: md.names,
|
||||
values: md.values,
|
||||
currentResultSet: md.currentResultSet,
|
||||
}
|
||||
}
|
||||
|
||||
func (md *QueryExpr) WithCurrentResultSet(currentResultSet *models.ResultSet) *QueryExpr {
|
||||
return &QueryExpr{
|
||||
ast: md.ast,
|
||||
index: md.index,
|
||||
names: md.names,
|
||||
values: md.values,
|
||||
currentResultSet: currentResultSet,
|
||||
}
|
||||
}
|
||||
|
||||
func (md *QueryExpr) Plan(tableInfo *models.TableInfo) (*models.QueryExecutionPlan, error) {
|
||||
return md.ast.calcQuery(md.evalContext(), tableInfo)
|
||||
return md.ast.calcQuery(md.evalContext(), tableInfo, md.index)
|
||||
}
|
||||
|
||||
func (md *QueryExpr) EvalItem(item models.Item) (types.AttributeValue, error) {
|
||||
return md.ast.evalItem(md.evalContext(), item)
|
||||
val, err := md.ast.evalItem(md.evalContext(), item)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if val == nil {
|
||||
return nil, nil
|
||||
}
|
||||
return val.asAttributeValue(), nil
|
||||
}
|
||||
|
||||
func (md *QueryExpr) DeleteAttribute(item models.Item) error {
|
||||
|
@ -176,7 +217,11 @@ func (md *QueryExpr) DeleteAttribute(item models.Item) error {
|
|||
}
|
||||
|
||||
func (md *QueryExpr) SetEvalItem(item models.Item, newValue types.AttributeValue) error {
|
||||
return md.ast.setEvalItem(md.evalContext(), item, newValue)
|
||||
val, err := newExprValueFromAttributeValue(newValue)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return md.ast.setEvalItem(md.evalContext(), item, val)
|
||||
}
|
||||
|
||||
func (md *QueryExpr) IsModifiablePath(item models.Item) bool {
|
||||
|
@ -187,6 +232,7 @@ func (md *QueryExpr) evalContext() *evalContext {
|
|||
return &evalContext{
|
||||
namePlaceholders: md.names,
|
||||
valuePlaceholders: md.values,
|
||||
ctxResultSet: md.currentResultSet,
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -237,6 +283,8 @@ type evalContext struct {
|
|||
nameLookup func(string) (string, bool)
|
||||
valuePlaceholders map[string]types.AttributeValue
|
||||
valueLookup func(string) (types.AttributeValue, bool)
|
||||
timeSource timeSource
|
||||
ctxResultSet *models.ResultSet
|
||||
}
|
||||
|
||||
func (ec *evalContext) lookupName(name string) (string, bool) {
|
||||
|
@ -264,3 +312,10 @@ func (ec *evalContext) lookupValue(name string) (types.AttributeValue, bool) {
|
|||
|
||||
return nil, false
|
||||
}
|
||||
|
||||
func (ec *evalContext) getTimeSource() timeSource {
|
||||
if ts := ec.timeSource; ts != nil {
|
||||
return ts
|
||||
}
|
||||
return defaultTimeSource{}
|
||||
}
|
||||
|
|
|
@ -3,12 +3,14 @@ package queryexpr_test
|
|||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/aws/aws-sdk-go-v2/aws"
|
||||
"github.com/aws/aws-sdk-go-v2/service/dynamodb/types"
|
||||
"github.com/lmika/audax/internal/dynamo-browse/models/queryexpr"
|
||||
"testing"
|
||||
"github.com/lmika/dynamo-browse/internal/dynamo-browse/models/queryexpr"
|
||||
|
||||
"github.com/lmika/audax/internal/dynamo-browse/models"
|
||||
"github.com/lmika/dynamo-browse/internal/dynamo-browse/models"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
|
@ -34,6 +36,13 @@ func TestModExpr_Query(t *testing.T) {
|
|||
SortKey: "sk",
|
||||
},
|
||||
},
|
||||
{
|
||||
Name: "with-apples-and-oranges",
|
||||
Keys: models.KeyAttribute{
|
||||
PartitionKey: "apples",
|
||||
SortKey: "oranges",
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
|
@ -44,6 +53,11 @@ func TestModExpr_Query(t *testing.T) {
|
|||
`#0 = :0`,
|
||||
exprNameIsString(0, 0, "pk", "prefix"),
|
||||
),
|
||||
//scanCase("when request pk is fixed (reverse)",
|
||||
// `prefix="pk"`,
|
||||
// `#0 = :0`,
|
||||
// exprNameIsString(0, 0, "pk", "prefix"),
|
||||
//),
|
||||
scanCase("when request pk is fixed in parens #1",
|
||||
`(pk="prefix")`,
|
||||
`#0 = :0`,
|
||||
|
@ -112,6 +126,13 @@ func TestModExpr_Query(t *testing.T) {
|
|||
exprNameIsString(0, 0, "pk", "prefix"),
|
||||
exprNameIsNumber(1, 1, "sk", "100"),
|
||||
),
|
||||
scanCase("when request pk is equals and sk is greater or equal to",
|
||||
`pk="prefix" and sk between 100 and 200`,
|
||||
`(#0 = :0) AND (#1 BETWEEN :1 AND :2)`,
|
||||
exprNameIsString(0, 0, "pk", "prefix"),
|
||||
exprNameIsNumber(1, 1, "sk", "100"),
|
||||
exprValueIsNumber(2, "200"),
|
||||
),
|
||||
|
||||
scanCase("with placeholders",
|
||||
`:partition=$valuePrefix and :sort=$valueAnother`,
|
||||
|
@ -149,6 +170,13 @@ func TestModExpr_Query(t *testing.T) {
|
|||
exprNameIsString(0, 0, "color", "yellow"),
|
||||
exprNameIsString(1, 1, "shade", "dark"),
|
||||
),
|
||||
|
||||
// Function calls
|
||||
scanCase("use the value of fn call in query",
|
||||
`pk = _x_concat("Hello ", "world")`,
|
||||
`#0 = :0`,
|
||||
exprNameIsString(0, 0, "pk", "Hello world"),
|
||||
),
|
||||
}
|
||||
|
||||
for _, scenario := range scenarios {
|
||||
|
@ -238,6 +266,13 @@ func TestModExpr_Query(t *testing.T) {
|
|||
exprNameIsString(0, 0, "pk", "prefix"),
|
||||
),
|
||||
|
||||
scanCase("with between", `pk between "a" and "z"`,
|
||||
`#0 BETWEEN :0 AND :1`,
|
||||
exprName(0, "pk"),
|
||||
exprValueIsString(0, "a"),
|
||||
exprValueIsString(1, "z"),
|
||||
),
|
||||
|
||||
scanCase("with in", `pk in ("alpha", "bravo", "charlie")`,
|
||||
`#0 IN (:0, :1, :2)`,
|
||||
exprName(0, "pk"),
|
||||
|
@ -374,6 +409,53 @@ func TestModExpr_Query(t *testing.T) {
|
|||
})
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("with index clash", func(t *testing.T) {
|
||||
t.Run("should return error if attempt to run query with two indices that can be chosen", func(t *testing.T) {
|
||||
modExpr, err := queryexpr.Parse(`apples="this"`)
|
||||
assert.NoError(t, err)
|
||||
|
||||
_, err = modExpr.Plan(tableInfo)
|
||||
assert.Error(t, err)
|
||||
})
|
||||
|
||||
t.Run("should run as scan if explicitly forced to", func(t *testing.T) {
|
||||
modExpr, err := queryexpr.Parse(`apples="this" using scan`)
|
||||
assert.NoError(t, err)
|
||||
|
||||
plan, err := modExpr.Plan(tableInfo)
|
||||
assert.NoError(t, err)
|
||||
assert.False(t, plan.CanQuery)
|
||||
})
|
||||
|
||||
t.Run("should run as query with the 'with-apples' index", func(t *testing.T) {
|
||||
modExpr, err := queryexpr.Parse(`apples="this" using index("with-apples")`)
|
||||
assert.NoError(t, err)
|
||||
|
||||
plan, err := modExpr.Plan(tableInfo)
|
||||
assert.NoError(t, err)
|
||||
assert.True(t, plan.CanQuery)
|
||||
assert.Equal(t, "with-apples", plan.IndexName)
|
||||
})
|
||||
|
||||
t.Run("should run as query with the 'with-apples-and-oranges' index", func(t *testing.T) {
|
||||
modExpr, err := queryexpr.Parse(`apples="this" using index("with-apples-and-oranges")`)
|
||||
assert.NoError(t, err)
|
||||
|
||||
plan, err := modExpr.Plan(tableInfo)
|
||||
assert.NoError(t, err)
|
||||
assert.True(t, plan.CanQuery)
|
||||
assert.Equal(t, "with-apples-and-oranges", plan.IndexName)
|
||||
})
|
||||
|
||||
t.Run("should return error if the chosen index can't be used", func(t *testing.T) {
|
||||
modExpr, err := queryexpr.Parse(`apples="this" using index("with-missing")`)
|
||||
assert.NoError(t, err)
|
||||
|
||||
_, err = modExpr.Plan(tableInfo)
|
||||
assert.Error(t, err)
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
func TestQueryExpr_EvalItem(t *testing.T) {
|
||||
|
@ -395,7 +477,9 @@ func TestQueryExpr_EvalItem(t *testing.T) {
|
|||
&types.AttributeValueMemberN{Value: "7"},
|
||||
},
|
||||
},
|
||||
"one": &types.AttributeValueMemberN{Value: "1"},
|
||||
"three": &types.AttributeValueMemberN{Value: "3"},
|
||||
"five": &types.AttributeValueMemberN{Value: "5"},
|
||||
}
|
||||
)
|
||||
|
||||
|
@ -433,6 +517,20 @@ func TestQueryExpr_EvalItem(t *testing.T) {
|
|||
{expr: "three < 2", expected: &types.AttributeValueMemberBOOL{Value: false}},
|
||||
{expr: "three <= 2", expected: &types.AttributeValueMemberBOOL{Value: false}},
|
||||
|
||||
// Between
|
||||
{expr: "3 between 1 and 5", expected: &types.AttributeValueMemberBOOL{Value: true}},
|
||||
{expr: "three between 1 and 5", expected: &types.AttributeValueMemberBOOL{Value: true}},
|
||||
{expr: "three between one and five", expected: &types.AttributeValueMemberBOOL{Value: true}},
|
||||
{expr: "three between 10 and 15", expected: &types.AttributeValueMemberBOOL{Value: false}},
|
||||
{expr: "three between 1 and 2", expected: &types.AttributeValueMemberBOOL{Value: false}},
|
||||
{expr: "8 between five and 10", expected: &types.AttributeValueMemberBOOL{Value: true}},
|
||||
{expr: "three between 1 and 3", expected: &types.AttributeValueMemberBOOL{Value: true}},
|
||||
{expr: "three between 3 and 5", expected: &types.AttributeValueMemberBOOL{Value: true}},
|
||||
|
||||
{expr: `"e" between "a" and "z"`, expected: &types.AttributeValueMemberBOOL{Value: true}},
|
||||
{expr: `"eee" between "aaa" and "zzz"`, expected: &types.AttributeValueMemberBOOL{Value: true}},
|
||||
{expr: `"e" between "between" and "beyond"`, expected: &types.AttributeValueMemberBOOL{Value: false}},
|
||||
|
||||
// In
|
||||
{expr: "three in (2, 3, 4, 5)", expected: &types.AttributeValueMemberBOOL{Value: true}},
|
||||
{expr: "three in (20, 30, 40)", expected: &types.AttributeValueMemberBOOL{Value: false}},
|
||||
|
@ -509,6 +607,50 @@ func TestQueryExpr_EvalItem(t *testing.T) {
|
|||
}
|
||||
})
|
||||
|
||||
t.Run("functions", func(t *testing.T) {
|
||||
timeNow := time.Now()
|
||||
|
||||
contextResultSet := models.ResultSet{}
|
||||
contextResultSet.SetItems([]models.Item{
|
||||
{"pk": &types.AttributeValueMemberS{Value: "1"}, "num": &types.AttributeValueMemberN{Value: "1"}},
|
||||
{"pk": &types.AttributeValueMemberS{Value: "2"}, "num": &types.AttributeValueMemberN{Value: "2"}},
|
||||
{"pk": &types.AttributeValueMemberS{Value: "3"}, "num": &types.AttributeValueMemberN{Value: "3"}},
|
||||
{"pk": &types.AttributeValueMemberS{Value: "4"}, "num": &types.AttributeValueMemberN{Value: "4"}},
|
||||
})
|
||||
contextResultSet.SetMark(0, true)
|
||||
contextResultSet.SetMark(1, true)
|
||||
|
||||
scenarios := []struct {
|
||||
expr string
|
||||
expected types.AttributeValue
|
||||
}{
|
||||
// _x_now() -- unreleased version of now
|
||||
{expr: `_x_now()`, expected: &types.AttributeValueMemberN{Value: fmt.Sprint(timeNow.Unix())}},
|
||||
|
||||
// Marked
|
||||
{expr: `marked("num")`, expected: &types.AttributeValueMemberL{Value: []types.AttributeValue{
|
||||
&types.AttributeValueMemberN{Value: "1"},
|
||||
&types.AttributeValueMemberN{Value: "2"},
|
||||
}}},
|
||||
{expr: `one in marked("num")`, expected: &types.AttributeValueMemberBOOL{Value: true}},
|
||||
{expr: `three in marked("num")`, expected: &types.AttributeValueMemberBOOL{Value: false}},
|
||||
}
|
||||
for _, scenario := range scenarios {
|
||||
t.Run(scenario.expr, func(t *testing.T) {
|
||||
modExpr, err := queryexpr.Parse(scenario.expr)
|
||||
assert.NoError(t, err)
|
||||
|
||||
res, err := modExpr.
|
||||
WithTestTimeSource(timeNow).
|
||||
WithCurrentResultSet(&contextResultSet).
|
||||
EvalItem(item)
|
||||
assert.NoError(t, err)
|
||||
|
||||
assert.Equal(t, scenario.expected, res)
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("unparsed expression", func(t *testing.T) {
|
||||
scenarios := []struct {
|
||||
expr string
|
||||
|
@ -532,6 +674,10 @@ func TestQueryExpr_EvalItem(t *testing.T) {
|
|||
}{
|
||||
{expr: `alpha.bravo`, expectedError: queryexpr.ValueNotAMapError([]string{"alpha", "bravo"})},
|
||||
{expr: `charlie.tree.bla`, expectedError: queryexpr.ValueNotAMapError([]string{"charlie", "tree", "bla"})},
|
||||
|
||||
{expr: `missing="no"`, expectedError: queryexpr.ValuesNotComparable{Right: &types.AttributeValueMemberS{Value: "no"}}},
|
||||
{expr: `missing!="no"`, expectedError: queryexpr.ValuesNotComparable{Right: &types.AttributeValueMemberS{Value: "no"}}},
|
||||
{expr: `missing^="no"`, expectedError: queryexpr.ValueNotConvertableToString{nil}},
|
||||
}
|
||||
|
||||
for _, scenario := range scenarios {
|
||||
|
|
15
internal/dynamo-browse/models/queryexpr/fieldevaluator.go
Normal file
15
internal/dynamo-browse/models/queryexpr/fieldevaluator.go
Normal 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
|
||||
}
|
|
@ -2,12 +2,12 @@ package queryexpr
|
|||
|
||||
import (
|
||||
"context"
|
||||
"github.com/aws/aws-sdk-go-v2/feature/dynamodb/expression"
|
||||
"github.com/aws/aws-sdk-go-v2/service/dynamodb/types"
|
||||
"github.com/lmika/audax/internal/common/sliceutils"
|
||||
"github.com/lmika/audax/internal/dynamo-browse/models"
|
||||
"github.com/pkg/errors"
|
||||
"strings"
|
||||
|
||||
"github.com/aws/aws-sdk-go-v2/feature/dynamodb/expression"
|
||||
"github.com/lmika/dynamo-browse/internal/common/sliceutils"
|
||||
"github.com/lmika/dynamo-browse/internal/dynamo-browse/models"
|
||||
"github.com/pkg/errors"
|
||||
)
|
||||
|
||||
func (a *astFunctionCall) evalToIR(ctx *evalContext, info *models.TableInfo) (irAtom, error) {
|
||||
|
@ -29,7 +29,7 @@ func (a *astFunctionCall) evalToIR(ctx *evalContext, info *models.TableInfo) (ir
|
|||
return nil, err
|
||||
}
|
||||
|
||||
// TODO: do this properly
|
||||
// Special handling of functions that have IR nodes
|
||||
switch nameIr.keyName() {
|
||||
case "size":
|
||||
if len(irNodes) != 1 {
|
||||
|
@ -40,20 +40,34 @@ func (a *astFunctionCall) evalToIR(ctx *evalContext, info *models.TableInfo) (ir
|
|||
return nil, OperandNotANameError(a.Args[0].String())
|
||||
}
|
||||
return irSizeFn{name}, nil
|
||||
case "range":
|
||||
if len(irNodes) != 2 {
|
||||
return nil, InvalidArgumentNumberError{Name: "range", Expected: 2, Actual: len(irNodes)}
|
||||
}
|
||||
|
||||
// TEMP
|
||||
fromVal := irNodes[0].(valueIRAtom).goValue().(int64)
|
||||
toVal := irNodes[1].(valueIRAtom).goValue().(int64)
|
||||
return irRangeFn{fromVal, toVal}, nil
|
||||
}
|
||||
return nil, UnrecognisedFunctionError{Name: nameIr.keyName()}
|
||||
|
||||
builtinFn, hasBuiltin := nativeFuncs[nameIr.keyName()]
|
||||
if !hasBuiltin {
|
||||
return nil, UnrecognisedFunctionError{Name: nameIr.keyName()}
|
||||
}
|
||||
|
||||
// Normal functions which are evaluated to regular values
|
||||
irValues, err := sliceutils.MapWithError(irNodes, func(a irAtom) (exprValue, error) {
|
||||
v, isV := a.(valueIRAtom)
|
||||
if !isV {
|
||||
return nil, errors.New("cannot use value")
|
||||
}
|
||||
return v.exprValue(), nil
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
val, err := builtinFn(context.Background(), irValues)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return irValue{value: val}, nil
|
||||
}
|
||||
|
||||
func (a *astFunctionCall) evalItem(ctx *evalContext, item models.Item) (types.AttributeValue, error) {
|
||||
func (a *astFunctionCall) evalItem(ctx *evalContext, item models.Item) (exprValue, error) {
|
||||
if !a.IsCall {
|
||||
return a.Caller.evalItem(ctx, item)
|
||||
}
|
||||
|
@ -67,14 +81,16 @@ func (a *astFunctionCall) evalItem(ctx *evalContext, item models.Item) (types.At
|
|||
return nil, UnrecognisedFunctionError{Name: name}
|
||||
}
|
||||
|
||||
args, err := sliceutils.MapWithError(a.Args, func(a *astExpr) (types.AttributeValue, error) {
|
||||
args, err := sliceutils.MapWithError(a.Args, func(a *astExpr) (exprValue, error) {
|
||||
return a.evalItem(ctx, item)
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return fn(context.Background(), args)
|
||||
cCtx := context.WithValue(context.Background(), timeSourceContextKey, ctx.timeSource)
|
||||
cCtx = context.WithValue(cCtx, currentResultSetContextKey, ctx.ctxResultSet)
|
||||
return fn(cCtx, args)
|
||||
}
|
||||
|
||||
func (a *astFunctionCall) canModifyItem(ctx *evalContext, item models.Item) bool {
|
||||
|
@ -85,7 +101,7 @@ func (a *astFunctionCall) canModifyItem(ctx *evalContext, item models.Item) bool
|
|||
return a.Caller.canModifyItem(ctx, item)
|
||||
}
|
||||
|
||||
func (a *astFunctionCall) setEvalItem(ctx *evalContext, item models.Item, value types.AttributeValue) error {
|
||||
func (a *astFunctionCall) setEvalItem(ctx *evalContext, item models.Item, value exprValue) error {
|
||||
// TODO: Should a function vall return an item?
|
||||
if a.IsCall {
|
||||
return PathNotSettableError{}
|
||||
|
@ -147,3 +163,15 @@ func (i irRangeFn) calcGoValues(info *models.TableInfo) ([]any, error) {
|
|||
}
|
||||
return xs, nil
|
||||
}
|
||||
|
||||
type multiValueFnResult struct {
|
||||
items []any
|
||||
}
|
||||
|
||||
func (i multiValueFnResult) calcQueryForScan(info *models.TableInfo) (expression.ConditionBuilder, error) {
|
||||
return expression.ConditionBuilder{}, errors.New("cannot run as scan")
|
||||
}
|
||||
|
||||
func (i multiValueFnResult) calcGoValues(info *models.TableInfo) ([]any, error) {
|
||||
return i.items, nil
|
||||
}
|
||||
|
|
17
internal/dynamo-browse/models/queryexpr/helpers_test.go
Normal file
17
internal/dynamo-browse/models/queryexpr/helpers_test.go
Normal 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
|
||||
}
|
||||
|
|
@ -1,13 +1,10 @@
|
|||
package queryexpr
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"github.com/aws/aws-sdk-go-v2/feature/dynamodb/expression"
|
||||
"github.com/aws/aws-sdk-go-v2/service/dynamodb/types"
|
||||
"github.com/lmika/audax/internal/common/sliceutils"
|
||||
"github.com/lmika/audax/internal/dynamo-browse/models"
|
||||
"github.com/lmika/audax/internal/dynamo-browse/models/attrutils"
|
||||
"github.com/lmika/dynamo-browse/internal/common/sliceutils"
|
||||
"github.com/lmika/dynamo-browse/internal/dynamo-browse/models"
|
||||
"github.com/lmika/dynamo-browse/internal/dynamo-browse/models/attrutils"
|
||||
"github.com/pkg/errors"
|
||||
"strings"
|
||||
)
|
||||
|
@ -71,6 +68,13 @@ func (a *astIn) evalToIR(ctx *evalContext, info *models.TableInfo) (irAtom, erro
|
|||
return nil, OperandNotANameError(a.Ref.String())
|
||||
}
|
||||
ir = irContains{needle: lit, haystack: t}
|
||||
case valueIRAtom:
|
||||
nameIR, isNameIR := leftIR.(irNamePath)
|
||||
if !isNameIR {
|
||||
return nil, OperandNotANameError(a.Ref.String())
|
||||
}
|
||||
|
||||
ir = irLiteralValues{name: nameIR, values: t}
|
||||
case oprIRAtom:
|
||||
nameIR, isNameIR := leftIR.(irNamePath)
|
||||
if !isNameIR {
|
||||
|
@ -78,13 +82,6 @@ func (a *astIn) evalToIR(ctx *evalContext, info *models.TableInfo) (irAtom, erro
|
|||
}
|
||||
|
||||
ir = irIn{name: nameIR, values: []oprIRAtom{t}}
|
||||
case multiValueIRAtom:
|
||||
nameIR, isNameIR := leftIR.(irNamePath)
|
||||
if !isNameIR {
|
||||
return nil, OperandNotANameError(a.Ref.String())
|
||||
}
|
||||
|
||||
ir = irLiteralValues{name: nameIR, values: t}
|
||||
default:
|
||||
return nil, OperandNotAnOperandError{}
|
||||
}
|
||||
|
@ -96,7 +93,7 @@ func (a *astIn) evalToIR(ctx *evalContext, info *models.TableInfo) (irAtom, erro
|
|||
return ir, nil
|
||||
}
|
||||
|
||||
func (a *astIn) evalItem(ctx *evalContext, item models.Item) (types.AttributeValue, error) {
|
||||
func (a *astIn) evalItem(ctx *evalContext, item models.Item) (exprValue, error) {
|
||||
val, err := a.Ref.evalItem(ctx, item)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
|
@ -112,14 +109,15 @@ func (a *astIn) evalItem(ctx *evalContext, item models.Item) (types.AttributeVal
|
|||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
cmp, isComparable := attrutils.CompareScalarAttributes(val, evalOp)
|
||||
// TODO: use native types here
|
||||
cmp, isComparable := attrutils.CompareScalarAttributes(val.asAttributeValue(), evalOp.asAttributeValue())
|
||||
if !isComparable {
|
||||
continue
|
||||
} else if cmp == 0 {
|
||||
return &types.AttributeValueMemberBOOL{Value: true}, nil
|
||||
return boolExprValue(true), nil
|
||||
}
|
||||
}
|
||||
return &types.AttributeValueMemberBOOL{Value: false}, nil
|
||||
return boolExprValue(false), nil
|
||||
case a.SingleOperand != nil:
|
||||
evalOp, err := a.SingleOperand.evalItem(ctx, item)
|
||||
if err != nil {
|
||||
|
@ -127,69 +125,38 @@ func (a *astIn) evalItem(ctx *evalContext, item models.Item) (types.AttributeVal
|
|||
}
|
||||
|
||||
switch t := evalOp.(type) {
|
||||
case *types.AttributeValueMemberS:
|
||||
str, canToStr := attrutils.AttributeToString(val)
|
||||
case stringableExprValue:
|
||||
str, canToStr := val.(stringableExprValue)
|
||||
if !canToStr {
|
||||
return &types.AttributeValueMemberBOOL{Value: false}, nil
|
||||
return boolExprValue(false), nil
|
||||
}
|
||||
|
||||
return &types.AttributeValueMemberBOOL{Value: strings.Contains(t.Value, str)}, nil
|
||||
case *types.AttributeValueMemberL:
|
||||
for _, listItem := range t.Value {
|
||||
cmp, isComparable := attrutils.CompareScalarAttributes(val, listItem)
|
||||
return boolExprValue(strings.Contains(t.asString(), str.asString())), nil
|
||||
case slicableExprValue:
|
||||
for i := 0; i < t.len(); i++ {
|
||||
va, err := t.valueAt(i)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// TODO: use expr value types here
|
||||
cmp, isComparable := attrutils.CompareScalarAttributes(val.asAttributeValue(), va.asAttributeValue())
|
||||
if !isComparable {
|
||||
continue
|
||||
} else if cmp == 0 {
|
||||
return &types.AttributeValueMemberBOOL{Value: true}, nil
|
||||
return boolExprValue(true), nil
|
||||
}
|
||||
}
|
||||
return &types.AttributeValueMemberBOOL{Value: false}, nil
|
||||
case *types.AttributeValueMemberSS:
|
||||
str, canToStr := attrutils.AttributeToString(val)
|
||||
return boolExprValue(false), nil
|
||||
case mappableExprValue:
|
||||
str, canToStr := val.(stringableExprValue)
|
||||
if !canToStr {
|
||||
return &types.AttributeValueMemberBOOL{Value: false}, nil
|
||||
return boolExprValue(false), nil
|
||||
}
|
||||
|
||||
for _, listItem := range t.Value {
|
||||
if str != listItem {
|
||||
return &types.AttributeValueMemberBOOL{Value: false}, nil
|
||||
}
|
||||
}
|
||||
return &types.AttributeValueMemberBOOL{Value: true}, nil
|
||||
case *types.AttributeValueMemberBS:
|
||||
b, isB := val.(*types.AttributeValueMemberB)
|
||||
if !isB {
|
||||
return &types.AttributeValueMemberBOOL{Value: false}, nil
|
||||
}
|
||||
|
||||
for _, listItem := range t.Value {
|
||||
if !bytes.Equal(b.Value, listItem) {
|
||||
return &types.AttributeValueMemberBOOL{Value: false}, nil
|
||||
}
|
||||
}
|
||||
return &types.AttributeValueMemberBOOL{Value: true}, nil
|
||||
case *types.AttributeValueMemberNS:
|
||||
n, isN := val.(*types.AttributeValueMemberN)
|
||||
if !isN {
|
||||
return &types.AttributeValueMemberBOOL{Value: false}, nil
|
||||
}
|
||||
|
||||
for _, listItem := range t.Value {
|
||||
// TODO: this is not actually right
|
||||
if n.Value != listItem {
|
||||
return &types.AttributeValueMemberBOOL{Value: false}, nil
|
||||
}
|
||||
}
|
||||
return &types.AttributeValueMemberBOOL{Value: true}, nil
|
||||
case *types.AttributeValueMemberM:
|
||||
str, canToStr := attrutils.AttributeToString(val)
|
||||
if !canToStr {
|
||||
return &types.AttributeValueMemberBOOL{Value: false}, nil
|
||||
}
|
||||
_, hasItem := t.Value[str]
|
||||
return &types.AttributeValueMemberBOOL{Value: hasItem}, nil
|
||||
hasKey := t.hasKey(str.asString())
|
||||
return boolExprValue(hasKey), nil
|
||||
}
|
||||
return nil, ValuesNotInnableError{Val: evalOp}
|
||||
return nil, ValuesNotInnableError{Val: evalOp.asAttributeValue()}
|
||||
}
|
||||
return nil, errors.New("internal error: unhandled 'in' case")
|
||||
}
|
||||
|
@ -201,7 +168,7 @@ func (a *astIn) canModifyItem(ctx *evalContext, item models.Item) bool {
|
|||
return a.Ref.canModifyItem(ctx, item)
|
||||
}
|
||||
|
||||
func (a *astIn) setEvalItem(ctx *evalContext, item models.Item, value types.AttributeValue) error {
|
||||
func (a *astIn) setEvalItem(ctx *evalContext, item models.Item, value exprValue) error {
|
||||
if len(a.Operand) != 0 || a.SingleOperand != nil {
|
||||
return PathNotSettableError{}
|
||||
}
|
||||
|
@ -263,19 +230,38 @@ func (i irIn) calcQueryForScan(info *models.TableInfo) (expression.ConditionBuil
|
|||
|
||||
type irLiteralValues struct {
|
||||
name nameIRAtom
|
||||
values multiValueIRAtom
|
||||
values valueIRAtom
|
||||
}
|
||||
|
||||
func (i irLiteralValues) calcQueryForScan(info *models.TableInfo) (expression.ConditionBuilder, error) {
|
||||
vals, err := i.values.calcGoValues(info)
|
||||
if err != nil {
|
||||
return expression.ConditionBuilder{}, err
|
||||
func (iv irLiteralValues) calcQueryForScan(info *models.TableInfo) (expression.ConditionBuilder, error) {
|
||||
if sliceable, isSliceable := iv.values.exprValue().(slicableExprValue); isSliceable {
|
||||
if sliceable.len() == 1 {
|
||||
va, err := sliceable.valueAt(0)
|
||||
if err != nil {
|
||||
return expression.ConditionBuilder{}, err
|
||||
}
|
||||
|
||||
return iv.name.calcName(info).In(buildExpressionFromValue(va)), nil
|
||||
} else if sliceable.len() == 0 {
|
||||
// name is not in an empty slice, so this branch always evaluates to false
|
||||
// TODO: would be better to not even include this branch in some way?
|
||||
return expression.Equal(expression.Value(false), expression.Value(true)), nil
|
||||
}
|
||||
|
||||
items := make([]expression.OperandBuilder, sliceable.len())
|
||||
for i := 0; i < sliceable.len(); i++ {
|
||||
va, err := sliceable.valueAt(i)
|
||||
if err != nil {
|
||||
return expression.ConditionBuilder{}, err
|
||||
}
|
||||
|
||||
items[i] = buildExpressionFromValue(va)
|
||||
}
|
||||
|
||||
return iv.name.calcName(info).In(items[0], items[1:]...), nil
|
||||
}
|
||||
|
||||
oprValues := sliceutils.Map(vals, func(t any) expression.OperandBuilder {
|
||||
return expression.Value(t)
|
||||
})
|
||||
return i.name.calcName(info).In(oprValues[0], oprValues[1:]...), nil
|
||||
return iv.name.calcName(info).In(buildExpressionFromValue(iv.values.exprValue())), nil
|
||||
}
|
||||
|
||||
type irContains struct {
|
||||
|
@ -284,8 +270,11 @@ type irContains struct {
|
|||
}
|
||||
|
||||
func (i irContains) calcQueryForScan(info *models.TableInfo) (expression.ConditionBuilder, error) {
|
||||
needle := i.needle.goValue()
|
||||
haystack := i.haystack.calcName(info)
|
||||
strNeedle, isString := i.needle.exprValue().(stringableExprValue)
|
||||
if !isString {
|
||||
return expression.ConditionBuilder{}, errors.New("value cannot be converted to string")
|
||||
}
|
||||
|
||||
return haystack.Contains(fmt.Sprint(needle)), nil
|
||||
haystack := i.haystack.calcName(info)
|
||||
return haystack.Contains(strNeedle.asString()), nil
|
||||
}
|
||||
|
|
|
@ -2,7 +2,7 @@ package queryexpr
|
|||
|
||||
import (
|
||||
"github.com/aws/aws-sdk-go-v2/feature/dynamodb/expression"
|
||||
"github.com/lmika/audax/internal/dynamo-browse/models"
|
||||
"github.com/lmika/dynamo-browse/internal/dynamo-browse/models"
|
||||
)
|
||||
|
||||
// TO DELETE = operandFieldName() string
|
||||
|
@ -36,11 +36,7 @@ type nameIRAtom interface {
|
|||
|
||||
type valueIRAtom interface {
|
||||
oprIRAtom
|
||||
goValue() any
|
||||
}
|
||||
|
||||
type multiValueIRAtom interface {
|
||||
calcGoValues(info *models.TableInfo) ([]any, error)
|
||||
exprValue() exprValue
|
||||
}
|
||||
|
||||
func canExecuteAsQuery(ir irAtom, qci *queryCalcInfo) bool {
|
||||
|
|
|
@ -2,9 +2,7 @@ package queryexpr
|
|||
|
||||
import (
|
||||
"github.com/aws/aws-sdk-go-v2/feature/dynamodb/expression"
|
||||
"github.com/aws/aws-sdk-go-v2/service/dynamodb/types"
|
||||
"github.com/lmika/audax/internal/dynamo-browse/models"
|
||||
"github.com/lmika/audax/internal/dynamo-browse/models/attrutils"
|
||||
"github.com/lmika/dynamo-browse/internal/dynamo-browse/models"
|
||||
"reflect"
|
||||
"strings"
|
||||
)
|
||||
|
@ -12,50 +10,50 @@ import (
|
|||
type isTypeInfo struct {
|
||||
isAny bool
|
||||
attributeType expression.DynamoDBAttributeType
|
||||
goType reflect.Type
|
||||
goTypes []reflect.Type
|
||||
}
|
||||
|
||||
var validIsTypeNames = map[string]isTypeInfo{
|
||||
"ANY": {isAny: true},
|
||||
"B": {
|
||||
attributeType: expression.Binary,
|
||||
goType: reflect.TypeOf(&types.AttributeValueMemberB{}),
|
||||
// TODO
|
||||
},
|
||||
"BOOL": {
|
||||
attributeType: expression.Boolean,
|
||||
goType: reflect.TypeOf(&types.AttributeValueMemberBOOL{}),
|
||||
goTypes: []reflect.Type{reflect.TypeOf(boolExprValue(false))},
|
||||
},
|
||||
"S": {
|
||||
attributeType: expression.String,
|
||||
goType: reflect.TypeOf(&types.AttributeValueMemberS{}),
|
||||
goTypes: []reflect.Type{reflect.TypeOf(stringExprValue(""))},
|
||||
},
|
||||
"N": {
|
||||
attributeType: expression.Number,
|
||||
goType: reflect.TypeOf(&types.AttributeValueMemberN{}),
|
||||
goTypes: []reflect.Type{reflect.TypeOf(int64ExprValue(0)), reflect.TypeOf(bigNumExprValue{})},
|
||||
},
|
||||
"NULL": {
|
||||
attributeType: expression.Null,
|
||||
goType: reflect.TypeOf(&types.AttributeValueMemberNULL{}),
|
||||
goTypes: []reflect.Type{reflect.TypeOf(nullExprValue{})},
|
||||
},
|
||||
"L": {
|
||||
attributeType: expression.List,
|
||||
goType: reflect.TypeOf(&types.AttributeValueMemberL{}),
|
||||
goTypes: []reflect.Type{reflect.TypeOf(listExprValue{}), reflect.TypeOf(listProxyValue{})},
|
||||
},
|
||||
"M": {
|
||||
attributeType: expression.Map,
|
||||
goType: reflect.TypeOf(&types.AttributeValueMemberM{}),
|
||||
goTypes: []reflect.Type{reflect.TypeOf(mapExprValue{}), reflect.TypeOf(mapProxyValue{})},
|
||||
},
|
||||
"BS": {
|
||||
attributeType: expression.BinarySet,
|
||||
goType: reflect.TypeOf(&types.AttributeValueMemberBS{}),
|
||||
// TODO
|
||||
},
|
||||
"NS": {
|
||||
attributeType: expression.NumberSet,
|
||||
goType: reflect.TypeOf(&types.AttributeValueMemberNS{}),
|
||||
// TODO
|
||||
},
|
||||
"SS": {
|
||||
attributeType: expression.StringSet,
|
||||
goType: reflect.TypeOf(&types.AttributeValueMemberSS{}),
|
||||
// TODO
|
||||
},
|
||||
}
|
||||
|
||||
|
@ -83,14 +81,14 @@ func (a *astIsOp) evalToIR(ctx *evalContext, info *models.TableInfo) (irAtom, er
|
|||
if !isValueIR {
|
||||
return nil, ValueMustBeLiteralError{}
|
||||
}
|
||||
strValue, isStringValue := valueIR.goValue().(string)
|
||||
strValue, isStringValue := valueIR.exprValue().(stringableExprValue)
|
||||
if !isStringValue {
|
||||
return nil, ValueMustBeStringError{}
|
||||
}
|
||||
|
||||
typeInfo, isValidType := validIsTypeNames[strings.ToUpper(strValue)]
|
||||
typeInfo, isValidType := validIsTypeNames[strings.ToUpper(strValue.asString())]
|
||||
if !isValidType {
|
||||
return nil, InvalidTypeForIsError{TypeName: strValue}
|
||||
return nil, InvalidTypeForIsError{TypeName: strValue.asString()}
|
||||
}
|
||||
|
||||
var ir = irIs{name: nameIR, typeInfo: typeInfo}
|
||||
|
@ -104,7 +102,7 @@ func (a *astIsOp) evalToIR(ctx *evalContext, info *models.TableInfo) (irAtom, er
|
|||
return ir, nil
|
||||
}
|
||||
|
||||
func (a *astIsOp) evalItem(ctx *evalContext, item models.Item) (types.AttributeValue, error) {
|
||||
func (a *astIsOp) evalItem(ctx *evalContext, item models.Item) (exprValue, error) {
|
||||
ref, err := a.Ref.evalItem(ctx, item)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
|
@ -118,26 +116,32 @@ func (a *astIsOp) evalItem(ctx *evalContext, item models.Item) (types.AttributeV
|
|||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
str, canToStr := attrutils.AttributeToString(expTypeVal)
|
||||
str, canToStr := expTypeVal.(stringableExprValue)
|
||||
if !canToStr {
|
||||
return nil, ValueMustBeStringError{}
|
||||
}
|
||||
typeInfo, hasTypeInfo := validIsTypeNames[strings.ToUpper(str)]
|
||||
typeInfo, hasTypeInfo := validIsTypeNames[strings.ToUpper(str.asString())]
|
||||
if !hasTypeInfo {
|
||||
return nil, InvalidTypeForIsError{TypeName: str}
|
||||
return nil, InvalidTypeForIsError{TypeName: str.asString()}
|
||||
}
|
||||
|
||||
var resultOfIs bool
|
||||
if typeInfo.isAny {
|
||||
resultOfIs = ref != nil
|
||||
resultOfIs = ref != undefinedExprValue{}
|
||||
} else {
|
||||
refType := reflect.TypeOf(ref)
|
||||
resultOfIs = typeInfo.goType.AssignableTo(refType)
|
||||
|
||||
for _, t := range typeInfo.goTypes {
|
||||
if t.AssignableTo(refType) {
|
||||
resultOfIs = true
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
if a.HasNot {
|
||||
resultOfIs = !resultOfIs
|
||||
}
|
||||
return &types.AttributeValueMemberBOOL{Value: resultOfIs}, nil
|
||||
return boolExprValue(resultOfIs), nil
|
||||
}
|
||||
|
||||
func (a *astIsOp) canModifyItem(ctx *evalContext, item models.Item) bool {
|
||||
|
@ -147,7 +151,7 @@ func (a *astIsOp) canModifyItem(ctx *evalContext, item models.Item) bool {
|
|||
return a.Ref.canModifyItem(ctx, item)
|
||||
}
|
||||
|
||||
func (a *astIsOp) setEvalItem(ctx *evalContext, item models.Item, value types.AttributeValue) error {
|
||||
func (a *astIsOp) setEvalItem(ctx *evalContext, item models.Item, value exprValue) error {
|
||||
if a.Value != nil {
|
||||
return PathNotSettableError{}
|
||||
}
|
||||
|
|
|
@ -1,8 +1,7 @@
|
|||
package queryexpr
|
||||
|
||||
import (
|
||||
"github.com/aws/aws-sdk-go-v2/service/dynamodb/types"
|
||||
"github.com/lmika/audax/internal/dynamo-browse/models"
|
||||
"github.com/lmika/dynamo-browse/internal/dynamo-browse/models"
|
||||
"github.com/pkg/errors"
|
||||
)
|
||||
|
||||
|
@ -21,7 +20,12 @@ func (p *astPlaceholder) evalToIR(ctx *evalContext, info *models.TableInfo) (irA
|
|||
return nil, MissingPlaceholderError{Placeholder: p.Placeholder}
|
||||
}
|
||||
|
||||
return irValue{value: val}, nil
|
||||
ev, err := newExprValueFromAttributeValue(val)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return irValue{value: ev}, nil
|
||||
} else if placeholderType == namePlaceholderPrefix {
|
||||
name, hasName := ctx.lookupName(placeholder)
|
||||
if !hasName {
|
||||
|
@ -34,7 +38,7 @@ func (p *astPlaceholder) evalToIR(ctx *evalContext, info *models.TableInfo) (irA
|
|||
return nil, errors.New("unrecognised placeholder")
|
||||
}
|
||||
|
||||
func (p *astPlaceholder) evalItem(ctx *evalContext, item models.Item) (types.AttributeValue, error) {
|
||||
func (p *astPlaceholder) evalItem(ctx *evalContext, item models.Item) (exprValue, error) {
|
||||
placeholderType := p.Placeholder[0]
|
||||
placeholder := p.Placeholder[1:]
|
||||
|
||||
|
@ -43,7 +47,7 @@ func (p *astPlaceholder) evalItem(ctx *evalContext, item models.Item) (types.Att
|
|||
if !hasVal {
|
||||
return nil, MissingPlaceholderError{Placeholder: p.Placeholder}
|
||||
}
|
||||
return val, nil
|
||||
return newExprValueFromAttributeValue(val)
|
||||
} else if placeholderType == namePlaceholderPrefix {
|
||||
name, hasName := ctx.lookupName(placeholder)
|
||||
if !hasName {
|
||||
|
@ -55,7 +59,7 @@ func (p *astPlaceholder) evalItem(ctx *evalContext, item models.Item) (types.Att
|
|||
return nil, nil
|
||||
}
|
||||
|
||||
return res, nil
|
||||
return newExprValueFromAttributeValue(res)
|
||||
}
|
||||
|
||||
return nil, errors.New("unrecognised placeholder")
|
||||
|
@ -66,7 +70,7 @@ func (p *astPlaceholder) canModifyItem(ctx *evalContext, item models.Item) bool
|
|||
return placeholderType == namePlaceholderPrefix
|
||||
}
|
||||
|
||||
func (p *astPlaceholder) setEvalItem(ctx *evalContext, item models.Item, value types.AttributeValue) error {
|
||||
func (p *astPlaceholder) setEvalItem(ctx *evalContext, item models.Item, value exprValue) error {
|
||||
placeholderType := p.Placeholder[0]
|
||||
placeholder := p.Placeholder[1:]
|
||||
|
||||
|
@ -78,7 +82,7 @@ func (p *astPlaceholder) setEvalItem(ctx *evalContext, item models.Item, value t
|
|||
return MissingPlaceholderError{Placeholder: p.Placeholder}
|
||||
}
|
||||
|
||||
item[name] = value
|
||||
item[name] = value.asAttributeValue()
|
||||
return nil
|
||||
}
|
||||
|
||||
|
|
|
@ -1,10 +1,8 @@
|
|||
package queryexpr
|
||||
|
||||
import (
|
||||
"github.com/aws/aws-sdk-go-v2/service/dynamodb/types"
|
||||
"github.com/lmika/audax/internal/common/sliceutils"
|
||||
"github.com/lmika/audax/internal/dynamo-browse/models"
|
||||
"strconv"
|
||||
"github.com/lmika/dynamo-browse/internal/common/sliceutils"
|
||||
"github.com/lmika/dynamo-browse/internal/dynamo-browse/models"
|
||||
"strings"
|
||||
)
|
||||
|
||||
|
@ -34,7 +32,7 @@ func (r *astSubRef) evalToIR(ctx *evalContext, info *models.TableInfo) (irAtom,
|
|||
return irNamePath{name: namePath.name, quals: quals}, nil
|
||||
}
|
||||
|
||||
func (r *astSubRef) evalItem(ctx *evalContext, item models.Item) (types.AttributeValue, error) {
|
||||
func (r *astSubRef) evalItem(ctx *evalContext, item models.Item) (exprValue, error) {
|
||||
res, err := r.Ref.evalItem(ctx, item)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
|
@ -48,7 +46,7 @@ func (r *astSubRef) evalItem(ctx *evalContext, item models.Item) (types.Attribut
|
|||
return res, nil
|
||||
}
|
||||
|
||||
func (r *astSubRef) evalSubRefs(ctx *evalContext, item models.Item, res types.AttributeValue, subRefs []*astSubRefType) (types.AttributeValue, error) {
|
||||
func (r *astSubRef) evalSubRefs(ctx *evalContext, item models.Item, res exprValue, subRefs []*astSubRefType) (exprValue, error) {
|
||||
for i, sr := range subRefs {
|
||||
sv, err := sr.evalToStrOrInt(ctx, nil)
|
||||
if err != nil {
|
||||
|
@ -57,24 +55,30 @@ func (r *astSubRef) evalSubRefs(ctx *evalContext, item models.Item, res types.At
|
|||
|
||||
switch val := sv.(type) {
|
||||
case string:
|
||||
var hasV bool
|
||||
mapRes, isMapRes := res.(*types.AttributeValueMemberM)
|
||||
mapRes, isMapRes := res.(mappableExprValue)
|
||||
if !isMapRes {
|
||||
return nil, newValueNotAMapError(r, subRefs[:i+1])
|
||||
}
|
||||
|
||||
res, hasV = mapRes.Value[val]
|
||||
if !hasV {
|
||||
return nil, nil
|
||||
if mapRes.hasKey(val) {
|
||||
res, err = mapRes.valueOf(val)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
} else {
|
||||
res = nil
|
||||
}
|
||||
case int:
|
||||
listRes, isMapRes := res.(*types.AttributeValueMemberL)
|
||||
case int64:
|
||||
listRes, isMapRes := res.(slicableExprValue)
|
||||
if !isMapRes {
|
||||
return nil, newValueNotAListError(r, subRefs[:i+1])
|
||||
}
|
||||
|
||||
// TODO - deal with index properly
|
||||
res = listRes.Value[val]
|
||||
// TODO - deal with index properly (i.e. error handling)
|
||||
res, err = listRes.valueAt(int(val))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
}
|
||||
return res, nil
|
||||
|
@ -84,7 +88,7 @@ func (r *astSubRef) canModifyItem(ctx *evalContext, item models.Item) bool {
|
|||
return r.Ref.canModifyItem(ctx, item)
|
||||
}
|
||||
|
||||
func (r *astSubRef) setEvalItem(ctx *evalContext, item models.Item, value types.AttributeValue) error {
|
||||
func (r *astSubRef) setEvalItem(ctx *evalContext, item models.Item, value exprValue) error {
|
||||
if len(r.SubRefs) == 0 {
|
||||
return r.Ref.setEvalItem(ctx, item, value)
|
||||
}
|
||||
|
@ -108,20 +112,19 @@ func (r *astSubRef) setEvalItem(ctx *evalContext, item models.Item, value types.
|
|||
|
||||
switch val := sv.(type) {
|
||||
case string:
|
||||
mapRes, isMapRes := parentItem.(*types.AttributeValueMemberM)
|
||||
mapRes, isMapRes := parentItem.(modifiableMapExprValue)
|
||||
if !isMapRes {
|
||||
return newValueNotAMapError(r, r.SubRefs)
|
||||
}
|
||||
|
||||
mapRes.Value[val] = value
|
||||
case int:
|
||||
listRes, isMapRes := parentItem.(*types.AttributeValueMemberL)
|
||||
mapRes.setValueOf(val, value)
|
||||
case int64:
|
||||
listRes, isMapRes := parentItem.(modifiableSliceExprValue)
|
||||
if !isMapRes {
|
||||
return newValueNotAListError(r, r.SubRefs)
|
||||
}
|
||||
|
||||
// TODO: handle indexes
|
||||
listRes.Value[val] = value
|
||||
listRes.setValueAt(int(val), value)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
@ -136,20 +139,6 @@ func (r *astSubRef) deleteAttribute(ctx *evalContext, item models.Item) error {
|
|||
return err
|
||||
}
|
||||
|
||||
/*
|
||||
for i, key := range r.Quals {
|
||||
mapItem, isMapItem := parentItem.(*types.AttributeValueMemberM)
|
||||
if !isMapItem {
|
||||
return PathNotSettableError{}
|
||||
}
|
||||
|
||||
if isLast := i == len(r.Quals)-1; isLast {
|
||||
delete(mapItem.Value, key)
|
||||
} else {
|
||||
parentItem = mapItem.Value[key]
|
||||
}
|
||||
}
|
||||
*/
|
||||
if len(r.SubRefs) > 1 {
|
||||
parentItem, err = r.evalSubRefs(ctx, item, parentItem, r.SubRefs[0:len(r.SubRefs)-1])
|
||||
if err != nil {
|
||||
|
@ -164,23 +153,20 @@ func (r *astSubRef) deleteAttribute(ctx *evalContext, item models.Item) error {
|
|||
|
||||
switch val := sv.(type) {
|
||||
case string:
|
||||
mapRes, isMapRes := parentItem.(*types.AttributeValueMemberM)
|
||||
mapRes, isMapRes := parentItem.(modifiableMapExprValue)
|
||||
if !isMapRes {
|
||||
return newValueNotAMapError(r, r.SubRefs)
|
||||
}
|
||||
|
||||
delete(mapRes.Value, val)
|
||||
case int:
|
||||
listRes, isMapRes := parentItem.(*types.AttributeValueMemberL)
|
||||
mapRes.deleteValueOf(val)
|
||||
case int64:
|
||||
listRes, isMapRes := parentItem.(modifiableSliceExprValue)
|
||||
if !isMapRes {
|
||||
return newValueNotAListError(r, r.SubRefs)
|
||||
}
|
||||
|
||||
// TODO: handle indexes out of bounds
|
||||
oldList := listRes.Value
|
||||
newList := append([]types.AttributeValue{}, oldList[:val]...)
|
||||
newList = append(newList, oldList[val+1:]...)
|
||||
listRes.Value = newList
|
||||
listRes.deleteValueAt(int(val))
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
@ -214,18 +200,10 @@ func (sr *astSubRefType) evalToStrOrInt(ctx *evalContext, item models.Item) (any
|
|||
return nil, err
|
||||
}
|
||||
switch v := subEvalItem.(type) {
|
||||
case *types.AttributeValueMemberS:
|
||||
return v.Value, nil
|
||||
case *types.AttributeValueMemberN:
|
||||
intVal, err := strconv.Atoi(v.Value)
|
||||
if err == nil {
|
||||
return intVal, nil
|
||||
}
|
||||
flVal, err := strconv.ParseFloat(v.Value, 64)
|
||||
if err == nil {
|
||||
return int(flVal), nil
|
||||
}
|
||||
return nil, err
|
||||
case stringableExprValue:
|
||||
return v.asString(), nil
|
||||
case numberableExprValue:
|
||||
return v.asInt(), nil
|
||||
}
|
||||
return nil, ValueNotUsableAsASubref{}
|
||||
}
|
||||
|
|
|
@ -1 +1,414 @@
|
|||
package queryexpr
|
||||
|
||||
import (
|
||||
"github.com/aws/aws-sdk-go-v2/feature/dynamodb/expression"
|
||||
"github.com/aws/aws-sdk-go-v2/service/dynamodb/types"
|
||||
"github.com/lmika/dynamo-browse/internal/common/maputils"
|
||||
"github.com/lmika/dynamo-browse/internal/common/sliceutils"
|
||||
"github.com/pkg/errors"
|
||||
"math/big"
|
||||
"strconv"
|
||||
)
|
||||
|
||||
type exprValue interface {
|
||||
typeName() string
|
||||
asGoValue() any
|
||||
asAttributeValue() types.AttributeValue
|
||||
}
|
||||
|
||||
type stringableExprValue interface {
|
||||
exprValue
|
||||
asString() string
|
||||
}
|
||||
|
||||
type numberableExprValue interface {
|
||||
exprValue
|
||||
asBigFloat() *big.Float
|
||||
asInt() int64
|
||||
}
|
||||
|
||||
type slicableExprValue interface {
|
||||
exprValue
|
||||
len() int
|
||||
valueAt(idx int) (exprValue, error)
|
||||
}
|
||||
|
||||
type modifiableSliceExprValue interface {
|
||||
setValueAt(idx int, value exprValue)
|
||||
deleteValueAt(idx int)
|
||||
}
|
||||
|
||||
type mappableExprValue interface {
|
||||
len() int
|
||||
hasKey(name string) bool
|
||||
valueOf(name string) (exprValue, error)
|
||||
}
|
||||
|
||||
type modifiableMapExprValue interface {
|
||||
setValueOf(name string, value exprValue)
|
||||
deleteValueOf(name string)
|
||||
}
|
||||
|
||||
func buildExpressionFromValue(ev exprValue) expression.ValueBuilder {
|
||||
return expression.Value(ev)
|
||||
}
|
||||
|
||||
func newExprValueFromAttributeValue(ev types.AttributeValue) (exprValue, error) {
|
||||
if ev == nil {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
switch xVal := ev.(type) {
|
||||
case *types.AttributeValueMemberS:
|
||||
return stringExprValue(xVal.Value), nil
|
||||
case *types.AttributeValueMemberN:
|
||||
xNumVal, _, err := big.ParseFloat(xVal.Value, 10, 63, big.ToNearestEven)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return bigNumExprValue{num: xNumVal}, nil
|
||||
case *types.AttributeValueMemberBOOL:
|
||||
return boolExprValue(xVal.Value), nil
|
||||
case *types.AttributeValueMemberNULL:
|
||||
return nullExprValue{}, nil
|
||||
case *types.AttributeValueMemberL:
|
||||
return listProxyValue{list: xVal}, nil
|
||||
case *types.AttributeValueMemberM:
|
||||
return mapProxyValue{mapValue: xVal}, nil
|
||||
case *types.AttributeValueMemberSS:
|
||||
return stringSetProxyValue{stringSet: xVal}, nil
|
||||
case *types.AttributeValueMemberNS:
|
||||
return numberSetProxyValue{numberSet: xVal}, nil
|
||||
}
|
||||
return nil, errors.New("cannot convert to expr value")
|
||||
}
|
||||
|
||||
type undefinedExprValue struct{}
|
||||
|
||||
func (b undefinedExprValue) asGoValue() any {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (b undefinedExprValue) asAttributeValue() types.AttributeValue {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s undefinedExprValue) typeName() string {
|
||||
return "UNDEFINED"
|
||||
}
|
||||
|
||||
type stringExprValue string
|
||||
|
||||
func (s stringExprValue) asGoValue() any {
|
||||
return string(s)
|
||||
}
|
||||
|
||||
func (s stringExprValue) asAttributeValue() types.AttributeValue {
|
||||
return &types.AttributeValueMemberS{Value: string(s)}
|
||||
}
|
||||
|
||||
func (s stringExprValue) asString() string {
|
||||
return string(s)
|
||||
}
|
||||
|
||||
func (s stringExprValue) typeName() string {
|
||||
return "S"
|
||||
}
|
||||
|
||||
type int64ExprValue int64
|
||||
|
||||
func (i int64ExprValue) asGoValue() any {
|
||||
return int(i)
|
||||
}
|
||||
|
||||
func (i int64ExprValue) asAttributeValue() types.AttributeValue {
|
||||
return &types.AttributeValueMemberN{Value: strconv.Itoa(int(i))}
|
||||
}
|
||||
|
||||
func (i int64ExprValue) asInt() int64 {
|
||||
return int64(i)
|
||||
}
|
||||
|
||||
func (i int64ExprValue) asBigFloat() *big.Float {
|
||||
var f big.Float
|
||||
f.SetInt64(int64(i))
|
||||
return &f
|
||||
}
|
||||
|
||||
func (s int64ExprValue) typeName() string {
|
||||
return "N"
|
||||
}
|
||||
|
||||
type bigNumExprValue struct {
|
||||
num *big.Float
|
||||
}
|
||||
|
||||
func (i bigNumExprValue) asGoValue() any {
|
||||
return i.num
|
||||
}
|
||||
|
||||
func (i bigNumExprValue) asAttributeValue() types.AttributeValue {
|
||||
return &types.AttributeValueMemberN{Value: i.num.String()}
|
||||
}
|
||||
|
||||
func (i bigNumExprValue) asInt() int64 {
|
||||
x, _ := i.num.Int64()
|
||||
return x
|
||||
}
|
||||
|
||||
func (i bigNumExprValue) asBigFloat() *big.Float {
|
||||
return i.num
|
||||
}
|
||||
|
||||
func (s bigNumExprValue) typeName() string {
|
||||
return "N"
|
||||
}
|
||||
|
||||
type boolExprValue bool
|
||||
|
||||
func (b boolExprValue) asGoValue() any {
|
||||
return bool(b)
|
||||
}
|
||||
|
||||
func (b boolExprValue) asAttributeValue() types.AttributeValue {
|
||||
return &types.AttributeValueMemberBOOL{Value: bool(b)}
|
||||
}
|
||||
|
||||
func (s boolExprValue) typeName() string {
|
||||
return "BOOL"
|
||||
}
|
||||
|
||||
type nullExprValue struct{}
|
||||
|
||||
func (b nullExprValue) asGoValue() any {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (b nullExprValue) asAttributeValue() types.AttributeValue {
|
||||
return &types.AttributeValueMemberNULL{Value: true}
|
||||
}
|
||||
|
||||
func (s nullExprValue) typeName() string {
|
||||
return "NULL"
|
||||
}
|
||||
|
||||
type listExprValue []exprValue
|
||||
|
||||
func (bs listExprValue) asGoValue() any {
|
||||
return sliceutils.Map(bs, func(t exprValue) any {
|
||||
return t.asGoValue()
|
||||
})
|
||||
}
|
||||
|
||||
func (bs listExprValue) asAttributeValue() types.AttributeValue {
|
||||
return &types.AttributeValueMemberL{Value: sliceutils.Map(bs, func(t exprValue) types.AttributeValue {
|
||||
return t.asAttributeValue()
|
||||
})}
|
||||
}
|
||||
|
||||
func (bs listExprValue) len() int {
|
||||
return len(bs)
|
||||
}
|
||||
|
||||
func (bs listExprValue) valueAt(i int) (exprValue, error) {
|
||||
return bs[i], nil
|
||||
}
|
||||
|
||||
func (s listExprValue) typeName() string {
|
||||
return "L"
|
||||
}
|
||||
|
||||
type mapExprValue map[string]exprValue
|
||||
|
||||
func (bs mapExprValue) asGoValue() any {
|
||||
return maputils.MapValues(bs, func(t exprValue) any {
|
||||
return t.asGoValue()
|
||||
})
|
||||
}
|
||||
|
||||
func (bs mapExprValue) asAttributeValue() types.AttributeValue {
|
||||
return &types.AttributeValueMemberM{Value: maputils.MapValues(bs, func(t exprValue) types.AttributeValue {
|
||||
return t.asAttributeValue()
|
||||
})}
|
||||
}
|
||||
|
||||
func (bs mapExprValue) len() int {
|
||||
return len(bs)
|
||||
}
|
||||
|
||||
func (bs mapExprValue) hasKey(name string) bool {
|
||||
_, ok := bs[name]
|
||||
return ok
|
||||
}
|
||||
|
||||
func (bs mapExprValue) valueOf(name string) (exprValue, error) {
|
||||
return bs[name], nil
|
||||
}
|
||||
|
||||
func (s mapExprValue) typeName() string {
|
||||
return "M"
|
||||
}
|
||||
|
||||
type listProxyValue struct {
|
||||
list *types.AttributeValueMemberL
|
||||
}
|
||||
|
||||
func (bs listProxyValue) asGoValue() any {
|
||||
resultingList := make([]any, len(bs.list.Value))
|
||||
for i, item := range bs.list.Value {
|
||||
if av, _ := newExprValueFromAttributeValue(item); av != nil {
|
||||
resultingList[i] = av.asGoValue()
|
||||
} else {
|
||||
resultingList[i] = nil
|
||||
}
|
||||
}
|
||||
return resultingList
|
||||
}
|
||||
|
||||
func (bs listProxyValue) asAttributeValue() types.AttributeValue {
|
||||
return bs.list
|
||||
}
|
||||
|
||||
func (bs listProxyValue) len() int {
|
||||
return len(bs.list.Value)
|
||||
}
|
||||
|
||||
func (bs listProxyValue) valueAt(i int) (exprValue, error) {
|
||||
return newExprValueFromAttributeValue(bs.list.Value[i])
|
||||
}
|
||||
|
||||
func (bs listProxyValue) setValueAt(i int, newVal exprValue) {
|
||||
bs.list.Value[i] = newVal.asAttributeValue()
|
||||
}
|
||||
|
||||
func (bs listProxyValue) deleteValueAt(idx int) {
|
||||
newList := append([]types.AttributeValue{}, bs.list.Value[:idx]...)
|
||||
newList = append(newList, bs.list.Value[idx+1:]...)
|
||||
bs.list = &types.AttributeValueMemberL{Value: newList}
|
||||
}
|
||||
|
||||
func (s listProxyValue) typeName() string {
|
||||
return "L"
|
||||
}
|
||||
|
||||
type mapProxyValue struct {
|
||||
mapValue *types.AttributeValueMemberM
|
||||
}
|
||||
|
||||
func (bs mapProxyValue) asGoValue() any {
|
||||
resultingMap := make(map[string]any)
|
||||
for k, item := range bs.mapValue.Value {
|
||||
if av, _ := newExprValueFromAttributeValue(item); av != nil {
|
||||
resultingMap[k] = av.asGoValue()
|
||||
} else {
|
||||
resultingMap[k] = nil
|
||||
}
|
||||
}
|
||||
return resultingMap
|
||||
}
|
||||
|
||||
func (bs mapProxyValue) asAttributeValue() types.AttributeValue {
|
||||
return bs.mapValue
|
||||
}
|
||||
|
||||
func (bs mapProxyValue) len() int {
|
||||
return len(bs.mapValue.Value)
|
||||
}
|
||||
|
||||
func (bs mapProxyValue) hasKey(name string) bool {
|
||||
_, ok := bs.mapValue.Value[name]
|
||||
return ok
|
||||
}
|
||||
|
||||
func (bs mapProxyValue) valueOf(name string) (exprValue, error) {
|
||||
return newExprValueFromAttributeValue(bs.mapValue.Value[name])
|
||||
}
|
||||
|
||||
func (bs mapProxyValue) setValueOf(name string, newVal exprValue) {
|
||||
bs.mapValue.Value[name] = newVal.asAttributeValue()
|
||||
}
|
||||
|
||||
func (bs mapProxyValue) deleteValueOf(name string) {
|
||||
delete(bs.mapValue.Value, name)
|
||||
}
|
||||
|
||||
func (s mapProxyValue) typeName() string {
|
||||
return "M"
|
||||
}
|
||||
|
||||
type stringSetProxyValue struct {
|
||||
stringSet *types.AttributeValueMemberSS
|
||||
}
|
||||
|
||||
func (bs stringSetProxyValue) asGoValue() any {
|
||||
return bs.stringSet.Value
|
||||
}
|
||||
|
||||
func (bs stringSetProxyValue) asAttributeValue() types.AttributeValue {
|
||||
return bs.stringSet
|
||||
}
|
||||
|
||||
func (bs stringSetProxyValue) len() int {
|
||||
return len(bs.stringSet.Value)
|
||||
}
|
||||
|
||||
func (bs stringSetProxyValue) valueAt(i int) (exprValue, error) {
|
||||
return stringExprValue(bs.stringSet.Value[i]), nil
|
||||
}
|
||||
|
||||
func (bs stringSetProxyValue) setValueAt(i int, newVal exprValue) {
|
||||
if str, isStr := newVal.(stringableExprValue); isStr {
|
||||
bs.stringSet.Value[i] = str.asString()
|
||||
}
|
||||
}
|
||||
|
||||
func (bs stringSetProxyValue) deleteValueAt(idx int) {
|
||||
newList := append([]string{}, bs.stringSet.Value[:idx]...)
|
||||
newList = append(newList, bs.stringSet.Value[idx+1:]...)
|
||||
bs.stringSet = &types.AttributeValueMemberSS{Value: newList}
|
||||
}
|
||||
|
||||
func (s stringSetProxyValue) typeName() string {
|
||||
return "SS"
|
||||
}
|
||||
|
||||
type numberSetProxyValue struct {
|
||||
numberSet *types.AttributeValueMemberNS
|
||||
}
|
||||
|
||||
func (bs numberSetProxyValue) asGoValue() any {
|
||||
return bs.numberSet.Value
|
||||
}
|
||||
|
||||
func (bs numberSetProxyValue) asAttributeValue() types.AttributeValue {
|
||||
return bs.numberSet
|
||||
}
|
||||
|
||||
func (bs numberSetProxyValue) len() int {
|
||||
return len(bs.numberSet.Value)
|
||||
}
|
||||
|
||||
func (bs numberSetProxyValue) valueAt(i int) (exprValue, error) {
|
||||
fs, _, err := big.ParseFloat(bs.numberSet.Value[i], 10, 63, big.ToNearestEven)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return bigNumExprValue{fs}, nil
|
||||
}
|
||||
|
||||
func (bs numberSetProxyValue) setValueAt(i int, newVal exprValue) {
|
||||
if str, isStr := newVal.(numberableExprValue); isStr {
|
||||
bs.numberSet.Value[i] = str.asBigFloat().String()
|
||||
}
|
||||
}
|
||||
|
||||
func (bs numberSetProxyValue) deleteValueAt(idx int) {
|
||||
newList := append([]string{}, bs.numberSet.Value[:idx]...)
|
||||
newList = append(newList, bs.numberSet.Value[idx+1:]...)
|
||||
bs.numberSet = &types.AttributeValueMemberNS{Value: newList}
|
||||
}
|
||||
|
||||
func (s numberSetProxyValue) typeName() string {
|
||||
return "NS"
|
||||
}
|
||||
|
|
|
@ -2,59 +2,34 @@ package queryexpr
|
|||
|
||||
import (
|
||||
"github.com/aws/aws-sdk-go-v2/feature/dynamodb/expression"
|
||||
"github.com/lmika/audax/internal/dynamo-browse/models"
|
||||
"github.com/lmika/dynamo-browse/internal/dynamo-browse/models"
|
||||
"strconv"
|
||||
|
||||
"github.com/aws/aws-sdk-go-v2/service/dynamodb/types"
|
||||
"github.com/pkg/errors"
|
||||
)
|
||||
|
||||
func (a *astLiteralValue) evalToIR(ctx *evalContext, info *models.TableInfo) (irAtom, error) {
|
||||
v, err := a.goValue()
|
||||
v, err := a.exprValue()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return irValue{value: v}, nil
|
||||
}
|
||||
|
||||
func (a *astLiteralValue) dynamoValue() (types.AttributeValue, error) {
|
||||
if a == nil {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
goValue, err := a.goValue()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
switch v := goValue.(type) {
|
||||
case string:
|
||||
return &types.AttributeValueMemberS{Value: v}, nil
|
||||
case int64:
|
||||
return &types.AttributeValueMemberN{Value: strconv.FormatInt(v, 10)}, nil
|
||||
}
|
||||
|
||||
return nil, errors.New("unrecognised type")
|
||||
}
|
||||
|
||||
func (a *astLiteralValue) goValue() (any, error) {
|
||||
if a == nil {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
func (a *astLiteralValue) exprValue() (exprValue, error) {
|
||||
switch {
|
||||
case a.StringVal != nil:
|
||||
s, err := strconv.Unquote(*a.StringVal)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "cannot unquote string")
|
||||
}
|
||||
return s, nil
|
||||
return stringExprValue(s), nil
|
||||
case a.IntVal != nil:
|
||||
return *a.IntVal, nil
|
||||
return int64ExprValue(*a.IntVal), nil
|
||||
case a.TrueBoolValue:
|
||||
return true, nil
|
||||
return boolExprValue(true), nil
|
||||
case a.FalseBoolValue:
|
||||
return false, nil
|
||||
return boolExprValue(false), nil
|
||||
}
|
||||
return nil, errors.New("unrecognised type")
|
||||
}
|
||||
|
@ -78,17 +53,17 @@ func (a *astLiteralValue) String() string {
|
|||
}
|
||||
|
||||
type irValue struct {
|
||||
value any
|
||||
value exprValue
|
||||
}
|
||||
|
||||
func (i irValue) calcQueryForScan(info *models.TableInfo) (expression.ConditionBuilder, error) {
|
||||
return expression.ConditionBuilder{}, NodeCannotBeConvertedToQueryError{}
|
||||
}
|
||||
|
||||
func (i irValue) goValue() any {
|
||||
func (i irValue) exprValue() exprValue {
|
||||
return i.value
|
||||
}
|
||||
|
||||
func (a irValue) calcOperand(info *models.TableInfo) expression.OperandBuilder {
|
||||
return expression.Value(a.goValue())
|
||||
return expression.Value(a.value.asGoValue())
|
||||
}
|
||||
|
|
12
internal/dynamo-browse/models/relitems/relitem.go
Normal file
12
internal/dynamo-browse/models/relitems/relitem.go
Normal 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
|
||||
}
|
|
@ -2,7 +2,7 @@ package serialisable
|
|||
|
||||
import (
|
||||
"bytes"
|
||||
"github.com/lmika/audax/internal/dynamo-browse/models/queryexpr"
|
||||
"github.com/lmika/dynamo-browse/internal/dynamo-browse/models/queryexpr"
|
||||
"time"
|
||||
)
|
||||
|
||||
|
|
|
@ -1,20 +1,67 @@
|
|||
package models
|
||||
|
||||
import (
|
||||
"github.com/lmika/audax/internal/dynamo-browse/models/attrutils"
|
||||
"github.com/lmika/dynamo-browse/internal/dynamo-browse/models/attrutils"
|
||||
"sort"
|
||||
)
|
||||
|
||||
// sortedItems is a collection of items that is sorted.
|
||||
// Items are sorted based on the PK, and SK in ascending order
|
||||
type sortedItems struct {
|
||||
tableInfo *TableInfo
|
||||
items []Item
|
||||
criteria SortCriteria
|
||||
items []Item
|
||||
}
|
||||
|
||||
type SortField struct {
|
||||
Field FieldValueEvaluator
|
||||
Asc bool
|
||||
}
|
||||
|
||||
type SortCriteria struct {
|
||||
Fields []SortField
|
||||
}
|
||||
|
||||
func (sc SortCriteria) FirstField() SortField {
|
||||
if len(sc.Fields) == 0 {
|
||||
return SortField{}
|
||||
}
|
||||
return sc.Fields[0]
|
||||
}
|
||||
|
||||
func (sc SortCriteria) Equals(osc SortCriteria) bool {
|
||||
if len(sc.Fields) != len(osc.Fields) {
|
||||
return false
|
||||
}
|
||||
|
||||
for i := range osc.Fields {
|
||||
if sc.Fields[i].Field != osc.Fields[i].Field ||
|
||||
sc.Fields[i].Asc != osc.Fields[i].Asc {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
func (sc SortCriteria) Append(osc SortCriteria) SortCriteria {
|
||||
newItems := make([]SortField, 0, len(osc.Fields))
|
||||
newItems = append(newItems, sc.Fields...)
|
||||
newItems = append(newItems, osc.Fields...)
|
||||
return SortCriteria{Fields: newItems}
|
||||
}
|
||||
|
||||
func PKSKSortFilter(ti *TableInfo) SortCriteria {
|
||||
return SortCriteria{
|
||||
Fields: []SortField{
|
||||
{Field: SimpleFieldValueEvaluator(ti.Keys.PartitionKey), Asc: true},
|
||||
{Field: SimpleFieldValueEvaluator(ti.Keys.SortKey), Asc: true},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// Sort sorts the items in place
|
||||
func Sort(items []Item, tableInfo *TableInfo) {
|
||||
si := sortedItems{items: items, tableInfo: tableInfo}
|
||||
func Sort(items []Item, criteria SortCriteria) {
|
||||
si := sortedItems{items: items, criteria: criteria}
|
||||
sort.Sort(&si)
|
||||
}
|
||||
|
||||
|
@ -23,30 +70,21 @@ func (si *sortedItems) Len() int {
|
|||
}
|
||||
|
||||
func (si *sortedItems) Less(i, j int) bool {
|
||||
// Compare primary keys
|
||||
pv1, pv2 := si.items[i][si.tableInfo.Keys.PartitionKey], si.items[j][si.tableInfo.Keys.PartitionKey]
|
||||
pc, ok := attrutils.CompareScalarAttributes(pv1, pv2)
|
||||
if !ok {
|
||||
return i < j
|
||||
}
|
||||
|
||||
if pc < 0 {
|
||||
return true
|
||||
} else if pc > 0 {
|
||||
return false
|
||||
}
|
||||
|
||||
// Partition keys are equal, compare sort key
|
||||
if sortKey := si.tableInfo.Keys.SortKey; sortKey != "" {
|
||||
sv1, sv2 := si.items[i][sortKey], si.items[j][sortKey]
|
||||
sc, ok := attrutils.CompareScalarAttributes(sv1, sv2)
|
||||
for _, field := range si.criteria.Fields {
|
||||
// Compare primary keys
|
||||
pv1, pv2 := field.Field.EvaluateForItem(si.items[i]), field.Field.EvaluateForItem(si.items[j])
|
||||
pc, ok := attrutils.CompareScalarAttributes(pv1, pv2)
|
||||
if !ok {
|
||||
return i < j
|
||||
}
|
||||
|
||||
if sc < 0 {
|
||||
if !field.Asc {
|
||||
pc = -pc
|
||||
}
|
||||
|
||||
if pc < 0 {
|
||||
return true
|
||||
} else if sc > 0 {
|
||||
} else if pc > 0 {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
|
|
@ -4,7 +4,7 @@ import (
|
|||
"testing"
|
||||
|
||||
"github.com/aws/aws-sdk-go-v2/service/dynamodb/types"
|
||||
"github.com/lmika/audax/internal/dynamo-browse/models"
|
||||
"github.com/lmika/dynamo-browse/internal/dynamo-browse/models"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
|
@ -15,7 +15,7 @@ func TestSort(t *testing.T) {
|
|||
items := make([]models.Item, len(testStringData))
|
||||
copy(items, testStringData)
|
||||
|
||||
models.Sort(items, tableInfo)
|
||||
models.Sort(items, models.PKSKSortFilter(tableInfo))
|
||||
|
||||
assert.Equal(t, items[0], testStringData[1])
|
||||
assert.Equal(t, items[1], testStringData[2])
|
||||
|
@ -28,7 +28,7 @@ func TestSort(t *testing.T) {
|
|||
items := make([]models.Item, len(testNumberData))
|
||||
copy(items, testNumberData)
|
||||
|
||||
models.Sort(items, tableInfo)
|
||||
models.Sort(items, models.PKSKSortFilter(tableInfo))
|
||||
|
||||
assert.Equal(t, items[0], testNumberData[2])
|
||||
assert.Equal(t, items[1], testNumberData[1])
|
||||
|
@ -41,7 +41,7 @@ func TestSort(t *testing.T) {
|
|||
items := make([]models.Item, len(testBoolData))
|
||||
copy(items, testBoolData)
|
||||
|
||||
models.Sort(items, tableInfo)
|
||||
models.Sort(items, models.PKSKSortFilter(tableInfo))
|
||||
|
||||
assert.Equal(t, items[0], testBoolData[2])
|
||||
assert.Equal(t, items[1], testBoolData[1])
|
||||
|
|
|
@ -7,9 +7,9 @@ import (
|
|||
"github.com/aws/aws-sdk-go-v2/feature/dynamodb/expression"
|
||||
"github.com/aws/aws-sdk-go-v2/service/dynamodb"
|
||||
"github.com/aws/aws-sdk-go-v2/service/dynamodb/types"
|
||||
"github.com/lmika/audax/internal/common/sliceutils"
|
||||
"github.com/lmika/audax/internal/dynamo-browse/models"
|
||||
"github.com/lmika/audax/internal/dynamo-browse/services/jobs"
|
||||
"github.com/lmika/dynamo-browse/internal/common/sliceutils"
|
||||
"github.com/lmika/dynamo-browse/internal/dynamo-browse/models"
|
||||
"github.com/lmika/dynamo-browse/internal/dynamo-browse/services/jobs"
|
||||
"github.com/pkg/errors"
|
||||
"time"
|
||||
)
|
||||
|
|
|
@ -3,12 +3,12 @@ package dynamo_test
|
|||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"github.com/lmika/audax/internal/dynamo-browse/models"
|
||||
"github.com/lmika/dynamo-browse/internal/dynamo-browse/models"
|
||||
"testing"
|
||||
|
||||
"github.com/aws/aws-sdk-go-v2/service/dynamodb/types"
|
||||
"github.com/lmika/audax/internal/dynamo-browse/providers/dynamo"
|
||||
"github.com/lmika/audax/test/testdynamo"
|
||||
"github.com/lmika/dynamo-browse/internal/dynamo-browse/providers/dynamo"
|
||||
"github.com/lmika/dynamo-browse/test/testdynamo"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
|
|
|
@ -3,8 +3,8 @@ package inputhistorystore
|
|||
import (
|
||||
"context"
|
||||
"github.com/asdine/storm"
|
||||
"github.com/lmika/audax/internal/common/sliceutils"
|
||||
"github.com/lmika/audax/internal/common/workspaces"
|
||||
"github.com/lmika/dynamo-browse/internal/common/sliceutils"
|
||||
"github.com/lmika/dynamo-browse/internal/common/workspaces"
|
||||
"github.com/pkg/errors"
|
||||
"sort"
|
||||
"time"
|
||||
|
|
|
@ -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
|
||||
}
|
|
@ -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
|
||||
}
|
|
@ -2,7 +2,7 @@ package settingstore
|
|||
|
||||
import (
|
||||
"github.com/asdine/storm"
|
||||
"github.com/lmika/audax/internal/common/workspaces"
|
||||
"github.com/lmika/dynamo-browse/internal/common/workspaces"
|
||||
"github.com/pkg/errors"
|
||||
"io/fs"
|
||||
"log"
|
||||
|
@ -113,7 +113,7 @@ func (c *SettingStore) SetDefaultLimit(limit int) error {
|
|||
|
||||
func (c *SettingStore) getStringValue(key string, def string) (string, error) {
|
||||
var val string
|
||||
if err := c.ws.Get(settingBucket, keyTableReadOnly, &val); err != nil {
|
||||
if err := c.ws.Get(settingBucket, key, &val); err != nil {
|
||||
if errors.Is(err, storm.ErrNotFound) {
|
||||
return def, nil
|
||||
}
|
||||
|
|
|
@ -2,8 +2,8 @@ package workspacestore
|
|||
|
||||
import (
|
||||
"github.com/asdine/storm"
|
||||
"github.com/lmika/audax/internal/common/workspaces"
|
||||
"github.com/lmika/audax/internal/dynamo-browse/models/serialisable"
|
||||
"github.com/lmika/dynamo-browse/internal/common/workspaces"
|
||||
"github.com/lmika/dynamo-browse/internal/dynamo-browse/models/serialisable"
|
||||
"github.com/pkg/errors"
|
||||
"log"
|
||||
)
|
||||
|
|
|
@ -2,7 +2,7 @@ package inputhistory
|
|||
|
||||
import (
|
||||
"context"
|
||||
"github.com/lmika/audax/internal/dynamo-browse/services"
|
||||
"github.com/lmika/dynamo-browse/internal/dynamo-browse/services"
|
||||
"log"
|
||||
"strings"
|
||||
)
|
||||
|
|
|
@ -2,8 +2,8 @@ package itemrenderer
|
|||
|
||||
import (
|
||||
"fmt"
|
||||
"github.com/lmika/audax/internal/dynamo-browse/models"
|
||||
"github.com/lmika/audax/internal/dynamo-browse/models/itemrender"
|
||||
"github.com/lmika/dynamo-browse/internal/dynamo-browse/models"
|
||||
"github.com/lmika/dynamo-browse/internal/dynamo-browse/models/itemrender"
|
||||
"io"
|
||||
"text/tabwriter"
|
||||
)
|
||||
|
|
6
internal/dynamo-browse/services/pasteboardprovider.go
Normal file
6
internal/dynamo-browse/services/pasteboardprovider.go
Normal file
|
@ -0,0 +1,6 @@
|
|||
package services
|
||||
|
||||
type PasteboardProvider interface {
|
||||
ReadText() (string, bool)
|
||||
WriteText(bts []byte) error
|
||||
}
|
|
@ -7,8 +7,11 @@ package scriptmanager
|
|||
|
||||
import (
|
||||
"context"
|
||||
"github.com/cloudcmds/tamarin/object"
|
||||
"fmt"
|
||||
"log"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
"github.com/risor-io/risor/object"
|
||||
)
|
||||
|
||||
func printBuiltin(ctx context.Context, args ...object.Object) object.Object {
|
||||
|
@ -53,3 +56,47 @@ func printfBuiltin(ctx context.Context, args ...object.Object) object.Object {
|
|||
log.Printf("%s "+format, values...)
|
||||
return object.Nil
|
||||
}
|
||||
|
||||
// This is taken from the args package
|
||||
func require(funcName string, count int, args []object.Object) *object.Error {
|
||||
nArgs := len(args)
|
||||
if nArgs != count {
|
||||
if count == 1 {
|
||||
return object.Errorf(
|
||||
fmt.Sprintf("type error: %s() takes exactly 1 argument (%d given)",
|
||||
funcName, nArgs))
|
||||
}
|
||||
return object.Errorf(
|
||||
fmt.Sprintf("type error: %s() takes exactly %d arguments (%d given)",
|
||||
funcName, count, nArgs))
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func bindArgs(funcName string, args []object.Object, bindArgs ...any) *object.Error {
|
||||
if err := require(funcName, len(bindArgs), args); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
for i, bindArg := range bindArgs {
|
||||
switch t := bindArg.(type) {
|
||||
case *string:
|
||||
str, err := object.AsString(args[i])
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
*t = str
|
||||
case **object.Function:
|
||||
fnRes, isFnRes := args[i].(*object.Function)
|
||||
if !isFnRes {
|
||||
return object.NewError(errors.Errorf("expected arg %v to be a function, was %T", i, bindArg))
|
||||
}
|
||||
|
||||
*t = fnRes
|
||||
default:
|
||||
return object.NewError(errors.Errorf("unhandled arg type %v", i))
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
|
|
@ -3,7 +3,7 @@ package scriptmanager
|
|||
import (
|
||||
"context"
|
||||
"github.com/aws/aws-sdk-go-v2/service/dynamodb/types"
|
||||
"github.com/lmika/audax/internal/dynamo-browse/models"
|
||||
"github.com/lmika/dynamo-browse/internal/dynamo-browse/models"
|
||||
)
|
||||
|
||||
//go:generate mockery --with-expecter --name UIService
|
||||
|
@ -32,6 +32,7 @@ type SessionService interface {
|
|||
|
||||
type QueryOptions struct {
|
||||
TableName string
|
||||
IndexName string
|
||||
NamePlaceholders map[string]string
|
||||
ValuePlaceholders map[string]types.AttributeValue
|
||||
}
|
||||
|
|
|
@ -5,10 +5,10 @@ package mocks
|
|||
import (
|
||||
context "context"
|
||||
|
||||
models "github.com/lmika/audax/internal/dynamo-browse/models"
|
||||
models "github.com/lmika/dynamo-browse/internal/dynamo-browse/models"
|
||||
mock "github.com/stretchr/testify/mock"
|
||||
|
||||
scriptmanager "github.com/lmika/audax/internal/dynamo-browse/services/scriptmanager"
|
||||
scriptmanager "github.com/lmika/dynamo-browse/internal/dynamo-browse/services/scriptmanager"
|
||||
)
|
||||
|
||||
// SessionService is an autogenerated mock type for the SessionService type
|
||||
|
|
|
@ -3,11 +3,13 @@ package scriptmanager
|
|||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"github.com/cloudcmds/tamarin/arg"
|
||||
"github.com/cloudcmds/tamarin/object"
|
||||
"github.com/cloudcmds/tamarin/scope"
|
||||
"github.com/pkg/errors"
|
||||
"regexp"
|
||||
|
||||
"github.com/aws/aws-sdk-go-v2/service/dynamodb/types"
|
||||
"github.com/lmika/dynamo-browse/internal/dynamo-browse/models"
|
||||
"github.com/lmika/dynamo-browse/internal/dynamo-browse/models/queryexpr"
|
||||
"github.com/pkg/errors"
|
||||
"github.com/risor-io/risor/object"
|
||||
)
|
||||
|
||||
var (
|
||||
|
@ -18,22 +20,18 @@ type extModule struct {
|
|||
scriptPlugin *ScriptPlugin
|
||||
}
|
||||
|
||||
func (m *extModule) register(scp *scope.Scope) {
|
||||
modScope := scope.New(scope.Opts{})
|
||||
mod := object.NewModule("ext", modScope)
|
||||
|
||||
modScope.AddBuiltins([]*object.Builtin{
|
||||
object.NewBuiltin("command", m.command, mod),
|
||||
object.NewBuiltin("key_binding", m.keyBinding, mod),
|
||||
func (m *extModule) register() *object.Module {
|
||||
return object.NewBuiltinsModule("ext", map[string]object.Object{
|
||||
"command": object.NewBuiltin("command", m.command),
|
||||
"key_binding": object.NewBuiltin("key_binding", m.keyBinding),
|
||||
"related_items": object.NewBuiltin("related_items", m.relatedItem),
|
||||
})
|
||||
|
||||
scp.Declare("ext", mod, true)
|
||||
}
|
||||
|
||||
func (m *extModule) command(ctx context.Context, args ...object.Object) object.Object {
|
||||
thisEnv := scriptEnvFromCtx(ctx)
|
||||
|
||||
if err := arg.Require("ext.command", 2, args); err != nil {
|
||||
if err := require("ext.command", 2, args); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
|
@ -59,11 +57,12 @@ func (m *extModule) command(ctx context.Context, args ...object.Object) object.O
|
|||
}
|
||||
|
||||
newEnv := thisEnv
|
||||
newEnv.options = m.scriptPlugin.scriptService.options
|
||||
ctx = ctxWithScriptEnv(ctx, newEnv)
|
||||
|
||||
res := callFn(ctx, fnRes.Scope(), fnRes, objArgs)
|
||||
if object.IsError(res) {
|
||||
res, err := callFn(ctx, fnRes, objArgs)
|
||||
if err != nil {
|
||||
return errors.Errorf("command error '%v':%v - %v", m.scriptPlugin.name, cmdName, err)
|
||||
} else if object.IsError(res) {
|
||||
errObj := res.(*object.Error)
|
||||
return errors.Errorf("command error '%v':%v - %v", m.scriptPlugin.name, cmdName, errObj.Inspect())
|
||||
}
|
||||
|
@ -80,7 +79,7 @@ func (m *extModule) command(ctx context.Context, args ...object.Object) object.O
|
|||
func (m *extModule) keyBinding(ctx context.Context, args ...object.Object) object.Object {
|
||||
thisEnv := scriptEnvFromCtx(ctx)
|
||||
|
||||
if err := arg.Require("ext.key_binding", 3, args); err != nil {
|
||||
if err := require("ext.key_binding", 3, args); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
|
@ -119,11 +118,12 @@ func (m *extModule) keyBinding(ctx context.Context, args ...object.Object) objec
|
|||
}
|
||||
|
||||
newEnv := thisEnv
|
||||
newEnv.options = m.scriptPlugin.scriptService.options
|
||||
ctx = ctxWithScriptEnv(ctx, newEnv)
|
||||
|
||||
res := callFn(ctx, fnRes.Scope(), fnRes, objArgs)
|
||||
if object.IsError(res) {
|
||||
res, err := callFn(ctx, fnRes, objArgs)
|
||||
if err != nil {
|
||||
return errors.Errorf("command error '%v':%v - %v", m.scriptPlugin.name, bindingName, err)
|
||||
} else if object.IsError(res) {
|
||||
errObj := res.(*object.Error)
|
||||
return errors.Errorf("command error '%v':%v - %v", m.scriptPlugin.name, bindingName, errObj.Inspect())
|
||||
}
|
||||
|
@ -141,3 +141,130 @@ func (m *extModule) keyBinding(ctx context.Context, args ...object.Object) objec
|
|||
m.scriptPlugin.keyToKeyBinding[defaultKey] = fullBindingName
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *extModule) relatedItem(ctx context.Context, args ...object.Object) object.Object {
|
||||
thisEnv := scriptEnvFromCtx(ctx)
|
||||
|
||||
var (
|
||||
tableName string
|
||||
callbackFn *object.Function
|
||||
)
|
||||
if err := bindArgs("ext.related_items", args, &tableName, &callbackFn); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
callFn, hasCallFn := object.GetCallFunc(ctx)
|
||||
if !hasCallFn {
|
||||
return object.NewError(errors.New("no callFn found in context"))
|
||||
}
|
||||
|
||||
newHandler := func(ctx context.Context, rs *models.ResultSet, index int) ([]relatedItem, error) {
|
||||
newEnv := thisEnv
|
||||
ctx = ctxWithScriptEnv(ctx, newEnv)
|
||||
|
||||
res, err := callFn(ctx, callbackFn, []object.Object{
|
||||
newItemProxy(newResultSetProxy(rs), index),
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
return nil, errors.Errorf("script error '%v':related_item - %v", m.scriptPlugin.name, err)
|
||||
} else if object.IsError(res) {
|
||||
errObj := res.(*object.Error)
|
||||
return nil, errors.Errorf("script error '%v':related_item - %v", m.scriptPlugin.name, errObj.Inspect())
|
||||
}
|
||||
|
||||
itr, objErr := object.AsIterator(res)
|
||||
if err != nil {
|
||||
return nil, objErr.Value()
|
||||
}
|
||||
|
||||
var relItems []relatedItem
|
||||
for next, hasNext := itr.Next(ctx); hasNext; next, hasNext = itr.Next(ctx) {
|
||||
var newRelItem relatedItem
|
||||
|
||||
itemMap, objErr := object.AsMap(next)
|
||||
if err != nil {
|
||||
return nil, objErr.Value()
|
||||
}
|
||||
|
||||
labelName, objErr := object.AsString(itemMap.Get("label"))
|
||||
if objErr != nil {
|
||||
continue
|
||||
}
|
||||
newRelItem.label = labelName
|
||||
|
||||
var tableStr = ""
|
||||
if itemMap.Get("table") != object.Nil {
|
||||
tableStr, objErr = object.AsString(itemMap.Get("table"))
|
||||
if objErr != nil {
|
||||
continue
|
||||
}
|
||||
}
|
||||
newRelItem.table = tableStr
|
||||
|
||||
if selectFn, ok := itemMap.Get("on_select").(*object.Function); ok {
|
||||
newRelItem.onSelect = func() error {
|
||||
thisNewEnv := thisEnv
|
||||
ctx = ctxWithScriptEnv(ctx, thisNewEnv)
|
||||
|
||||
res, err := callFn(ctx, selectFn, []object.Object{})
|
||||
if err != nil {
|
||||
return errors.Errorf("rel error '%v' - %v", m.scriptPlugin.name, err)
|
||||
} else if object.IsError(res) {
|
||||
errObj := res.(*object.Error)
|
||||
return errors.Errorf("rel error '%v' - %v", m.scriptPlugin.name, errObj.Inspect())
|
||||
}
|
||||
return nil
|
||||
}
|
||||
} else {
|
||||
queryExprStr, objErr := object.AsString(itemMap.Get("query"))
|
||||
if objErr != nil {
|
||||
continue
|
||||
}
|
||||
|
||||
query, err := queryexpr.Parse(queryExprStr)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
|
||||
// Placeholders
|
||||
if argsVal, isArgsValMap := object.AsMap(itemMap.Get("args")); isArgsValMap == nil {
|
||||
namePlaceholders := make(map[string]string)
|
||||
valuePlaceholders := make(map[string]types.AttributeValue)
|
||||
|
||||
for k, val := range argsVal.Value() {
|
||||
switch v := val.(type) {
|
||||
case *object.String:
|
||||
namePlaceholders[k] = v.Value()
|
||||
valuePlaceholders[k] = &types.AttributeValueMemberS{Value: v.Value()}
|
||||
case *object.Int:
|
||||
valuePlaceholders[k] = &types.AttributeValueMemberN{Value: fmt.Sprint(v.Value())}
|
||||
case *object.Float:
|
||||
valuePlaceholders[k] = &types.AttributeValueMemberN{Value: fmt.Sprint(v.Value())}
|
||||
case *object.Bool:
|
||||
valuePlaceholders[k] = &types.AttributeValueMemberBOOL{Value: v.Value()}
|
||||
case *object.NilType:
|
||||
valuePlaceholders[k] = &types.AttributeValueMemberNULL{Value: true}
|
||||
default:
|
||||
continue
|
||||
}
|
||||
}
|
||||
|
||||
query = query.WithNameParams(namePlaceholders).WithValueParams(valuePlaceholders)
|
||||
}
|
||||
newRelItem.query = query
|
||||
}
|
||||
|
||||
relItems = append(relItems, newRelItem)
|
||||
}
|
||||
|
||||
return relItems, nil
|
||||
}
|
||||
|
||||
m.scriptPlugin.relatedItems = append(m.scriptPlugin.relatedItems, &relatedItemBuilder{
|
||||
table: tableName,
|
||||
itemProduction: newHandler,
|
||||
})
|
||||
|
||||
return nil
|
||||
}
|
||||
|
|
151
internal/dynamo-browse/services/scriptmanager/modext_test.go
Normal file
151
internal/dynamo-browse/services/scriptmanager/modext_test.go
Normal 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())
|
||||
})
|
||||
}
|
|
@ -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)
|
||||
}
|
|
@ -2,8 +2,8 @@ package scriptmanager_test
|
|||
|
||||
import (
|
||||
"context"
|
||||
"github.com/lmika/audax/internal/dynamo-browse/services/scriptmanager"
|
||||
"github.com/lmika/audax/internal/dynamo-browse/services/scriptmanager/mocks"
|
||||
"github.com/lmika/dynamo-browse/internal/dynamo-browse/services/scriptmanager"
|
||||
"github.com/lmika/dynamo-browse/internal/dynamo-browse/services/scriptmanager/mocks"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/mock"
|
||||
"testing"
|
||||
|
@ -15,49 +15,16 @@ func TestOSModule_Env(t *testing.T) {
|
|||
t.Setenv("EMPTY_VALUE", "")
|
||||
|
||||
testFS := testScriptFile(t, "test.tm", `
|
||||
assert(os.env("FULL_VALUE") == "this is a value")
|
||||
assert(os.env("EMPTY_VALUE") == "")
|
||||
assert(os.env("MISSING_VALUE") == nil)
|
||||
assert(os.getenv("FULL_VALUE") == "this is a value")
|
||||
assert(os.getenv("EMPTY_VALUE") == "")
|
||||
assert(os.getenv("MISSING_VALUE") == "")
|
||||
|
||||
assert(bool(os.env("FULL_VALUE")) == true)
|
||||
assert(bool(os.env("EMPTY_VALUE")) == false)
|
||||
assert(bool(os.env("MISSING_VALUE")) == false)
|
||||
assert(bool(os.getenv("FULL_VALUE")) == true)
|
||||
assert(bool(os.getenv("EMPTY_VALUE")) == false)
|
||||
assert(bool(os.getenv("MISSING_VALUE")) == false)
|
||||
`)
|
||||
|
||||
srv := scriptmanager.New(scriptmanager.WithFS(testFS))
|
||||
srv.SetDefaultOptions(scriptmanager.Options{
|
||||
OSExecShell: "/bin/bash",
|
||||
Permissions: scriptmanager.Permissions{
|
||||
AllowEnv: true,
|
||||
},
|
||||
})
|
||||
|
||||
ctx := context.Background()
|
||||
err := <-srv.RunAdHocScript(ctx, "test.tm")
|
||||
assert.NoError(t, err)
|
||||
})
|
||||
|
||||
t.Run("should return nil when no access to environment variables", func(t *testing.T) {
|
||||
t.Setenv("FULL_VALUE", "this is a value")
|
||||
t.Setenv("EMPTY_VALUE", "")
|
||||
|
||||
testFS := testScriptFile(t, "test.tm", `
|
||||
assert(os.env("FULL_VALUE") == nil)
|
||||
assert(os.env("EMPTY_VALUE") == nil)
|
||||
assert(os.env("MISSING_VALUE") == nil)
|
||||
|
||||
assert(bool(os.env("FULL_VALUE")) == false)
|
||||
assert(bool(os.env("EMPTY_VALUE")) == false)
|
||||
assert(bool(os.env("MISSING_VALUE")) == false)
|
||||
`)
|
||||
|
||||
srv := scriptmanager.New(scriptmanager.WithFS(testFS))
|
||||
srv.SetDefaultOptions(scriptmanager.Options{
|
||||
OSExecShell: "/bin/bash",
|
||||
Permissions: scriptmanager.Permissions{
|
||||
AllowEnv: false,
|
||||
},
|
||||
})
|
||||
|
||||
ctx := context.Background()
|
||||
err := <-srv.RunAdHocScript(ctx, "test.tm")
|
||||
|
@ -68,22 +35,14 @@ func TestOSModule_Env(t *testing.T) {
|
|||
func TestOSModule_Exec(t *testing.T) {
|
||||
t.Run("should run command and return stdout", func(t *testing.T) {
|
||||
mockedUIService := mocks.NewUIService(t)
|
||||
mockedUIService.EXPECT().PrintMessage(mock.Anything, "false")
|
||||
mockedUIService.EXPECT().PrintMessage(mock.Anything, "hello world\n")
|
||||
|
||||
testFS := testScriptFile(t, "test.tm", `
|
||||
res := os.exec('echo "hello world"')
|
||||
ui.print(res.is_err())
|
||||
ui.print(res.unwrap())
|
||||
res := exec('echo', ["hello world"]).stdout
|
||||
ui.print(res)
|
||||
`)
|
||||
|
||||
srv := scriptmanager.New(scriptmanager.WithFS(testFS))
|
||||
srv.SetDefaultOptions(scriptmanager.Options{
|
||||
OSExecShell: "/bin/bash",
|
||||
Permissions: scriptmanager.Permissions{
|
||||
AllowShellCommands: true,
|
||||
},
|
||||
})
|
||||
srv.SetIFaces(scriptmanager.Ifaces{
|
||||
UI: mockedUIService,
|
||||
})
|
||||
|
@ -94,73 +53,4 @@ func TestOSModule_Exec(t *testing.T) {
|
|||
|
||||
mockedUIService.AssertExpectations(t)
|
||||
})
|
||||
|
||||
t.Run("should refuse to execute command if do not have permissions", func(t *testing.T) {
|
||||
mockedUIService := mocks.NewUIService(t)
|
||||
mockedUIService.EXPECT().PrintMessage(mock.Anything, "true")
|
||||
|
||||
testFS := testScriptFile(t, "test.tm", `
|
||||
res := os.exec('echo "hello world"')
|
||||
ui.print(res.is_err())
|
||||
`)
|
||||
|
||||
srv := scriptmanager.New(scriptmanager.WithFS(testFS))
|
||||
srv.SetDefaultOptions(scriptmanager.Options{
|
||||
OSExecShell: "/bin/bash",
|
||||
Permissions: scriptmanager.Permissions{
|
||||
AllowShellCommands: false,
|
||||
},
|
||||
})
|
||||
srv.SetIFaces(scriptmanager.Ifaces{
|
||||
UI: mockedUIService,
|
||||
})
|
||||
|
||||
ctx := context.Background()
|
||||
err := <-srv.RunAdHocScript(ctx, "test.tm")
|
||||
assert.NoError(t, err)
|
||||
|
||||
mockedUIService.AssertExpectations(t)
|
||||
})
|
||||
|
||||
t.Run("should be able to change permissions which will affect plugins", func(t *testing.T) {
|
||||
mockedUIService := mocks.NewUIService(t)
|
||||
mockedUIService.EXPECT().PrintMessage(mock.Anything, "Loaded the plugin\n")
|
||||
mockedUIService.EXPECT().PrintMessage(mock.Anything, "true")
|
||||
|
||||
testFS := testScriptFile(t, "test.tm", `
|
||||
ext.command("mycommand", func() {
|
||||
ui.print(os.exec('echo "this cannot run"').is_err())
|
||||
})
|
||||
|
||||
ui.print(os.exec('echo "Loaded the plugin"').unwrap())
|
||||
`)
|
||||
|
||||
srv := scriptmanager.New(scriptmanager.WithFS(testFS))
|
||||
srv.SetDefaultOptions(scriptmanager.Options{
|
||||
OSExecShell: "/bin/bash",
|
||||
Permissions: scriptmanager.Permissions{
|
||||
AllowShellCommands: true,
|
||||
},
|
||||
})
|
||||
srv.SetIFaces(scriptmanager.Ifaces{
|
||||
UI: mockedUIService,
|
||||
})
|
||||
|
||||
ctx := context.Background()
|
||||
_, err := srv.LoadScript(ctx, "test.tm")
|
||||
assert.NoError(t, err)
|
||||
|
||||
srv.SetDefaultOptions(scriptmanager.Options{
|
||||
OSExecShell: "/bin/bash",
|
||||
Permissions: scriptmanager.Permissions{
|
||||
AllowShellCommands: false,
|
||||
},
|
||||
})
|
||||
|
||||
errChan := make(chan error)
|
||||
assert.NoError(t, srv.LookupCommand("mycommand").Invoke(ctx, []string{}, errChan))
|
||||
assert.NoError(t, waitForErr(t, errChan))
|
||||
|
||||
mockedUIService.AssertExpectations(t)
|
||||
})
|
||||
}
|
||||
|
|
|
@ -4,10 +4,8 @@ import (
|
|||
"context"
|
||||
"fmt"
|
||||
"github.com/aws/aws-sdk-go-v2/service/dynamodb/types"
|
||||
"github.com/cloudcmds/tamarin/arg"
|
||||
"github.com/cloudcmds/tamarin/object"
|
||||
"github.com/cloudcmds/tamarin/scope"
|
||||
"github.com/pkg/errors"
|
||||
"github.com/risor-io/risor/object"
|
||||
)
|
||||
|
||||
type sessionModule struct {
|
||||
|
@ -44,6 +42,11 @@ func (um *sessionModule) query(ctx context.Context, args ...object.Object) objec
|
|||
}
|
||||
}
|
||||
|
||||
// Index name
|
||||
if val, isStr := objMap.Get("index").(*object.String); isStr {
|
||||
options.IndexName = val.Value()
|
||||
}
|
||||
|
||||
// Placeholders
|
||||
if argsVal, isArgsValMap := objMap.Get("args").(*object.Map); isArgsValMap {
|
||||
options.NamePlaceholders = make(map[string]string)
|
||||
|
@ -72,13 +75,13 @@ func (um *sessionModule) query(ctx context.Context, args ...object.Object) objec
|
|||
resp, err := um.sessionService.Query(ctx, expr, options)
|
||||
|
||||
if err != nil {
|
||||
return object.NewErrResult(object.NewError(err))
|
||||
return object.NewError(err)
|
||||
}
|
||||
return object.NewOkResult(&resultSetProxy{resultSet: resp})
|
||||
return &resultSetProxy{resultSet: resp}
|
||||
}
|
||||
|
||||
func (um *sessionModule) resultSet(ctx context.Context, args ...object.Object) object.Object {
|
||||
if err := arg.Require("session.result_set", 0, args); err != nil {
|
||||
if err := require("session.result_set", 0, args); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
|
@ -90,7 +93,7 @@ func (um *sessionModule) resultSet(ctx context.Context, args ...object.Object) o
|
|||
}
|
||||
|
||||
func (um *sessionModule) selectedItem(ctx context.Context, args ...object.Object) object.Object {
|
||||
if err := arg.Require("session.result_set", 0, args); err != nil {
|
||||
if err := require("session.result_set", 0, args); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
|
@ -105,7 +108,7 @@ func (um *sessionModule) selectedItem(ctx context.Context, args ...object.Object
|
|||
}
|
||||
|
||||
func (um *sessionModule) setResultSet(ctx context.Context, args ...object.Object) object.Object {
|
||||
if err := arg.Require("session.set_result_set", 1, args); err != nil {
|
||||
if err := require("session.set_result_set", 1, args); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
|
@ -119,7 +122,7 @@ func (um *sessionModule) setResultSet(ctx context.Context, args ...object.Object
|
|||
}
|
||||
|
||||
func (um *sessionModule) currentTable(ctx context.Context, args ...object.Object) object.Object {
|
||||
if err := arg.Require("session.current_table", 0, args); err != nil {
|
||||
if err := require("session.current_table", 0, args); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
|
@ -131,17 +134,12 @@ func (um *sessionModule) currentTable(ctx context.Context, args ...object.Object
|
|||
return &tableProxy{table: rs.TableInfo}
|
||||
}
|
||||
|
||||
func (um *sessionModule) register(scp *scope.Scope) {
|
||||
modScope := scope.New(scope.Opts{})
|
||||
mod := object.NewModule("session", modScope)
|
||||
|
||||
modScope.AddBuiltins([]*object.Builtin{
|
||||
object.NewBuiltin("query", um.query, mod),
|
||||
object.NewBuiltin("current_table", um.currentTable, mod),
|
||||
object.NewBuiltin("result_set", um.resultSet, mod),
|
||||
object.NewBuiltin("selected_item", um.selectedItem, mod),
|
||||
object.NewBuiltin("set_result_set", um.setResultSet, mod),
|
||||
func (um *sessionModule) register() *object.Module {
|
||||
return object.NewBuiltinsModule("session", map[string]object.Object{
|
||||
"query": object.NewBuiltin("query", um.query),
|
||||
"current_table": object.NewBuiltin("current_table", um.currentTable),
|
||||
"result_set": object.NewBuiltin("result_set", um.resultSet),
|
||||
"selected_item": object.NewBuiltin("selected_item", um.selectedItem),
|
||||
"set_result_set": object.NewBuiltin("set_result_set", um.setResultSet),
|
||||
})
|
||||
|
||||
scp.Declare("session", mod, true)
|
||||
}
|
||||
|
|
|
@ -3,9 +3,9 @@ package scriptmanager_test
|
|||
import (
|
||||
"context"
|
||||
"github.com/aws/aws-sdk-go-v2/service/dynamodb/types"
|
||||
"github.com/lmika/audax/internal/dynamo-browse/models"
|
||||
"github.com/lmika/audax/internal/dynamo-browse/services/scriptmanager"
|
||||
"github.com/lmika/audax/internal/dynamo-browse/services/scriptmanager/mocks"
|
||||
"github.com/lmika/dynamo-browse/internal/dynamo-browse/models"
|
||||
"github.com/lmika/dynamo-browse/internal/dynamo-browse/services/scriptmanager"
|
||||
"github.com/lmika/dynamo-browse/internal/dynamo-browse/services/scriptmanager/mocks"
|
||||
"github.com/pkg/errors"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/mock"
|
||||
|
@ -102,7 +102,7 @@ func TestModSession_Query(t *testing.T) {
|
|||
mockedUIService.EXPECT().PrintMessage(mock.Anything, "res[1].attr('size(pk)') = 4")
|
||||
|
||||
testFS := testScriptFile(t, "test.tm", `
|
||||
res := session.query("some expr").unwrap()
|
||||
res := session.query("some expr")
|
||||
ui.print(res.length)
|
||||
ui.print("res[0]['pk'].S = ", res[0].attr("pk"))
|
||||
ui.print("res[1]['pk'].S = ", res[1].attr("pk"))
|
||||
|
@ -128,13 +128,9 @@ func TestModSession_Query(t *testing.T) {
|
|||
mockedSessionService.EXPECT().Query(mock.Anything, "some expr", scriptmanager.QueryOptions{}).Return(nil, errors.New("bang"))
|
||||
|
||||
mockedUIService := mocks.NewUIService(t)
|
||||
mockedUIService.EXPECT().PrintMessage(mock.Anything, "true")
|
||||
mockedUIService.EXPECT().PrintMessage(mock.Anything, "err(\"bang\")")
|
||||
|
||||
testFS := testScriptFile(t, "test.tm", `
|
||||
res := session.query("some expr")
|
||||
ui.print(res.is_err())
|
||||
ui.print(res)
|
||||
`)
|
||||
|
||||
srv := scriptmanager.New(scriptmanager.WithFS(testFS))
|
||||
|
@ -145,7 +141,7 @@ func TestModSession_Query(t *testing.T) {
|
|||
|
||||
ctx := context.Background()
|
||||
err := <-srv.RunAdHocScript(ctx, "test.tm")
|
||||
assert.NoError(t, err)
|
||||
assert.Error(t, err)
|
||||
|
||||
mockedUIService.AssertExpectations(t)
|
||||
mockedSessionService.AssertExpectations(t)
|
||||
|
@ -165,7 +161,7 @@ func TestModSession_Query(t *testing.T) {
|
|||
res := session.query("some expr", {
|
||||
table: "some-table",
|
||||
})
|
||||
assert(!res.is_err())
|
||||
assert(res)
|
||||
`)
|
||||
|
||||
srv := scriptmanager.New(scriptmanager.WithFS(testFS))
|
||||
|
@ -201,7 +197,7 @@ func TestModSession_Query(t *testing.T) {
|
|||
res := session.query("some expr", {
|
||||
table: session.result_set().table,
|
||||
})
|
||||
assert(!res.is_err())
|
||||
assert(res)
|
||||
`)
|
||||
|
||||
srv := scriptmanager.New(scriptmanager.WithFS(testFS))
|
||||
|
@ -242,7 +238,7 @@ func TestModSession_Query(t *testing.T) {
|
|||
value: "world",
|
||||
},
|
||||
})
|
||||
assert(!res.is_err())
|
||||
assert(res)
|
||||
`)
|
||||
|
||||
srv := scriptmanager.New(scriptmanager.WithFS(testFS))
|
||||
|
@ -288,7 +284,7 @@ func TestModSession_Query(t *testing.T) {
|
|||
"nil": nil,
|
||||
},
|
||||
})
|
||||
assert(!res.is_err())
|
||||
assert(res)
|
||||
`)
|
||||
|
||||
srv := scriptmanager.New(scriptmanager.WithFS(testFS))
|
||||
|
@ -315,7 +311,6 @@ func TestModSession_Query(t *testing.T) {
|
|||
"bad": func() { },
|
||||
},
|
||||
})
|
||||
assert(res.is_err())
|
||||
`)
|
||||
|
||||
srv := scriptmanager.New(scriptmanager.WithFS(testFS))
|
||||
|
@ -411,7 +406,7 @@ func TestModSession_SetResultSet(t *testing.T) {
|
|||
mockedUIService := mocks.NewUIService(t)
|
||||
|
||||
testFS := testScriptFile(t, "test.tm", `
|
||||
res := session.query("some expr").unwrap()
|
||||
res := session.query("some expr")
|
||||
session.set_result_set(res)
|
||||
`)
|
||||
|
||||
|
|
|
@ -2,10 +2,9 @@ package scriptmanager
|
|||
|
||||
import (
|
||||
"context"
|
||||
"github.com/cloudcmds/tamarin/arg"
|
||||
"github.com/cloudcmds/tamarin/object"
|
||||
"github.com/cloudcmds/tamarin/scope"
|
||||
"strings"
|
||||
|
||||
"github.com/risor-io/risor/object"
|
||||
)
|
||||
|
||||
type uiModule struct {
|
||||
|
@ -15,6 +14,10 @@ type uiModule struct {
|
|||
func (um *uiModule) print(ctx context.Context, args ...object.Object) object.Object {
|
||||
var msg strings.Builder
|
||||
for _, arg := range args {
|
||||
if arg == nil {
|
||||
continue
|
||||
}
|
||||
|
||||
switch a := arg.(type) {
|
||||
case *object.String:
|
||||
msg.WriteString(a.Value())
|
||||
|
@ -28,7 +31,7 @@ func (um *uiModule) print(ctx context.Context, args ...object.Object) object.Obj
|
|||
}
|
||||
|
||||
func (um *uiModule) prompt(ctx context.Context, args ...object.Object) object.Object {
|
||||
if err := arg.Require("ui.prompt", 1, args); err != nil {
|
||||
if err := require("ui.prompt", 1, args); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
|
@ -47,14 +50,9 @@ func (um *uiModule) prompt(ctx context.Context, args ...object.Object) object.Ob
|
|||
}
|
||||
}
|
||||
|
||||
func (um *uiModule) register(scp *scope.Scope) {
|
||||
modScope := scope.New(scope.Opts{})
|
||||
mod := object.NewModule("ui", modScope)
|
||||
|
||||
modScope.AddBuiltins([]*object.Builtin{
|
||||
object.NewBuiltin("print", um.print, mod),
|
||||
object.NewBuiltin("prompt", um.prompt, mod),
|
||||
func (um *uiModule) register() *object.Module {
|
||||
return object.NewBuiltinsModule("ui", map[string]object.Object{
|
||||
"print": object.NewBuiltin("print", um.print),
|
||||
"prompt": object.NewBuiltin("prompt", um.prompt),
|
||||
})
|
||||
|
||||
scp.Declare("ui", mod, true)
|
||||
}
|
||||
|
|
|
@ -2,8 +2,8 @@ package scriptmanager_test
|
|||
|
||||
import (
|
||||
"context"
|
||||
"github.com/lmika/audax/internal/dynamo-browse/services/scriptmanager"
|
||||
"github.com/lmika/audax/internal/dynamo-browse/services/scriptmanager/mocks"
|
||||
"github.com/lmika/dynamo-browse/internal/dynamo-browse/services/scriptmanager"
|
||||
"github.com/lmika/dynamo-browse/internal/dynamo-browse/services/scriptmanager/mocks"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/mock"
|
||||
"testing"
|
||||
|
|
|
@ -2,42 +2,12 @@ package scriptmanager
|
|||
|
||||
import (
|
||||
"context"
|
||||
"os"
|
||||
"github.com/risor-io/risor/limits"
|
||||
)
|
||||
|
||||
type Options struct {
|
||||
// OSExecShell is the shell to use for calls to 'os.exec'. If not defined,
|
||||
// it will use the value of the SHELL environment variable, otherwise it will
|
||||
// default to '/bin/bash'
|
||||
OSExecShell string
|
||||
|
||||
// Permissions are the permissions the script can execute in
|
||||
Permissions Permissions
|
||||
}
|
||||
|
||||
func (opts Options) configuredShell() string {
|
||||
if opts.OSExecShell != "" {
|
||||
return opts.OSExecShell
|
||||
}
|
||||
if shell, hasShell := os.LookupEnv("SHELL"); hasShell {
|
||||
return shell
|
||||
}
|
||||
return "/bin/bash"
|
||||
}
|
||||
|
||||
// Permissions control the set of permissions of a script
|
||||
type Permissions struct {
|
||||
// AllowShellCommands determines whether or not a script can execute shell commands.
|
||||
AllowShellCommands bool
|
||||
|
||||
// AllowEnv determines whether or not a script can access environment variables
|
||||
AllowEnv bool
|
||||
}
|
||||
|
||||
// scriptEnv is the runtime environment for a particular script execution
|
||||
type scriptEnv struct {
|
||||
filename string
|
||||
options Options
|
||||
}
|
||||
|
||||
type scriptEnvKeyType struct{}
|
||||
|
@ -50,5 +20,7 @@ func scriptEnvFromCtx(ctx context.Context) scriptEnv {
|
|||
}
|
||||
|
||||
func ctxWithScriptEnv(ctx context.Context, perms scriptEnv) context.Context {
|
||||
return context.WithValue(ctx, scriptEnvKey, perms)
|
||||
newCtx := context.WithValue(ctx, scriptEnvKey, perms)
|
||||
newCtx = limits.WithLimits(newCtx, limits.New())
|
||||
return newCtx
|
||||
}
|
||||
|
|
57
internal/dynamo-browse/services/scriptmanager/relitem.go
Normal file
57
internal/dynamo-browse/services/scriptmanager/relitem.go
Normal 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)
|
||||
}
|
|
@ -2,17 +2,37 @@ package scriptmanager
|
|||
|
||||
import (
|
||||
"context"
|
||||
"github.com/cloudcmds/tamarin/arg"
|
||||
"github.com/cloudcmds/tamarin/object"
|
||||
"github.com/lmika/audax/internal/dynamo-browse/models"
|
||||
"github.com/lmika/audax/internal/dynamo-browse/models/queryexpr"
|
||||
"time"
|
||||
|
||||
"github.com/aws/aws-sdk-go-v2/service/dynamodb/types"
|
||||
"github.com/lmika/dynamo-browse/internal/dynamo-browse/models"
|
||||
"github.com/lmika/dynamo-browse/internal/dynamo-browse/models/attrutils"
|
||||
"github.com/lmika/dynamo-browse/internal/dynamo-browse/models/queryexpr"
|
||||
"github.com/pkg/errors"
|
||||
"github.com/risor-io/risor/object"
|
||||
"github.com/risor-io/risor/op"
|
||||
)
|
||||
|
||||
type resultSetProxy struct {
|
||||
resultSet *models.ResultSet
|
||||
}
|
||||
|
||||
func newResultSetProxy(rs *models.ResultSet) *resultSetProxy {
|
||||
return &resultSetProxy{resultSet: rs}
|
||||
}
|
||||
|
||||
func (r *resultSetProxy) SetAttr(name string, value object.Object) error {
|
||||
return errors.Errorf("attribute error: %v", name)
|
||||
}
|
||||
|
||||
func (r *resultSetProxy) RunOperation(opType op.BinaryOpType, right object.Object) object.Object {
|
||||
return object.Errorf("op error: unsupported %v", opType)
|
||||
}
|
||||
|
||||
func (r *resultSetProxy) Cost() int {
|
||||
return len(r.resultSet.Items())
|
||||
}
|
||||
|
||||
func (r *resultSetProxy) Interface() interface{} {
|
||||
return r.resultSet
|
||||
}
|
||||
|
@ -95,17 +115,105 @@ func (r *resultSetProxy) GetAttr(name string) (object.Object, bool) {
|
|||
return &tableProxy{table: r.resultSet.TableInfo}, true
|
||||
case "length":
|
||||
return object.NewInt(int64(len(r.resultSet.Items()))), true
|
||||
case "find":
|
||||
return object.NewBuiltin("find", r.find), true
|
||||
case "merge":
|
||||
return object.NewBuiltin("merge", r.merge), true
|
||||
}
|
||||
|
||||
return nil, false
|
||||
}
|
||||
|
||||
func (i *resultSetProxy) find(ctx context.Context, args ...object.Object) object.Object {
|
||||
if objErr := require("resultset.find", 1, args); objErr != nil {
|
||||
return objErr
|
||||
}
|
||||
|
||||
str, objErr := object.AsString(args[0])
|
||||
if objErr != nil {
|
||||
return objErr
|
||||
}
|
||||
|
||||
modExpr, err := queryexpr.Parse(str)
|
||||
if err != nil {
|
||||
return object.Errorf("arg error: invalid path expression: %v", err)
|
||||
}
|
||||
|
||||
for idx, item := range i.resultSet.Items() {
|
||||
rs, err := modExpr.EvalItem(item)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
|
||||
if attrutils.Truthy(rs) {
|
||||
return newItemProxy(i, idx)
|
||||
}
|
||||
}
|
||||
|
||||
return object.Nil
|
||||
}
|
||||
|
||||
func (i *resultSetProxy) merge(ctx context.Context, args ...object.Object) object.Object {
|
||||
type pksk struct {
|
||||
pk types.AttributeValue
|
||||
sk types.AttributeValue
|
||||
}
|
||||
|
||||
if objErr := require("resultset.merge", 1, args); objErr != nil {
|
||||
return objErr
|
||||
}
|
||||
|
||||
otherRS, isRS := args[0].(*resultSetProxy)
|
||||
if !isRS {
|
||||
return object.NewError(errors.Errorf("type error: expected a resultset (got %v)", args[0].Type()))
|
||||
}
|
||||
|
||||
if !i.resultSet.TableInfo.Equal(otherRS.resultSet.TableInfo) {
|
||||
return object.Nil
|
||||
}
|
||||
|
||||
itemsInI := make(map[pksk]models.Item)
|
||||
newItems := make([]models.Item, 0, len(i.resultSet.Items())+len(otherRS.resultSet.Items()))
|
||||
for _, item := range i.resultSet.Items() {
|
||||
pk, sk := item.PKSK(i.resultSet.TableInfo)
|
||||
itemsInI[pksk{pk, sk}] = item
|
||||
newItems = append(newItems, item)
|
||||
}
|
||||
|
||||
for _, item := range otherRS.resultSet.Items() {
|
||||
pk, sk := item.PKSK(i.resultSet.TableInfo)
|
||||
if _, hasItem := itemsInI[pksk{pk, sk}]; !hasItem {
|
||||
newItems = append(newItems, item)
|
||||
}
|
||||
}
|
||||
|
||||
newResultSet := &models.ResultSet{
|
||||
Created: time.Now(),
|
||||
TableInfo: i.resultSet.TableInfo,
|
||||
}
|
||||
newResultSet.SetItems(newItems)
|
||||
|
||||
return &resultSetProxy{resultSet: newResultSet}
|
||||
}
|
||||
|
||||
type itemProxy struct {
|
||||
resultSetProxy *resultSetProxy
|
||||
itemIndex int
|
||||
item models.Item
|
||||
}
|
||||
|
||||
func (i *itemProxy) SetAttr(name string, value object.Object) error {
|
||||
return errors.Errorf("attribute error: %v", name)
|
||||
}
|
||||
|
||||
func (i *itemProxy) RunOperation(opType op.BinaryOpType, right object.Object) object.Object {
|
||||
return object.Errorf("op error: unsupported %v", opType)
|
||||
}
|
||||
|
||||
func (i *itemProxy) Cost() int {
|
||||
return len(i.item)
|
||||
}
|
||||
|
||||
func newItemProxy(rs *resultSetProxy, itemIndex int) *itemProxy {
|
||||
return &itemProxy{
|
||||
resultSetProxy: rs,
|
||||
|
@ -154,7 +262,7 @@ func (i *itemProxy) GetAttr(name string) (object.Object, bool) {
|
|||
}
|
||||
|
||||
func (i *itemProxy) value(ctx context.Context, args ...object.Object) object.Object {
|
||||
if objErr := arg.Require("item.attr", 1, args); objErr != nil {
|
||||
if objErr := require("item.attr", 1, args); objErr != nil {
|
||||
return objErr
|
||||
}
|
||||
|
||||
|
@ -180,7 +288,7 @@ func (i *itemProxy) value(ctx context.Context, args ...object.Object) object.Obj
|
|||
}
|
||||
|
||||
func (i *itemProxy) setValue(ctx context.Context, args ...object.Object) object.Object {
|
||||
if objErr := arg.Require("item.set_attr", 2, args); objErr != nil {
|
||||
if objErr := require("item.set_attr", 2, args); objErr != nil {
|
||||
return objErr
|
||||
}
|
||||
|
||||
|
@ -207,7 +315,7 @@ func (i *itemProxy) setValue(ctx context.Context, args ...object.Object) object.
|
|||
}
|
||||
|
||||
func (i *itemProxy) deleteAttr(ctx context.Context, args ...object.Object) object.Object {
|
||||
if objErr := arg.Require("item.delete_attr", 1, args); objErr != nil {
|
||||
if objErr := require("item.delete_attr", 1, args); objErr != nil {
|
||||
return objErr
|
||||
}
|
||||
|
||||
|
|
|
@ -2,13 +2,14 @@ package scriptmanager_test
|
|||
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
|
||||
"github.com/aws/aws-sdk-go-v2/service/dynamodb/types"
|
||||
"github.com/lmika/audax/internal/dynamo-browse/models"
|
||||
"github.com/lmika/audax/internal/dynamo-browse/services/scriptmanager"
|
||||
"github.com/lmika/audax/internal/dynamo-browse/services/scriptmanager/mocks"
|
||||
"github.com/lmika/dynamo-browse/internal/dynamo-browse/models"
|
||||
"github.com/lmika/dynamo-browse/internal/dynamo-browse/services/scriptmanager"
|
||||
"github.com/lmika/dynamo-browse/internal/dynamo-browse/services/scriptmanager/mocks"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/mock"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestResultSetProxy(t *testing.T) {
|
||||
|
@ -29,7 +30,7 @@ func TestResultSetProxy(t *testing.T) {
|
|||
mockedUIService := mocks.NewUIService(t)
|
||||
|
||||
testFS := testScriptFile(t, "test.tm", `
|
||||
res := session.query("some expr").unwrap()
|
||||
res := session.query("some expr")
|
||||
|
||||
// Test properties of the result set
|
||||
assert(res.table.name, "hello")
|
||||
|
@ -60,6 +61,123 @@ func TestResultSetProxy(t *testing.T) {
|
|||
})
|
||||
}
|
||||
|
||||
func TestResultSetProxy_Find(t *testing.T) {
|
||||
t.Run("should return the first item that matches the given expression", func(t *testing.T) {
|
||||
rs := &models.ResultSet{}
|
||||
rs.SetItems([]models.Item{
|
||||
{"pk": &types.AttributeValueMemberS{Value: "abc"}},
|
||||
{"pk": &types.AttributeValueMemberS{Value: "abc"}, "sk": &types.AttributeValueMemberS{Value: "abc"}, "primary": &types.AttributeValueMemberS{Value: "yes"}},
|
||||
{"pk": &types.AttributeValueMemberS{Value: "1232"}, "findMe": &types.AttributeValueMemberS{Value: "yes"}},
|
||||
{"pk": &types.AttributeValueMemberS{Value: "2345"}, "findMe": &types.AttributeValueMemberS{Value: "second"}},
|
||||
})
|
||||
|
||||
mockedSessionService := mocks.NewSessionService(t)
|
||||
mockedSessionService.EXPECT().Query(mock.Anything, "some expr", scriptmanager.QueryOptions{}).Return(rs, nil)
|
||||
|
||||
testFS := testScriptFile(t, "test.tm", `
|
||||
res := session.query("some expr")
|
||||
|
||||
assert(res.find('findMe is "any"').attr("pk") == "1232")
|
||||
assert(res.find('findMe = "second"').attr("pk") == "2345")
|
||||
assert(res.find('pk = sk').attr("primary") == "yes")
|
||||
|
||||
assert(res.find('findMe = "missing"') == nil)
|
||||
`)
|
||||
|
||||
srv := scriptmanager.New(scriptmanager.WithFS(testFS))
|
||||
srv.SetIFaces(scriptmanager.Ifaces{
|
||||
Session: mockedSessionService,
|
||||
})
|
||||
|
||||
ctx := context.Background()
|
||||
err := <-srv.RunAdHocScript(ctx, "test.tm")
|
||||
assert.NoError(t, err)
|
||||
|
||||
mockedSessionService.AssertExpectations(t)
|
||||
})
|
||||
}
|
||||
|
||||
func TestResultSetProxy_Merge(t *testing.T) {
|
||||
t.Run("should return a result set with items from both if both are from the same table", func(t *testing.T) {
|
||||
td := &models.TableInfo{Name: "test", Keys: models.KeyAttribute{PartitionKey: "pk", SortKey: "sk"}}
|
||||
|
||||
rs1 := &models.ResultSet{TableInfo: td}
|
||||
rs1.SetItems([]models.Item{
|
||||
{"pk": &types.AttributeValueMemberS{Value: "abc"}, "sk": &types.AttributeValueMemberS{Value: "123"}},
|
||||
})
|
||||
|
||||
rs2 := &models.ResultSet{TableInfo: td}
|
||||
rs2.SetItems([]models.Item{
|
||||
{"pk": &types.AttributeValueMemberS{Value: "bcd"}, "sk": &types.AttributeValueMemberS{Value: "234"}},
|
||||
})
|
||||
|
||||
mockedSessionService := mocks.NewSessionService(t)
|
||||
mockedSessionService.EXPECT().Query(mock.Anything, "rs1", scriptmanager.QueryOptions{}).Return(rs1, nil)
|
||||
mockedSessionService.EXPECT().Query(mock.Anything, "rs2", scriptmanager.QueryOptions{}).Return(rs2, nil)
|
||||
|
||||
testFS := testScriptFile(t, "test.tm", `
|
||||
r1 := session.query("rs1")
|
||||
r2 := session.query("rs2")
|
||||
|
||||
res := r1.merge(r2)
|
||||
|
||||
assert(res[0].attr("pk") == "abc")
|
||||
assert(res[0].attr("sk") == "123")
|
||||
assert(res[1].attr("pk") == "bcd")
|
||||
assert(res[1].attr("sk") == "234")
|
||||
`)
|
||||
|
||||
srv := scriptmanager.New(scriptmanager.WithFS(testFS))
|
||||
srv.SetIFaces(scriptmanager.Ifaces{
|
||||
Session: mockedSessionService,
|
||||
})
|
||||
|
||||
ctx := context.Background()
|
||||
err := <-srv.RunAdHocScript(ctx, "test.tm")
|
||||
assert.NoError(t, err)
|
||||
|
||||
mockedSessionService.AssertExpectations(t)
|
||||
})
|
||||
|
||||
t.Run("should return nil if result-sets are from different tables", func(t *testing.T) {
|
||||
td1 := &models.TableInfo{Name: "test", Keys: models.KeyAttribute{PartitionKey: "pk", SortKey: "sk"}}
|
||||
rs1 := &models.ResultSet{TableInfo: td1}
|
||||
rs1.SetItems([]models.Item{
|
||||
{"pk": &types.AttributeValueMemberS{Value: "abc"}, "sk": &types.AttributeValueMemberS{Value: "123"}},
|
||||
})
|
||||
|
||||
td2 := &models.TableInfo{Name: "test2", Keys: models.KeyAttribute{PartitionKey: "pk2", SortKey: "sk"}}
|
||||
rs2 := &models.ResultSet{TableInfo: td2}
|
||||
rs2.SetItems([]models.Item{
|
||||
{"pk": &types.AttributeValueMemberS{Value: "bcd"}, "sk": &types.AttributeValueMemberS{Value: "234"}},
|
||||
})
|
||||
|
||||
mockedSessionService := mocks.NewSessionService(t)
|
||||
mockedSessionService.EXPECT().Query(mock.Anything, "rs1", scriptmanager.QueryOptions{}).Return(rs1, nil)
|
||||
mockedSessionService.EXPECT().Query(mock.Anything, "rs2", scriptmanager.QueryOptions{}).Return(rs2, nil)
|
||||
|
||||
testFS := testScriptFile(t, "test.tm", `
|
||||
r1 := session.query("rs1")
|
||||
r2 := session.query("rs2")
|
||||
|
||||
res := r1.merge(r2)
|
||||
|
||||
assert(res == nil)
|
||||
`)
|
||||
|
||||
srv := scriptmanager.New(scriptmanager.WithFS(testFS))
|
||||
srv.SetIFaces(scriptmanager.Ifaces{
|
||||
Session: mockedSessionService,
|
||||
})
|
||||
|
||||
ctx := context.Background()
|
||||
err := <-srv.RunAdHocScript(ctx, "test.tm")
|
||||
assert.NoError(t, err)
|
||||
|
||||
mockedSessionService.AssertExpectations(t)
|
||||
})
|
||||
}
|
||||
|
||||
func TestResultSetProxy_GetAttr(t *testing.T) {
|
||||
t.Run("should return the value of items within a result set", func(t *testing.T) {
|
||||
rs := &models.ResultSet{}
|
||||
|
@ -87,7 +205,7 @@ func TestResultSetProxy_GetAttr(t *testing.T) {
|
|||
mockedSessionService.EXPECT().Query(mock.Anything, "some expr", scriptmanager.QueryOptions{}).Return(rs, nil)
|
||||
|
||||
testFS := testScriptFile(t, "test.tm", `
|
||||
res := session.query("some expr").unwrap()
|
||||
res := session.query("some expr")
|
||||
|
||||
assert(res[0].attr("pk") == "abc", "str attr")
|
||||
assert(res[0].attr("sk") == 123, "num attr")
|
||||
|
@ -164,7 +282,7 @@ func TestResultSetProxy_SetAttr(t *testing.T) {
|
|||
mockedUIService := mocks.NewUIService(t)
|
||||
|
||||
testFS := testScriptFile(t, "test.tm", `
|
||||
res := session.query("some expr").unwrap()
|
||||
res := session.query("some expr")
|
||||
|
||||
res[0].set_attr("pk", "bla-di-bla")
|
||||
res[0].set_attr("num", 123)
|
||||
|
@ -215,7 +333,7 @@ func TestResultSetProxy_DeleteAttr(t *testing.T) {
|
|||
mockedUIService := mocks.NewUIService(t)
|
||||
|
||||
testFS := testScriptFile(t, "test.tm", `
|
||||
res := session.query("some expr").unwrap()
|
||||
res := session.query("some expr")
|
||||
res[0].delete_attr("deleteMe")
|
||||
session.set_result_set(res)
|
||||
`)
|
||||
|
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue