Initial version of scripting (#40)
* scripting: added service and controller for scripting * scripting: have got prompts working Scripts are now running in a separate go-routine. When a prompt is encountered, the script is paused and the user is prompted for input. This means that the script no longer needs to worry about synchronisation issues. * scripting: started working on the session methods * scripting: added methods to get items and attributes * scripting: have got loading of scripts working These act more like plugins and allow defining new commands. * scripting: have got script scheduling working Scripts are now executed on a dedicated goroutine and only one script can run at any one time. * scripting: added session.set_result_set(rs) * scripting: upgraded tamarin to 0.14 * scripting: started working on set_value * tamarin: replaced ad-hoc path with query expressions * scripting: changed value() and set_value() to attr() and set_attr() Also added 'delete_attr()' * scripting: added os.exec() This method is controlled by permissions which govern whether shellouts are allowed Also fixed a resizing bug with the status window which was not properly handling status messages with newlines * scripting: added the session.current_item() method * scripting: added placeholders to query expressions * scripting: added support for setting and deleteing items with placeholders Also refactored the dot AST type so that it support placeholders. Placeholders are not yet supported for subrefs yet, they need to be identifiers. * scripting: made setting the result-set push the current result-set to the backstack * scripting: started working on byte encoding of attribute values * scripting: finished attrcodec * scripting: integrated codec into expression * scripting: added equals and hashcode to queryexpr This finally finishes the work required to store queries in the backstack * scripting: fixed some bugs with the back-stack * scripting: upgraded Tamarin * scripting: removed some commented out code
This commit is contained in:
parent
cd9700569c
commit
c89b09447c
|
@ -18,6 +18,7 @@ import (
|
||||||
"github.com/lmika/audax/internal/dynamo-browse/services/itemrenderer"
|
"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/jobs"
|
||||||
keybindings_service "github.com/lmika/audax/internal/dynamo-browse/services/keybindings"
|
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/tables"
|
||||||
"github.com/lmika/audax/internal/dynamo-browse/services/viewsnapshot"
|
"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"
|
||||||
|
@ -95,6 +96,7 @@ func main() {
|
||||||
tableService := tables.NewService(dynamoProvider, settingStore)
|
tableService := tables.NewService(dynamoProvider, settingStore)
|
||||||
workspaceService := viewsnapshot.NewService(resultSetSnapshotStore)
|
workspaceService := viewsnapshot.NewService(resultSetSnapshotStore)
|
||||||
itemRendererService := itemrenderer.NewService(uiStyles.ItemView.FieldType, uiStyles.ItemView.MetaInfo)
|
itemRendererService := itemrenderer.NewService(uiStyles.ItemView.FieldType, uiStyles.ItemView.MetaInfo)
|
||||||
|
scriptManagerService := scriptmanager.New()
|
||||||
jobsService := jobs.NewService(eventBus)
|
jobsService := jobs.NewService(eventBus)
|
||||||
|
|
||||||
state := controllers.NewState()
|
state := controllers.NewState()
|
||||||
|
@ -103,13 +105,15 @@ func main() {
|
||||||
tableWriteController := controllers.NewTableWriteController(state, tableService, jobsController, tableReadController, settingStore)
|
tableWriteController := controllers.NewTableWriteController(state, tableService, jobsController, tableReadController, settingStore)
|
||||||
columnsController := controllers.NewColumnsController(eventBus)
|
columnsController := controllers.NewColumnsController(eventBus)
|
||||||
exportController := controllers.NewExportController(state, columnsController)
|
exportController := controllers.NewExportController(state, columnsController)
|
||||||
settingsController := controllers.NewSettingsController(settingStore)
|
settingsController := controllers.NewSettingsController(settingStore, eventBus)
|
||||||
keyBindings := keybindings.Default()
|
keyBindings := keybindings.Default()
|
||||||
|
scriptController := controllers.NewScriptController(scriptManagerService, tableReadController, settingsController, eventBus)
|
||||||
|
|
||||||
keyBindingService := keybindings_service.NewService(keyBindings)
|
keyBindingService := keybindings_service.NewService(keyBindings)
|
||||||
keyBindingController := controllers.NewKeyBindingController(keyBindingService)
|
keyBindingController := controllers.NewKeyBindingController(keyBindingService)
|
||||||
|
|
||||||
commandController := commandctrl.NewCommandController()
|
commandController := commandctrl.NewCommandController()
|
||||||
|
commandController.AddCommandLookupExtension(scriptController)
|
||||||
|
|
||||||
model := ui.NewModel(
|
model := ui.NewModel(
|
||||||
tableReadController,
|
tableReadController,
|
||||||
|
@ -120,6 +124,8 @@ func main() {
|
||||||
jobsController,
|
jobsController,
|
||||||
itemRendererService,
|
itemRendererService,
|
||||||
commandController,
|
commandController,
|
||||||
|
scriptController,
|
||||||
|
eventBus,
|
||||||
keyBindingController,
|
keyBindingController,
|
||||||
keyBindings,
|
keyBindings,
|
||||||
)
|
)
|
||||||
|
@ -130,6 +136,8 @@ func main() {
|
||||||
p := tea.NewProgram(model, tea.WithAltScreen())
|
p := tea.NewProgram(model, tea.WithAltScreen())
|
||||||
|
|
||||||
jobsController.SetMessageSender(p.Send)
|
jobsController.SetMessageSender(p.Send)
|
||||||
|
scriptController.Init()
|
||||||
|
scriptController.SetMessageSender(p.Send)
|
||||||
|
|
||||||
log.Println("launching")
|
log.Println("launching")
|
||||||
if err := p.Start(); err != nil {
|
if err := p.Start(); err != nil {
|
||||||
|
|
32
go.mod
32
go.mod
|
@ -23,12 +23,16 @@ require (
|
||||||
github.com/lmika/go-bubble-table v0.2.2-0.20220616114432-6bbb2995e538
|
github.com/lmika/go-bubble-table v0.2.2-0.20220616114432-6bbb2995e538
|
||||||
github.com/lmika/gopkgs v0.0.0-20211210041137-0dc91e939890
|
github.com/lmika/gopkgs v0.0.0-20211210041137-0dc91e939890
|
||||||
github.com/lmika/shellwords v0.0.0-20140714114018-ce258dd729fe
|
github.com/lmika/shellwords v0.0.0-20140714114018-ce258dd729fe
|
||||||
|
github.com/mattn/go-runewidth v0.0.14
|
||||||
|
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/pkg/errors v0.9.1
|
||||||
github.com/stretchr/testify v1.7.1
|
github.com/stretchr/testify v1.8.1
|
||||||
golang.design/x/clipboard v0.6.2
|
golang.design/x/clipboard v0.6.2
|
||||||
)
|
)
|
||||||
|
|
||||||
require (
|
require (
|
||||||
|
atomicgo.dev/keyboard v0.2.8 // indirect
|
||||||
github.com/DataDog/zstd v1.5.2 // indirect
|
github.com/DataDog/zstd v1.5.2 // indirect
|
||||||
github.com/Sereal/Sereal v0.0.0-20220220040404-e0d1e550e879 // indirect
|
github.com/Sereal/Sereal v0.0.0-20220220040404-e0d1e550e879 // indirect
|
||||||
github.com/atotto/clipboard v0.1.4 // indirect
|
github.com/atotto/clipboard v0.1.4 // indirect
|
||||||
|
@ -44,32 +48,44 @@ require (
|
||||||
github.com/aws/aws-sdk-go-v2/service/sts v1.14.0 // indirect
|
github.com/aws/aws-sdk-go-v2/service/sts v1.14.0 // indirect
|
||||||
github.com/aws/smithy-go v1.11.3 // indirect
|
github.com/aws/smithy-go v1.11.3 // indirect
|
||||||
github.com/aymanbagabas/go-osc52 v1.0.3 // indirect
|
github.com/aymanbagabas/go-osc52 v1.0.3 // indirect
|
||||||
|
github.com/cloudcmds/tamarin v1.0.0 // indirect
|
||||||
github.com/containerd/console v1.0.3 // indirect
|
github.com/containerd/console v1.0.3 // indirect
|
||||||
github.com/davecgh/go-spew v1.1.1 // indirect
|
github.com/davecgh/go-spew v1.1.1 // indirect
|
||||||
|
github.com/fatih/color v1.13.0 // indirect
|
||||||
|
github.com/gofrs/uuid v4.3.1+incompatible // indirect
|
||||||
github.com/golang/protobuf v1.5.2 // indirect
|
github.com/golang/protobuf v1.5.2 // indirect
|
||||||
github.com/golang/snappy v0.0.4 // indirect
|
github.com/golang/snappy v0.0.4 // indirect
|
||||||
|
github.com/hashicorp/errwrap v1.0.0 // indirect
|
||||||
|
github.com/hashicorp/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/jmespath/go-jmespath v0.4.0 // indirect
|
github.com/jmespath/go-jmespath v0.4.0 // indirect
|
||||||
github.com/juju/ansiterm v0.0.0-20210929141451-8b71cc96ebdc // indirect
|
github.com/juju/ansiterm v0.0.0-20210929141451-8b71cc96ebdc // indirect
|
||||||
github.com/lucasb-eyer/go-colorful v1.2.0 // indirect
|
github.com/lucasb-eyer/go-colorful v1.2.0 // indirect
|
||||||
github.com/lunixbochs/vtclean v1.0.0 // indirect
|
github.com/lunixbochs/vtclean v1.0.0 // indirect
|
||||||
|
github.com/mattn/go-colorable v0.1.12 // indirect
|
||||||
github.com/mattn/go-isatty v0.0.16 // indirect
|
github.com/mattn/go-isatty v0.0.16 // indirect
|
||||||
github.com/mattn/go-localereader v0.0.1 // indirect
|
github.com/mattn/go-localereader v0.0.1 // indirect
|
||||||
github.com/mattn/go-runewidth v0.0.14 // indirect
|
|
||||||
github.com/muesli/ansi v0.0.0-20211031195517-c9f0611b6c70 // indirect
|
|
||||||
github.com/muesli/cancelreader v0.2.2 // indirect
|
github.com/muesli/cancelreader v0.2.2 // indirect
|
||||||
github.com/muesli/reflow v0.3.0 // indirect
|
|
||||||
github.com/muesli/termenv v0.13.0 // indirect
|
github.com/muesli/termenv v0.13.0 // indirect
|
||||||
github.com/pmezard/go-difflib v1.0.0 // indirect
|
github.com/pmezard/go-difflib v1.0.0 // indirect
|
||||||
github.com/rivo/uniseg v0.4.2 // indirect
|
github.com/rivo/uniseg v0.4.2 // indirect
|
||||||
github.com/sahilm/fuzzy v0.1.0 // indirect
|
github.com/sahilm/fuzzy v0.1.0 // indirect
|
||||||
|
github.com/stretchr/objx v0.5.0 // indirect
|
||||||
|
github.com/tidwall/gjson v1.14.3 // indirect
|
||||||
|
github.com/tidwall/match v1.1.1 // indirect
|
||||||
|
github.com/tidwall/pretty v1.2.1 // indirect
|
||||||
github.com/vmihailenco/msgpack v4.0.4+incompatible // indirect
|
github.com/vmihailenco/msgpack v4.0.4+incompatible // indirect
|
||||||
|
github.com/wI2L/jsondiff v0.3.0 // indirect
|
||||||
go.etcd.io/bbolt v1.3.6 // indirect
|
go.etcd.io/bbolt v1.3.6 // indirect
|
||||||
golang.org/x/exp v0.0.0-20190731235908-ec7cb31e5a56 // indirect
|
golang.org/x/crypto v0.0.0-20220829220503-c86fa9a7ed90 // indirect
|
||||||
|
golang.org/x/exp v0.0.0-20230108222341-4b8118a2686a // indirect
|
||||||
golang.org/x/image v0.0.0-20211028202545-6944b10bf410 // indirect
|
golang.org/x/image v0.0.0-20211028202545-6944b10bf410 // indirect
|
||||||
golang.org/x/mobile v0.0.0-20210716004757-34ab1303b554 // indirect
|
golang.org/x/mobile v0.0.0-20210716004757-34ab1303b554 // indirect
|
||||||
golang.org/x/sys v0.0.0-20220928140112-f11e5e49a4ec // indirect
|
golang.org/x/sys v0.1.0 // indirect
|
||||||
golang.org/x/term v0.0.0-20220919170432-7a66f970e087 // indirect
|
golang.org/x/term v0.0.0-20220919170432-7a66f970e087 // indirect
|
||||||
golang.org/x/text v0.3.7 // indirect
|
golang.org/x/text v0.3.8 // indirect
|
||||||
google.golang.org/appengine v1.6.7 // indirect
|
google.golang.org/appengine v1.6.7 // indirect
|
||||||
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c // indirect
|
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||||
)
|
)
|
||||||
|
|
86
go.sum
86
go.sum
|
@ -1,6 +1,15 @@
|
||||||
|
atomicgo.dev/keyboard v0.2.8 h1:Di09BitwZgdTV1hPyX/b9Cqxi8HVuJQwWivnZUEqlj4=
|
||||||
|
atomicgo.dev/keyboard v0.2.8/go.mod h1:BC4w9g00XkxH/f1HXhW2sXmJFOCWbKn9xrOunSFtExQ=
|
||||||
github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo=
|
github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo=
|
||||||
github.com/DataDog/zstd v1.5.2 h1:vUG4lAyuPCXO0TLbXvPv7EB7cNK1QV/luu55UHLrrn8=
|
github.com/DataDog/zstd v1.5.2 h1:vUG4lAyuPCXO0TLbXvPv7EB7cNK1QV/luu55UHLrrn8=
|
||||||
github.com/DataDog/zstd v1.5.2/go.mod h1:g4AWEaM3yOg3HYfnJ3YIawPnVdXJh9QME85blwSAmyw=
|
github.com/DataDog/zstd v1.5.2/go.mod h1:g4AWEaM3yOg3HYfnJ3YIawPnVdXJh9QME85blwSAmyw=
|
||||||
|
github.com/MarvinJWendt/testza v0.1.0/go.mod h1:7AxNvlfeHP7Z/hDQ5JtE3OKYT3XFUeLCDE2DQninSqs=
|
||||||
|
github.com/MarvinJWendt/testza v0.2.1/go.mod h1:God7bhG8n6uQxwdScay+gjm9/LnO4D3kkcZX4hv9Rp8=
|
||||||
|
github.com/MarvinJWendt/testza v0.2.8/go.mod h1:nwIcjmr0Zz+Rcwfh3/4UhBp7ePKVhuBExvZqnKYWlII=
|
||||||
|
github.com/MarvinJWendt/testza v0.2.10/go.mod h1:pd+VWsoGUiFtq+hRKSU1Bktnn+DMCSrDrXDpX2bG66k=
|
||||||
|
github.com/MarvinJWendt/testza v0.2.12/go.mod h1:JOIegYyV7rX+7VZ9r77L/eH6CfJHHzXjB69adAhzZkI=
|
||||||
|
github.com/MarvinJWendt/testza v0.3.0/go.mod h1:eFcL4I0idjtIx8P9C6KkAuLgATNKpX4/2oUqKc6bF2c=
|
||||||
|
github.com/MarvinJWendt/testza v0.4.2/go.mod h1:mSdhXiKH8sg/gQehJ63bINcCKp7RtYewEjXsvsVUPbE=
|
||||||
github.com/Sereal/Sereal v0.0.0-20220220040404-e0d1e550e879 h1:M5ptEKnqKqpFTKbe+p5zEf3ro1deJ6opUz5j3g3/ErQ=
|
github.com/Sereal/Sereal v0.0.0-20220220040404-e0d1e550e879 h1:M5ptEKnqKqpFTKbe+p5zEf3ro1deJ6opUz5j3g3/ErQ=
|
||||||
github.com/Sereal/Sereal v0.0.0-20220220040404-e0d1e550e879/go.mod h1:D0JMgToj/WdxCgd30Kc1UcA9E+WdZoJqeVOuYW7iTBM=
|
github.com/Sereal/Sereal v0.0.0-20220220040404-e0d1e550e879/go.mod h1:D0JMgToj/WdxCgd30Kc1UcA9E+WdZoJqeVOuYW7iTBM=
|
||||||
github.com/alecthomas/participle v0.7.1 h1:2bN7reTw//5f0cugJcTOnY/NYZcWQOaajW+BwZB5xWs=
|
github.com/alecthomas/participle v0.7.1 h1:2bN7reTw//5f0cugJcTOnY/NYZcWQOaajW+BwZB5xWs=
|
||||||
|
@ -13,6 +22,7 @@ github.com/alecthomas/repr v0.0.0-20181024024818-d37bc2a10ba1/go.mod h1:xTS7Pm1p
|
||||||
github.com/alecthomas/repr v0.1.0 h1:ENn2e1+J3k09gyj2shc0dHr/yjaWSHRlrJ4DPMevDqE=
|
github.com/alecthomas/repr v0.1.0 h1:ENn2e1+J3k09gyj2shc0dHr/yjaWSHRlrJ4DPMevDqE=
|
||||||
github.com/asdine/storm v2.1.2+incompatible h1:dczuIkyqwY2LrtXPz8ixMrU/OFgZp71kbKTHGrXYt/Q=
|
github.com/asdine/storm v2.1.2+incompatible h1:dczuIkyqwY2LrtXPz8ixMrU/OFgZp71kbKTHGrXYt/Q=
|
||||||
github.com/asdine/storm v2.1.2+incompatible/go.mod h1:RarYDc9hq1UPLImuiXK3BIWPJLdIygvV3PsInK0FbVQ=
|
github.com/asdine/storm v2.1.2+incompatible/go.mod h1:RarYDc9hq1UPLImuiXK3BIWPJLdIygvV3PsInK0FbVQ=
|
||||||
|
github.com/atomicgo/cursor v0.0.1/go.mod h1:cBON2QmmrysudxNBFthvMtN32r3jxVRIvzkUiF/RuIk=
|
||||||
github.com/atotto/clipboard v0.1.4 h1:EH0zSVneZPSuFR11BlR9YppQTVDbh5+16AmcJi4g1z4=
|
github.com/atotto/clipboard v0.1.4 h1:EH0zSVneZPSuFR11BlR9YppQTVDbh5+16AmcJi4g1z4=
|
||||||
github.com/atotto/clipboard v0.1.4/go.mod h1:ZY9tmq7sm5xIbd9bOK4onWV4S6X0u6GY7Vn0Yu86PYI=
|
github.com/atotto/clipboard v0.1.4/go.mod h1:ZY9tmq7sm5xIbd9bOK4onWV4S6X0u6GY7Vn0Yu86PYI=
|
||||||
github.com/aws/aws-sdk-go-v2 v1.13.0/go.mod h1:L6+ZpqHaLbAaxsqV0L4cvxZY7QupWJB4fhkf8LXvC7w=
|
github.com/aws/aws-sdk-go-v2 v1.13.0/go.mod h1:L6+ZpqHaLbAaxsqV0L4cvxZY7QupWJB4fhkf8LXvC7w=
|
||||||
|
@ -80,11 +90,21 @@ github.com/charmbracelet/lipgloss v0.5.0 h1:lulQHuVeodSgDez+3rGiuxlPVXSnhth442DA
|
||||||
github.com/charmbracelet/lipgloss v0.5.0/go.mod h1:EZLha/HbzEt7cYqdFPovlqy5FZPj0xFhg5SaqxScmgs=
|
github.com/charmbracelet/lipgloss v0.5.0/go.mod h1:EZLha/HbzEt7cYqdFPovlqy5FZPj0xFhg5SaqxScmgs=
|
||||||
github.com/charmbracelet/lipgloss v0.6.0 h1:1StyZB9vBSOyuZxQUcUwGr17JmojPNm87inij9N3wJY=
|
github.com/charmbracelet/lipgloss v0.6.0 h1:1StyZB9vBSOyuZxQUcUwGr17JmojPNm87inij9N3wJY=
|
||||||
github.com/charmbracelet/lipgloss v0.6.0/go.mod h1:tHh2wr34xcHjC2HCXIlGSG1jaDF0S0atAUvBMP6Ppuk=
|
github.com/charmbracelet/lipgloss v0.6.0/go.mod h1:tHh2wr34xcHjC2HCXIlGSG1jaDF0S0atAUvBMP6Ppuk=
|
||||||
|
github.com/cloudcmds/tamarin v0.0.12 h1:xigMcfala5I81fh+6FSaJpjiKyWTOqzdf/GIQnsk/oc=
|
||||||
|
github.com/cloudcmds/tamarin v0.0.12/go.mod h1:U1aHBoAFtJbI9jzgaj8TUo9C6vfzUKzn1OhWKIdigVM=
|
||||||
|
github.com/cloudcmds/tamarin v0.0.14 h1:LNHz/CplhiM9u4SVy/9dGjyXpMTvKMmWcuO0+f0t5Ls=
|
||||||
|
github.com/cloudcmds/tamarin v0.0.14/go.mod h1:U1aHBoAFtJbI9jzgaj8TUo9C6vfzUKzn1OhWKIdigVM=
|
||||||
|
github.com/cloudcmds/tamarin v1.0.0 h1:PhrJ74FCUJo24/nIPXnQe9E3WVEIYo4aG58pICOMDBE=
|
||||||
|
github.com/cloudcmds/tamarin v1.0.0/go.mod h1:U1aHBoAFtJbI9jzgaj8TUo9C6vfzUKzn1OhWKIdigVM=
|
||||||
github.com/containerd/console v1.0.3 h1:lIr7SlA5PxZyMV30bDW0MGbiOPXwc63yRuCP0ARubLw=
|
github.com/containerd/console v1.0.3 h1:lIr7SlA5PxZyMV30bDW0MGbiOPXwc63yRuCP0ARubLw=
|
||||||
github.com/containerd/console v1.0.3/go.mod h1:7LqA/THxQ86k76b8c/EMSiaJ3h1eZkMkXar0TQ1gf3U=
|
github.com/containerd/console v1.0.3/go.mod h1:7LqA/THxQ86k76b8c/EMSiaJ3h1eZkMkXar0TQ1gf3U=
|
||||||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||||
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||||
|
github.com/fatih/color v1.13.0 h1:8LOYc1KYPPmyKMuN8QV2DNRWNbLo6LZ0iLs8+mlH53w=
|
||||||
|
github.com/fatih/color v1.13.0/go.mod h1:kLAiJbzzSOZDVNGyDpeOxJ47H46qBXwg5ILebYFFOfk=
|
||||||
|
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/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
|
github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
|
||||||
github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk=
|
github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk=
|
||||||
github.com/golang/protobuf v1.5.2 h1:ROPKBNFfQgOUMifHyP+KYbvpjbdoFNs+aK7DXlji0Tw=
|
github.com/golang/protobuf v1.5.2 h1:ROPKBNFfQgOUMifHyP+KYbvpjbdoFNs+aK7DXlji0Tw=
|
||||||
|
@ -99,12 +119,28 @@ github.com/google/go-cmp v0.5.8 h1:e6P7q2lk1O+qJJb4BtCQXlK8vWEO8V1ZeuEdJNOqZyg=
|
||||||
github.com/google/go-cmp v0.5.8/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
|
github.com/google/go-cmp v0.5.8/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
|
||||||
github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I=
|
github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I=
|
||||||
github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||||
|
github.com/gookit/color v1.4.2/go.mod h1:fqRyamkC1W8uxl+lxCQxOT09l/vYfZ+QeiX3rKQHCoQ=
|
||||||
|
github.com/gookit/color v1.5.0/go.mod h1:43aQb+Zerm/BWh2GnrgOQm7ffz7tvQXEKV6BFMl7wAo=
|
||||||
|
github.com/hashicorp/errwrap v1.0.0 h1:hLrqtEDnRye3+sgx6z4qVLNuviH3MR5aQ0ykNJa/UYA=
|
||||||
|
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/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/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/jmespath/go-jmespath v0.4.0 h1:BEgLn5cpjn8UN1mAw4NjwDrS35OdebyEtFe+9YPoQUg=
|
github.com/jmespath/go-jmespath v0.4.0 h1:BEgLn5cpjn8UN1mAw4NjwDrS35OdebyEtFe+9YPoQUg=
|
||||||
github.com/jmespath/go-jmespath v0.4.0/go.mod h1:T8mJZnbsbmF+m6zOOFylbeCJqk5+pHWvzYPziyZiYoo=
|
github.com/jmespath/go-jmespath v0.4.0/go.mod h1:T8mJZnbsbmF+m6zOOFylbeCJqk5+pHWvzYPziyZiYoo=
|
||||||
github.com/jmespath/go-jmespath/internal/testify v1.5.1 h1:shLQSRRSCCPj3f2gpwzGwWFoC7ycTf1rcQZHOlsJ6N8=
|
github.com/jmespath/go-jmespath/internal/testify v1.5.1 h1:shLQSRRSCCPj3f2gpwzGwWFoC7ycTf1rcQZHOlsJ6N8=
|
||||||
github.com/jmespath/go-jmespath/internal/testify v1.5.1/go.mod h1:L3OGu8Wl2/fWfCI6z80xFu9LTZmf1ZRjMHUOPmWr69U=
|
github.com/jmespath/go-jmespath/internal/testify v1.5.1/go.mod h1:L3OGu8Wl2/fWfCI6z80xFu9LTZmf1ZRjMHUOPmWr69U=
|
||||||
github.com/juju/ansiterm v0.0.0-20210929141451-8b71cc96ebdc h1:ZQrgZFsLzkw7o3CoDzsfBhx0bf/1rVBXrLy8dXKRe8o=
|
github.com/juju/ansiterm v0.0.0-20210929141451-8b71cc96ebdc h1:ZQrgZFsLzkw7o3CoDzsfBhx0bf/1rVBXrLy8dXKRe8o=
|
||||||
github.com/juju/ansiterm v0.0.0-20210929141451-8b71cc96ebdc/go.mod h1:PyXUpnI3olx3bsPcHt98FGPX/KCFZ1Fi+hw1XLI6384=
|
github.com/juju/ansiterm v0.0.0-20210929141451-8b71cc96ebdc/go.mod h1:PyXUpnI3olx3bsPcHt98FGPX/KCFZ1Fi+hw1XLI6384=
|
||||||
|
github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg=
|
||||||
|
github.com/klauspost/cpuid/v2 v2.0.10/go.mod h1:g2LTdtYhdyuGPqyWyv7qRAmj1WBqxuObKfj5c0PQa7c=
|
||||||
|
github.com/klauspost/cpuid/v2 v2.0.12/go.mod h1:g2LTdtYhdyuGPqyWyv7qRAmj1WBqxuObKfj5c0PQa7c=
|
||||||
|
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
|
||||||
github.com/kr/pretty v0.2.1 h1:Fmg33tUaq4/8ym9TJN1x7sLJnHVwhP33CNkpYV/7rwI=
|
github.com/kr/pretty v0.2.1 h1:Fmg33tUaq4/8ym9TJN1x7sLJnHVwhP33CNkpYV/7rwI=
|
||||||
github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI=
|
github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI=
|
||||||
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
|
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
|
||||||
|
@ -124,7 +160,10 @@ github.com/lucasb-eyer/go-colorful v1.2.0 h1:1nnpGOrhyZZuNyfu1QjKiUICQ74+3FNCN69
|
||||||
github.com/lucasb-eyer/go-colorful v1.2.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0=
|
github.com/lucasb-eyer/go-colorful v1.2.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0=
|
||||||
github.com/lunixbochs/vtclean v1.0.0 h1:xu2sLAri4lGiovBDQKxl5mrXyESr3gUr5m5SM5+LVb8=
|
github.com/lunixbochs/vtclean v1.0.0 h1:xu2sLAri4lGiovBDQKxl5mrXyESr3gUr5m5SM5+LVb8=
|
||||||
github.com/lunixbochs/vtclean v1.0.0/go.mod h1:pHhQNgMf3btfWnGBVipUOjRYhoOsdGqdm/+2c2E2WMI=
|
github.com/lunixbochs/vtclean v1.0.0/go.mod h1:pHhQNgMf3btfWnGBVipUOjRYhoOsdGqdm/+2c2E2WMI=
|
||||||
|
github.com/mattn/go-colorable v0.1.9/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc=
|
||||||
github.com/mattn/go-colorable v0.1.10/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc=
|
github.com/mattn/go-colorable v0.1.10/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc=
|
||||||
|
github.com/mattn/go-colorable v0.1.12 h1:jF+Du6AlPIjs2BiUiQlKOX0rt3SujHxPnksPKZbaA40=
|
||||||
|
github.com/mattn/go-colorable v0.1.12/go.mod h1:u5H1YNBxpqRaxsYJYSkiCWKzEfiAb1Gb520KVy5xxl4=
|
||||||
github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU=
|
github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU=
|
||||||
github.com/mattn/go-isatty v0.0.14 h1:yVuAays6BHfxijgZPzw+3Zlu5yQgKGP2/hcQbHb7S9Y=
|
github.com/mattn/go-isatty v0.0.14 h1:yVuAays6BHfxijgZPzw+3Zlu5yQgKGP2/hcQbHb7S9Y=
|
||||||
github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94=
|
github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94=
|
||||||
|
@ -158,6 +197,13 @@ github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
|
||||||
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||||
|
github.com/pterm/pterm v0.12.27/go.mod h1:PhQ89w4i95rhgE+xedAoqous6K9X+r6aSOI2eFF7DZI=
|
||||||
|
github.com/pterm/pterm v0.12.29/go.mod h1:WI3qxgvoQFFGKGjGnJR849gU0TsEOvKn5Q8LlY1U7lg=
|
||||||
|
github.com/pterm/pterm v0.12.30/go.mod h1:MOqLIyMOgmTDz9yorcYbcw+HsgoZo3BQfg2wtl3HEFE=
|
||||||
|
github.com/pterm/pterm v0.12.31/go.mod h1:32ZAWZVXD7ZfG0s8qqHXePte42kdz8ECtRyEejaWgXU=
|
||||||
|
github.com/pterm/pterm v0.12.33/go.mod h1:x+h2uL+n7CP/rel9+bImHD5lF3nM9vJj80k9ybiiTTE=
|
||||||
|
github.com/pterm/pterm v0.12.36/go.mod h1:NjiL09hFhT/vWjQHSj1athJpx6H8cjpHXNAK5bUw8T8=
|
||||||
|
github.com/pterm/pterm v0.12.40/go.mod h1:ffwPLwlbXxP+rxT0GsgDTzS3y3rmpAO1NMjUkGTYf8s=
|
||||||
github.com/rivo/uniseg v0.1.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
|
github.com/rivo/uniseg v0.1.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
|
||||||
github.com/rivo/uniseg v0.2.0 h1:S1pD9weZBuJdFmowNwbpi7BJ8TNftyUImj/0WQi72jY=
|
github.com/rivo/uniseg v0.2.0 h1:S1pD9weZBuJdFmowNwbpi7BJ8TNftyUImj/0WQi72jY=
|
||||||
github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
|
github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
|
||||||
|
@ -165,12 +211,34 @@ github.com/rivo/uniseg v0.4.2 h1:YwD0ulJSJytLpiaWua0sBDusfsCZohxjxzVTYjwxfV8=
|
||||||
github.com/rivo/uniseg v0.4.2/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88=
|
github.com/rivo/uniseg v0.4.2/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88=
|
||||||
github.com/sahilm/fuzzy v0.1.0 h1:FzWGaw2Opqyu+794ZQ9SYifWv2EIXpwP4q8dY1kDAwI=
|
github.com/sahilm/fuzzy v0.1.0 h1:FzWGaw2Opqyu+794ZQ9SYifWv2EIXpwP4q8dY1kDAwI=
|
||||||
github.com/sahilm/fuzzy v0.1.0/go.mod h1:VFvziUEIMCrT6A6tw2RFIXPXXmzXbOsSHF0DOI8ZK9Y=
|
github.com/sahilm/fuzzy v0.1.0/go.mod h1:VFvziUEIMCrT6A6tw2RFIXPXXmzXbOsSHF0DOI8ZK9Y=
|
||||||
|
github.com/sergi/go-diff v1.2.0/go.mod h1:STckp+ISIX8hZLjrqAeVduY0gWCT9IjLuqbuNXdaHfM=
|
||||||
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||||
|
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
|
||||||
|
github.com/stretchr/objx v0.5.0 h1:1zr/of2m5FGMsad5YfcqgdqdWrIhu+EBEJRhR1U7z/c=
|
||||||
|
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
|
||||||
|
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
|
||||||
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
|
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
|
||||||
|
github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA=
|
||||||
|
github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||||
|
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||||
github.com/stretchr/testify v1.7.1 h1:5TQK59W5E3v0r2duFAb7P95B6hEeOyEnHRa8MjYSMTY=
|
github.com/stretchr/testify v1.7.1 h1:5TQK59W5E3v0r2duFAb7P95B6hEeOyEnHRa8MjYSMTY=
|
||||||
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||||
|
github.com/stretchr/testify v1.8.0 h1:pSgiaMZlXftHpm5L7V1+rVB+AZJydKsMxsQBIJw4PKk=
|
||||||
|
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/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=
|
||||||
|
github.com/tidwall/match v1.1.1/go.mod h1:eRSPERbgtNPcGhD8UCthc6PmLEQXEWd3PRB5JTxsfmM=
|
||||||
|
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/vmihailenco/msgpack v4.0.4+incompatible h1:dSLoQfGFAo3F6OoNhwUmLwVgaUXK79GlxNBwueZn0xI=
|
github.com/vmihailenco/msgpack v4.0.4+incompatible h1:dSLoQfGFAo3F6OoNhwUmLwVgaUXK79GlxNBwueZn0xI=
|
||||||
github.com/vmihailenco/msgpack v4.0.4+incompatible/go.mod h1:fy3FlTQTDXWkZ7Bh6AcGMlsjHatGryHQYUTf1ShIgkk=
|
github.com/vmihailenco/msgpack v4.0.4+incompatible/go.mod h1:fy3FlTQTDXWkZ7Bh6AcGMlsjHatGryHQYUTf1ShIgkk=
|
||||||
|
github.com/wI2L/jsondiff v0.3.0 h1:iTzQ9u/d86GE9RsBzVHX88f2EA1vQUboHwLhSQFc1s4=
|
||||||
|
github.com/wI2L/jsondiff v0.3.0/go.mod h1:y1IMzNNjlSsk3IUoJdRJO7VRBtzMvRgyo4Vu0LdHpTc=
|
||||||
|
github.com/xo/terminfo v0.0.0-20210125001918-ca9a967f8778/go.mod h1:2MuV+tbUrU1zIOPMxZ5EncGwgmMJsa+9ucAQZXxsObs=
|
||||||
github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k=
|
github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k=
|
||||||
go.etcd.io/bbolt v1.3.6 h1:/ecaJf0sk1l4l6V4awd65v2C3ILy7MSj+s/x1ADCIMU=
|
go.etcd.io/bbolt v1.3.6 h1:/ecaJf0sk1l4l6V4awd65v2C3ILy7MSj+s/x1ADCIMU=
|
||||||
go.etcd.io/bbolt v1.3.6/go.mod h1:qXsaaIqmgQH0T+OPdb99Bf+PKfBBQVAdyD6TY9G8XM4=
|
go.etcd.io/bbolt v1.3.6/go.mod h1:qXsaaIqmgQH0T+OPdb99Bf+PKfBBQVAdyD6TY9G8XM4=
|
||||||
|
@ -179,8 +247,12 @@ golang.design/x/clipboard v0.6.2/go.mod h1:kqBSweBP0/im4SZGGjLrppH0D400Hnfo5WbFK
|
||||||
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
||||||
golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
||||||
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
||||||
|
golang.org/x/crypto v0.0.0-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/exp v0.0.0-20190731235908-ec7cb31e5a56 h1:estk1glOnSVeJ9tdEZZc5mAMDZk5lNJNyJ6DvrBkTEU=
|
golang.org/x/exp v0.0.0-20190731235908-ec7cb31e5a56 h1:estk1glOnSVeJ9tdEZZc5mAMDZk5lNJNyJ6DvrBkTEU=
|
||||||
golang.org/x/exp v0.0.0-20190731235908-ec7cb31e5a56/go.mod h1:JhuoJpWY28nO4Vef9tZUw9qufEGTyX1+7lmHxV5q5G4=
|
golang.org/x/exp v0.0.0-20190731235908-ec7cb31e5a56/go.mod h1:JhuoJpWY28nO4Vef9tZUw9qufEGTyX1+7lmHxV5q5G4=
|
||||||
|
golang.org/x/exp v0.0.0-20230108222341-4b8118a2686a h1:tlXy25amD5A7gOfbXdqCGN5k8ESEed/Ee1E5RcrYnqU=
|
||||||
|
golang.org/x/exp v0.0.0-20230108222341-4b8118a2686a/go.mod h1:CxIveKay+FTh1D0yPZemJVgC/95VzuuOLq5Qi4xnoYc=
|
||||||
golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js=
|
golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js=
|
||||||
golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0=
|
golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0=
|
||||||
golang.org/x/image v0.0.0-20211028202545-6944b10bf410 h1:hTftEOvwiOq2+O8k2D5/Q7COC7k5Qcrgc2TFURJYnvQ=
|
golang.org/x/image v0.0.0-20211028202545-6944b10bf410 h1:hTftEOvwiOq2+O8k2D5/Q7COC7k5Qcrgc2TFURJYnvQ=
|
||||||
|
@ -209,14 +281,21 @@ golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7w
|
||||||
golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
|
golang.org/x/sys v0.0.0-20210927094055-39ccf1dd6fa6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
|
golang.org/x/sys v0.0.0-20211013075003-97ac67df715c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
golang.org/x/sys v0.0.0-20220204135822-1c1b9b1eba6a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
golang.org/x/sys v0.0.0-20220204135822-1c1b9b1eba6a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
golang.org/x/sys v0.0.0-20220209214540-3681064d5158/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
golang.org/x/sys v0.0.0-20220209214540-3681064d5158/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
|
golang.org/x/sys v0.0.0-20220319134239-a9b59b0215f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a h1:dGzPydgVsqGcTRVwiLJ1jVbufYwmzD3LfVPLKsKg+0k=
|
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a h1:dGzPydgVsqGcTRVwiLJ1jVbufYwmzD3LfVPLKsKg+0k=
|
||||||
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/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-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
golang.org/x/sys v0.0.0-20220928140112-f11e5e49a4ec h1:BkDtF2Ih9xZ7le9ndzTA7KJow28VbQW3odyk/8drmuI=
|
golang.org/x/sys v0.0.0-20220928140112-f11e5e49a4ec h1:BkDtF2Ih9xZ7le9ndzTA7KJow28VbQW3odyk/8drmuI=
|
||||||
golang.org/x/sys v0.0.0-20220928140112-f11e5e49a4ec/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
golang.org/x/sys v0.0.0-20220928140112-f11e5e49a4ec/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/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
||||||
|
golang.org/x/term v0.0.0-20210220032956-6a3ed077a48d/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
||||||
|
golang.org/x/term v0.0.0-20210615171337-6886f2dfbf5b/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
|
||||||
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
|
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
|
||||||
golang.org/x/term v0.0.0-20220526004731-065cf7ba2467 h1:CBpWXWQpIRjzmkkA+M7q9Fqnwd2mZr3AFqexg8YTfoM=
|
golang.org/x/term v0.0.0-20220526004731-065cf7ba2467 h1:CBpWXWQpIRjzmkkA+M7q9Fqnwd2mZr3AFqexg8YTfoM=
|
||||||
golang.org/x/term v0.0.0-20220526004731-065cf7ba2467/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
|
golang.org/x/term v0.0.0-20220526004731-065cf7ba2467/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
|
||||||
|
@ -228,6 +307,8 @@ golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||||
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||||
golang.org/x/text v0.3.7 h1:olpwvP2KacW1ZWvsR7uQhoyTYvKAupfQrRGBFM352Gk=
|
golang.org/x/text v0.3.7 h1:olpwvP2KacW1ZWvsR7uQhoyTYvKAupfQrRGBFM352Gk=
|
||||||
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
|
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
|
||||||
|
golang.org/x/text v0.3.8 h1:nAL+RVCQ9uMn3vJZbV+MRnydTJFPf8qqY42YiA6MrqY=
|
||||||
|
golang.org/x/text v0.3.8/go.mod h1:E6s5w1FMmriuDzIBO73fBruAKo1PCIq6d2Q6DHfQ8WQ=
|
||||||
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||||
golang.org/x/tools v0.0.0-20190312151545-0bb0c0a6e846/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
|
golang.org/x/tools v0.0.0-20190312151545-0bb0c0a6e846/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
|
||||||
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||||
|
@ -242,10 +323,15 @@ google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp0
|
||||||
google.golang.org/protobuf v1.26.0 h1:bxAC2xTBsZGibn2RTntX0oH50xLsqy1OxA9tTL3p/lk=
|
google.golang.org/protobuf v1.26.0 h1:bxAC2xTBsZGibn2RTntX0oH50xLsqy1OxA9tTL3p/lk=
|
||||||
google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc=
|
google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc=
|
||||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||||
|
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||||
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
|
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
|
||||||
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
|
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
|
||||||
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||||
|
gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||||
gopkg.in/yaml.v2 v2.2.8 h1:obN1ZagJSUGI0Ek/LBmuj4SNLPfIny3KsKFopxRdj10=
|
gopkg.in/yaml.v2 v2.2.8 h1:obN1ZagJSUGI0Ek/LBmuj4SNLPfIny3KsKFopxRdj10=
|
||||||
gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||||
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c h1:dUUwHk2QECo/6vqA44rthZ8ie2QXMNeKRTHCNY2nXvo=
|
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c h1:dUUwHk2QECo/6vqA44rthZ8ie2QXMNeKRTHCNY2nXvo=
|
||||||
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||||
|
gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||||
|
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||||
|
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||||
|
|
|
@ -16,11 +16,13 @@ import (
|
||||||
|
|
||||||
type CommandController struct {
|
type CommandController struct {
|
||||||
commandList *CommandList
|
commandList *CommandList
|
||||||
|
lookupExtensions []CommandLookupExtension
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewCommandController() *CommandController {
|
func NewCommandController() *CommandController {
|
||||||
return &CommandController{
|
return &CommandController{
|
||||||
commandList: nil,
|
commandList: nil,
|
||||||
|
lookupExtensions: nil,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -29,6 +31,10 @@ func (c *CommandController) AddCommands(ctx *CommandList) {
|
||||||
c.commandList = ctx
|
c.commandList = ctx
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (c *CommandController) AddCommandLookupExtension(ext CommandLookupExtension) {
|
||||||
|
c.lookupExtensions = append(c.lookupExtensions, ext)
|
||||||
|
}
|
||||||
|
|
||||||
func (c *CommandController) Prompt() tea.Msg {
|
func (c *CommandController) Prompt() tea.Msg {
|
||||||
return events.PromptForInputMsg{
|
return events.PromptForInputMsg{
|
||||||
Prompt: ":",
|
Prompt: ":",
|
||||||
|
@ -80,6 +86,12 @@ func (c *CommandController) lookupCommand(name string) Command {
|
||||||
return cmd
|
return cmd
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
for _, exts := range c.lookupExtensions {
|
||||||
|
if cmd := exts.LookupCommand(name); cmd != nil {
|
||||||
|
return cmd
|
||||||
|
}
|
||||||
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -15,3 +15,7 @@ type CommandList struct {
|
||||||
|
|
||||||
parent *CommandList
|
parent *CommandList
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type CommandLookupExtension interface {
|
||||||
|
LookupCommand(name string) Command
|
||||||
|
}
|
||||||
|
|
|
@ -22,4 +22,5 @@ type ModeMessage string
|
||||||
type PromptForInputMsg struct {
|
type PromptForInputMsg struct {
|
||||||
Prompt string
|
Prompt string
|
||||||
OnDone func(value string) tea.Msg
|
OnDone func(value string) tea.Msg
|
||||||
|
OnCancel func() tea.Msg
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,96 +0,0 @@
|
||||||
package controllers
|
|
||||||
|
|
||||||
import (
|
|
||||||
"github.com/aws/aws-sdk-go-v2/service/dynamodb/types"
|
|
||||||
"github.com/lmika/audax/internal/dynamo-browse/models"
|
|
||||||
"github.com/pkg/errors"
|
|
||||||
"strings"
|
|
||||||
)
|
|
||||||
|
|
||||||
type attrPath []string
|
|
||||||
|
|
||||||
func newAttrPath(expr string) attrPath {
|
|
||||||
return strings.Split(expr, ".")
|
|
||||||
}
|
|
||||||
|
|
||||||
func (ap attrPath) follow(item models.Item) (types.AttributeValue, error) {
|
|
||||||
var step types.AttributeValue
|
|
||||||
for i, seg := range ap {
|
|
||||||
if i == 0 {
|
|
||||||
step = item[seg]
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
switch s := step.(type) {
|
|
||||||
case *types.AttributeValueMemberM:
|
|
||||||
step = s.Value[seg]
|
|
||||||
default:
|
|
||||||
return nil, errors.Errorf("seg %v expected to be a map", i)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return step, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (ap attrPath) deleteAt(item models.Item) error {
|
|
||||||
if len(ap) == 1 {
|
|
||||||
delete(item, ap[0])
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
var step types.AttributeValue
|
|
||||||
for i, seg := range ap[:len(ap)-1] {
|
|
||||||
if i == 0 {
|
|
||||||
step = item[seg]
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
switch s := step.(type) {
|
|
||||||
case *types.AttributeValueMemberM:
|
|
||||||
step = s.Value[seg]
|
|
||||||
default:
|
|
||||||
return errors.Errorf("seg %v expected to be a map", i)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
lastSeg := ap[len(ap)-1]
|
|
||||||
switch s := step.(type) {
|
|
||||||
case *types.AttributeValueMemberM:
|
|
||||||
delete(s.Value, lastSeg)
|
|
||||||
default:
|
|
||||||
return errors.Errorf("last seg expected to be a map, but was %T", lastSeg)
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (ap attrPath) setAt(item models.Item, newValue types.AttributeValue) error {
|
|
||||||
if len(ap) == 1 {
|
|
||||||
item[ap[0]] = newValue
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
var step types.AttributeValue
|
|
||||||
for i, seg := range ap[:len(ap)-1] {
|
|
||||||
if i == 0 {
|
|
||||||
step = item[seg]
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
switch s := step.(type) {
|
|
||||||
case *types.AttributeValueMemberM:
|
|
||||||
step = s.Value[seg]
|
|
||||||
default:
|
|
||||||
return errors.Errorf("seg %v expected to be a map", i)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
lastSeg := ap[len(ap)-1]
|
|
||||||
switch s := step.(type) {
|
|
||||||
case *types.AttributeValueMemberM:
|
|
||||||
s.Value[lastSeg] = newValue
|
|
||||||
default:
|
|
||||||
return errors.Errorf("last seg expected to be a map, but was %T", lastSeg)
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
|
@ -3,6 +3,7 @@ package controllers
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"github.com/lmika/audax/internal/dynamo-browse/models"
|
"github.com/lmika/audax/internal/dynamo-browse/models"
|
||||||
|
"io/fs"
|
||||||
)
|
)
|
||||||
|
|
||||||
type TableReadService interface {
|
type TableReadService interface {
|
||||||
|
@ -18,4 +19,6 @@ type SettingsProvider interface {
|
||||||
SetReadOnly(ro bool) error
|
SetReadOnly(ro bool) error
|
||||||
DefaultLimit() (limit int)
|
DefaultLimit() (limit int)
|
||||||
SetDefaultLimit(limit int) error
|
SetDefaultLimit(limit int) error
|
||||||
|
ScriptLookupFS() ([]fs.FS, error)
|
||||||
|
SetScriptLookupPaths(value string) error
|
||||||
}
|
}
|
||||||
|
|
194
internal/dynamo-browse/controllers/scripts.go
Normal file
194
internal/dynamo-browse/controllers/scripts.go
Normal file
|
@ -0,0 +1,194 @@
|
||||||
|
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"
|
||||||
|
)
|
||||||
|
|
||||||
|
type ScriptController struct {
|
||||||
|
scriptManager *scriptmanager.Service
|
||||||
|
tableReadController *TableReadController
|
||||||
|
settingsController *SettingsController
|
||||||
|
eventBus *bus.Bus
|
||||||
|
sendMsg func(msg tea.Msg)
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewScriptController(
|
||||||
|
scriptManager *scriptmanager.Service,
|
||||||
|
tableReadController *TableReadController,
|
||||||
|
settingsController *SettingsController,
|
||||||
|
eventBus *bus.Bus,
|
||||||
|
) *ScriptController {
|
||||||
|
sc := &ScriptController{
|
||||||
|
scriptManager: scriptManager,
|
||||||
|
tableReadController: tableReadController,
|
||||||
|
settingsController: settingsController,
|
||||||
|
eventBus: eventBus,
|
||||||
|
}
|
||||||
|
|
||||||
|
sessionImpl := &sessionImpl{sc: sc, lastSelectedItemIndex: -1}
|
||||||
|
scriptManager.SetIFaces(scriptmanager.Ifaces{
|
||||||
|
UI: &uiImpl{sc: sc},
|
||||||
|
Session: sessionImpl,
|
||||||
|
})
|
||||||
|
|
||||||
|
sessionImpl.subscribeToEvents(eventBus)
|
||||||
|
|
||||||
|
// Setup event handling when settings have changed
|
||||||
|
eventBus.On(BusEventSettingsUpdated, func(name, value string) {
|
||||||
|
if !strings.HasPrefix(name, "script.") {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
sc.Init()
|
||||||
|
})
|
||||||
|
|
||||||
|
return sc
|
||||||
|
}
|
||||||
|
|
||||||
|
func (sc *ScriptController) Init() {
|
||||||
|
if lookupPaths, err := sc.settingsController.settings.ScriptLookupFS(); err == nil {
|
||||||
|
sc.scriptManager.SetLookupPaths(lookupPaths)
|
||||||
|
} else {
|
||||||
|
log.Printf("warn: script lookup paths are invalid: %v", err)
|
||||||
|
}
|
||||||
|
sc.scriptManager.SetDefaultOptions(scriptmanager.Options{
|
||||||
|
OSExecShell: "/bin/bash",
|
||||||
|
Permissions: scriptmanager.Permissions{
|
||||||
|
AllowShellCommands: true,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func (sc *ScriptController) SetMessageSender(sendMsg func(msg tea.Msg)) {
|
||||||
|
sc.sendMsg = sendMsg
|
||||||
|
}
|
||||||
|
|
||||||
|
func (sc *ScriptController) LoadScript(filename string) tea.Msg {
|
||||||
|
ctx := context.Background()
|
||||||
|
plugin, err := sc.scriptManager.LoadScript(ctx, filename)
|
||||||
|
if err != nil {
|
||||||
|
return events.Error(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return events.StatusMsg(fmt.Sprintf("Script '%v' loaded", plugin.Name()))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (sc *ScriptController) RunScript(filename string) tea.Msg {
|
||||||
|
ctx := context.Background()
|
||||||
|
if err := sc.scriptManager.StartAdHocScript(ctx, filename, sc.waitAndPrintScriptError()); err != nil {
|
||||||
|
return events.Error(err)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (sc *ScriptController) waitAndPrintScriptError() chan error {
|
||||||
|
errChan := make(chan error)
|
||||||
|
go func() {
|
||||||
|
if err := <-errChan; err != nil {
|
||||||
|
sc.sendMsg(events.Error(err))
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
return errChan
|
||||||
|
}
|
||||||
|
|
||||||
|
func (sc *ScriptController) LookupCommand(name string) commandctrl.Command {
|
||||||
|
cmd := sc.scriptManager.LookupCommand(name)
|
||||||
|
if cmd == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return func(execCtx commandctrl.ExecContext, args []string) tea.Msg {
|
||||||
|
errChan := sc.waitAndPrintScriptError()
|
||||||
|
ctx := context.Background()
|
||||||
|
|
||||||
|
if err := cmd.Invoke(ctx, args, errChan); err != nil {
|
||||||
|
return events.Error(err)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
type uiImpl struct {
|
||||||
|
sc *ScriptController
|
||||||
|
}
|
||||||
|
|
||||||
|
func (u uiImpl) PrintMessage(ctx context.Context, msg string) {
|
||||||
|
u.sc.sendMsg(events.StatusMsg(msg))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (u uiImpl) Prompt(ctx context.Context, msg string) chan string {
|
||||||
|
resultChan := make(chan string)
|
||||||
|
u.sc.sendMsg(events.PromptForInputMsg{
|
||||||
|
Prompt: msg,
|
||||||
|
OnDone: func(value string) tea.Msg {
|
||||||
|
resultChan <- value
|
||||||
|
return nil
|
||||||
|
},
|
||||||
|
OnCancel: func() tea.Msg {
|
||||||
|
close(resultChan)
|
||||||
|
return nil
|
||||||
|
},
|
||||||
|
})
|
||||||
|
return resultChan
|
||||||
|
}
|
||||||
|
|
||||||
|
type sessionImpl struct {
|
||||||
|
sc *ScriptController
|
||||||
|
lastSelectedItemIndex int
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *sessionImpl) subscribeToEvents(bus *bus.Bus) {
|
||||||
|
bus.On("ui.new-item-selected", func(rs *models.ResultSet, itemIndex int) {
|
||||||
|
s.lastSelectedItemIndex = itemIndex
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *sessionImpl) SelectedItemIndex(ctx context.Context) int {
|
||||||
|
return s.lastSelectedItemIndex
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *sessionImpl) ResultSet(ctx context.Context) *models.ResultSet {
|
||||||
|
return s.sc.tableReadController.state.ResultSet()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *sessionImpl) SetResultSet(ctx context.Context, newResultSet *models.ResultSet) {
|
||||||
|
state := s.sc.tableReadController.state
|
||||||
|
msg := s.sc.tableReadController.setResultSetAndFilter(newResultSet, state.filter, true, resultSetUpdateScript)
|
||||||
|
s.sc.sendMsg(msg)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *sessionImpl) Query(ctx context.Context, query string, opts scriptmanager.QueryOptions) (*models.ResultSet, error) {
|
||||||
|
currentResultSet := s.sc.tableReadController.state.ResultSet()
|
||||||
|
if currentResultSet == nil {
|
||||||
|
// TODO: this should only be used if there's no current table
|
||||||
|
return nil, errors.New("no table selected")
|
||||||
|
}
|
||||||
|
|
||||||
|
expr, err := queryexpr.Parse(query)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if opts.NamePlaceholders != nil {
|
||||||
|
expr = expr.WithNameParams(opts.NamePlaceholders)
|
||||||
|
}
|
||||||
|
if opts.ValuePlaceholders != nil {
|
||||||
|
expr = expr.WithValueParams(opts.ValuePlaceholders)
|
||||||
|
}
|
||||||
|
|
||||||
|
newResultSet, err := s.sc.tableReadController.tableService.ScanOrQuery(context.Background(), currentResultSet.TableInfo, expr)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return newResultSet, nil
|
||||||
|
}
|
159
internal/dynamo-browse/controllers/scripts_test.go
Normal file
159
internal/dynamo-browse/controllers/scripts_test.go
Normal file
|
@ -0,0 +1,159 @@
|
||||||
|
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"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestScriptController_RunScript(t *testing.T) {
|
||||||
|
t.Run("should execute scripts successfully", func(t *testing.T) {
|
||||||
|
srv := newService(t, serviceConfig{
|
||||||
|
scriptFS: testScriptFile(t, "test.tm", `
|
||||||
|
ui.print("Hello world")
|
||||||
|
`),
|
||||||
|
})
|
||||||
|
|
||||||
|
msg := srv.scriptController.RunScript("test.tm")
|
||||||
|
assert.Nil(t, msg)
|
||||||
|
|
||||||
|
srv.msgSender.waitForAtLeastOneMessages(t, 5*time.Second)
|
||||||
|
|
||||||
|
assert.Len(t, srv.msgSender.msgs, 1)
|
||||||
|
assert.Equal(t, events.StatusMsg("Hello world"), srv.msgSender.msgs[0])
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("session.result_set", func(t *testing.T) {
|
||||||
|
t.Run("should return current result set if not-nil", func(t *testing.T) {
|
||||||
|
srv := newService(t, serviceConfig{
|
||||||
|
tableName: "alpha-table",
|
||||||
|
scriptFS: testScriptFile(t, "test.tm", `
|
||||||
|
rs := session.result_set()
|
||||||
|
ui.print(rs.length)
|
||||||
|
`),
|
||||||
|
})
|
||||||
|
|
||||||
|
invokeCommand(t, srv.readController.Init())
|
||||||
|
|
||||||
|
msg := srv.scriptController.RunScript("test.tm")
|
||||||
|
assert.Nil(t, msg)
|
||||||
|
|
||||||
|
srv.msgSender.waitForAtLeastOneMessages(t, 5*time.Second)
|
||||||
|
|
||||||
|
assert.Len(t, srv.msgSender.msgs, 1)
|
||||||
|
assert.Equal(t, events.StatusMsg("3"), srv.msgSender.msgs[0])
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("session.query", func(t *testing.T) {
|
||||||
|
t.Run("should run query against current table", func(t *testing.T) {
|
||||||
|
srv := newService(t, serviceConfig{
|
||||||
|
tableName: "alpha-table",
|
||||||
|
scriptFS: testScriptFile(t, "test.tm", `
|
||||||
|
rs := session.query('pk="abc"').unwrap()
|
||||||
|
ui.print(rs.length)
|
||||||
|
`),
|
||||||
|
})
|
||||||
|
|
||||||
|
invokeCommand(t, srv.readController.Init())
|
||||||
|
msg := srv.scriptController.RunScript("test.tm")
|
||||||
|
assert.Nil(t, msg)
|
||||||
|
|
||||||
|
srv.msgSender.waitForAtLeastOneMessages(t, 5*time.Second)
|
||||||
|
|
||||||
|
assert.Len(t, srv.msgSender.msgs, 1)
|
||||||
|
assert.Equal(t, events.StatusMsg("2"), srv.msgSender.msgs[0])
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("session.set_result_set", func(t *testing.T) {
|
||||||
|
t.Run("should set the result set from the result of a query", func(t *testing.T) {
|
||||||
|
srv := newService(t, serviceConfig{
|
||||||
|
tableName: "alpha-table",
|
||||||
|
scriptFS: testScriptFile(t, "test.tm", `
|
||||||
|
rs := session.query('pk="abc"').unwrap()
|
||||||
|
session.set_result_set(rs)
|
||||||
|
`),
|
||||||
|
})
|
||||||
|
|
||||||
|
invokeCommand(t, srv.readController.Init())
|
||||||
|
msg := srv.scriptController.RunScript("test.tm")
|
||||||
|
assert.Nil(t, msg)
|
||||||
|
|
||||||
|
srv.msgSender.waitForAtLeastOneMessages(t, 5*time.Second)
|
||||||
|
|
||||||
|
assert.Len(t, srv.msgSender.msgs, 1)
|
||||||
|
assert.IsType(t, controllers.NewResultSet{}, srv.msgSender.msgs[0])
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("changed attributes of the result set should show up as modified", func(t *testing.T) {
|
||||||
|
srv := newService(t, serviceConfig{
|
||||||
|
tableName: "alpha-table",
|
||||||
|
scriptFS: testScriptFile(t, "test.tm", `
|
||||||
|
rs := session.query('pk="abc"').unwrap()
|
||||||
|
rs[0].set_attr("pk", "131")
|
||||||
|
session.set_result_set(rs)
|
||||||
|
`),
|
||||||
|
})
|
||||||
|
|
||||||
|
invokeCommand(t, srv.readController.Init())
|
||||||
|
msg := srv.scriptController.RunScript("test.tm")
|
||||||
|
assert.Nil(t, msg)
|
||||||
|
|
||||||
|
srv.msgSender.waitForAtLeastOneMessages(t, 5*time.Second)
|
||||||
|
|
||||||
|
assert.Len(t, srv.msgSender.msgs, 1)
|
||||||
|
assert.IsType(t, controllers.NewResultSet{}, srv.msgSender.msgs[0])
|
||||||
|
|
||||||
|
assert.Equal(t, "131", srv.state.ResultSet().Items()[0]["pk"].(*types.AttributeValueMemberS).Value)
|
||||||
|
assert.True(t, srv.state.ResultSet().IsDirty(0))
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
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)
|
||||||
|
})
|
||||||
|
`),
|
||||||
|
})
|
||||||
|
|
||||||
|
invokeCommand(t, srv.scriptController.LoadScript("test.tm"))
|
||||||
|
invokeCommand(t, srv.commandController.Execute(`mycommand "test name"`))
|
||||||
|
|
||||||
|
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])
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("should only allow one script to run at a time", func(t *testing.T) {
|
||||||
|
srv := newService(t, serviceConfig{
|
||||||
|
tableName: "alpha-table",
|
||||||
|
scriptFS: testScriptFile(t, "test.tm", `
|
||||||
|
ext.command("mycommand", func() {
|
||||||
|
time.sleep(1.5)
|
||||||
|
ui.print("Done my thing")
|
||||||
|
})
|
||||||
|
`),
|
||||||
|
})
|
||||||
|
|
||||||
|
invokeCommand(t, srv.scriptController.LoadScript("test.tm"))
|
||||||
|
|
||||||
|
invokeCommand(t, srv.commandController.Execute(`mycommand`))
|
||||||
|
invokeCommandExpectingError(t, srv.commandController.Execute(`mycommand`))
|
||||||
|
|
||||||
|
srv.msgSender.waitForAtLeastOneMessages(t, 5*time.Second)
|
||||||
|
|
||||||
|
assert.Len(t, srv.msgSender.msgs, 1)
|
||||||
|
assert.Equal(t, events.StatusMsg("Done my thing"), srv.msgSender.msgs[0])
|
||||||
|
})
|
||||||
|
|
||||||
|
}
|
|
@ -4,18 +4,25 @@ import (
|
||||||
"fmt"
|
"fmt"
|
||||||
tea "github.com/charmbracelet/bubbletea"
|
tea "github.com/charmbracelet/bubbletea"
|
||||||
"github.com/lmika/audax/internal/common/ui/events"
|
"github.com/lmika/audax/internal/common/ui/events"
|
||||||
|
bus "github.com/lmika/events"
|
||||||
"github.com/pkg/errors"
|
"github.com/pkg/errors"
|
||||||
"log"
|
"log"
|
||||||
"strconv"
|
"strconv"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
BusEventSettingsUpdated = "settings.updated"
|
||||||
|
)
|
||||||
|
|
||||||
type SettingsController struct {
|
type SettingsController struct {
|
||||||
settings SettingsProvider
|
settings SettingsProvider
|
||||||
|
bus *bus.Bus
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewSettingsController(sp SettingsProvider) *SettingsController {
|
func NewSettingsController(sp SettingsProvider, bus *bus.Bus) *SettingsController {
|
||||||
return &SettingsController{
|
return &SettingsController{
|
||||||
settings: sp,
|
settings: sp,
|
||||||
|
bus: bus,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -40,7 +47,7 @@ func (sc *SettingsController) SetSetting(name string, value string) tea.Msg {
|
||||||
case "default-limit":
|
case "default-limit":
|
||||||
newLimit, err := strconv.Atoi(value)
|
newLimit, err := strconv.Atoi(value)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return errors.Wrapf(err, "bad value: %v", value)
|
return events.Error(errors.Wrapf(err, "bad value: %v", value))
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := sc.settings.SetDefaultLimit(newLimit); err != nil {
|
if err := sc.settings.SetDefaultLimit(newLimit); err != nil {
|
||||||
|
@ -50,6 +57,12 @@ func (sc *SettingsController) SetSetting(name string, value string) tea.Msg {
|
||||||
Message: events.StatusMsg(fmt.Sprintf("Default query limit now %v", newLimit)),
|
Message: events.StatusMsg(fmt.Sprintf("Default query limit now %v", newLimit)),
|
||||||
Next: SettingsUpdated{},
|
Next: SettingsUpdated{},
|
||||||
}
|
}
|
||||||
|
case "script.lookup-path":
|
||||||
|
if err := sc.settings.SetScriptLookupPaths(value); err != nil {
|
||||||
|
return events.Error(err)
|
||||||
|
}
|
||||||
|
sc.bus.Fire(BusEventSettingsUpdated, name, value)
|
||||||
|
return SettingsUpdated{}
|
||||||
}
|
}
|
||||||
|
|
||||||
return events.Error(errors.Errorf("unrecognised setting: %v", name))
|
return events.Error(errors.Errorf("unrecognised setting: %v", name))
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
package controllers
|
package controllers
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"bytes"
|
||||||
"context"
|
"context"
|
||||||
"fmt"
|
"fmt"
|
||||||
tea "github.com/charmbracelet/bubbletea"
|
tea "github.com/charmbracelet/bubbletea"
|
||||||
|
@ -27,6 +28,7 @@ const (
|
||||||
resultSetUpdateSnapshotRestore
|
resultSetUpdateSnapshotRestore
|
||||||
resultSetUpdateRescan
|
resultSetUpdateRescan
|
||||||
resultSetUpdateTouch
|
resultSetUpdateTouch
|
||||||
|
resultSetUpdateScript
|
||||||
)
|
)
|
||||||
|
|
||||||
type MarkOp int
|
type MarkOp int
|
||||||
|
@ -138,13 +140,22 @@ func (c *TableReadController) PromptForQuery() tea.Msg {
|
||||||
return events.StatusMsg("Result-set is nil")
|
return events.StatusMsg("Result-set is nil")
|
||||||
}
|
}
|
||||||
|
|
||||||
return c.runQuery(resultSet.TableInfo, value, "", true)
|
var q *queryexpr.QueryExpr
|
||||||
|
if value != "" {
|
||||||
|
var err error
|
||||||
|
q, err = queryexpr.Parse(value)
|
||||||
|
if err != nil {
|
||||||
|
return events.Error(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return c.runQuery(resultSet.TableInfo, q, "", true)
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *TableReadController) runQuery(tableInfo *models.TableInfo, query, newFilter string, pushSnapshot bool) tea.Msg {
|
func (c *TableReadController) runQuery(tableInfo *models.TableInfo, query *queryexpr.QueryExpr, newFilter string, pushSnapshot bool) tea.Msg {
|
||||||
if query == "" {
|
if query == nil {
|
||||||
return NewJob(c.jobController, "Scanning…", func(ctx context.Context) (*models.ResultSet, error) {
|
return NewJob(c.jobController, "Scanning…", func(ctx context.Context) (*models.ResultSet, error) {
|
||||||
newResultSet, err := c.tableService.ScanOrQuery(context.Background(), tableInfo, nil)
|
newResultSet, err := c.tableService.ScanOrQuery(context.Background(), tableInfo, nil)
|
||||||
|
|
||||||
|
@ -156,14 +167,9 @@ func (c *TableReadController) runQuery(tableInfo *models.TableInfo, query, newFi
|
||||||
}).OnEither(c.handleResultSetFromJobResult(newFilter, pushSnapshot, resultSetUpdateQuery)).Submit()
|
}).OnEither(c.handleResultSetFromJobResult(newFilter, pushSnapshot, resultSetUpdateQuery)).Submit()
|
||||||
}
|
}
|
||||||
|
|
||||||
expr, err := queryexpr.Parse(query)
|
|
||||||
if err != nil {
|
|
||||||
return events.Error(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
return c.doIfNoneDirty(func() tea.Msg {
|
return c.doIfNoneDirty(func() tea.Msg {
|
||||||
return NewJob(c.jobController, "Running query…", func(ctx context.Context) (*models.ResultSet, error) {
|
return NewJob(c.jobController, "Running query…", func(ctx context.Context) (*models.ResultSet, error) {
|
||||||
newResultSet, err := c.tableService.ScanOrQuery(context.Background(), tableInfo, expr)
|
newResultSet, err := c.tableService.ScanOrQuery(context.Background(), tableInfo, query)
|
||||||
|
|
||||||
if newFilter != "" && newResultSet != nil {
|
if newFilter != "" && newResultSet != nil {
|
||||||
newResultSet = c.tableService.Filter(newResultSet, newFilter)
|
newResultSet = c.tableService.Filter(newResultSet, newFilter)
|
||||||
|
@ -219,10 +225,17 @@ func (c *TableReadController) setResultSetAndFilter(resultSet *models.ResultSet,
|
||||||
TableName: resultSet.TableInfo.Name,
|
TableName: resultSet.TableInfo.Name,
|
||||||
Filter: filter,
|
Filter: filter,
|
||||||
}
|
}
|
||||||
|
|
||||||
if q := resultSet.Query; q != nil {
|
if q := resultSet.Query; q != nil {
|
||||||
details.Query = q.String()
|
if bs, err := q.SerializeToBytes(); err == nil {
|
||||||
|
details.Query = bs
|
||||||
|
details.QueryHash = q.HashCode()
|
||||||
|
} else {
|
||||||
|
log.Printf("cannot serialize query to bytes: %v", err)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
log.Printf("pushing to backstack: table = %v, filter = %v, query_hash = %v", details.TableName, details.Filter, details.QueryHash)
|
||||||
if err := c.workspaceService.PushSnapshot(details); err != nil {
|
if err := c.workspaceService.PushSnapshot(details); err != nil {
|
||||||
log.Printf("cannot push snapshot: %v", err)
|
log.Printf("cannot push snapshot: %v", err)
|
||||||
}
|
}
|
||||||
|
@ -307,6 +320,8 @@ func (c *TableReadController) ViewBack() tea.Msg {
|
||||||
return events.StatusMsg("Backstack is empty")
|
return events.StatusMsg("Backstack is empty")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
log.Printf("view back: table = %v, filter = %v, query_hash = %v",
|
||||||
|
viewSnapshot.Details.TableName, viewSnapshot.Details.Filter, viewSnapshot.Details.QueryHash)
|
||||||
return c.updateViewToSnapshot(viewSnapshot)
|
return c.updateViewToSnapshot(viewSnapshot)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -325,6 +340,14 @@ func (c *TableReadController) updateViewToSnapshot(viewSnapshot *serialisable.Vi
|
||||||
var err error
|
var err error
|
||||||
currentResultSet := c.state.ResultSet()
|
currentResultSet := c.state.ResultSet()
|
||||||
|
|
||||||
|
var query *queryexpr.QueryExpr
|
||||||
|
if len(viewSnapshot.Details.Query) > 0 {
|
||||||
|
query, err = queryexpr.DeserializeFrom(bytes.NewReader(viewSnapshot.Details.Query))
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if currentResultSet == nil {
|
if currentResultSet == nil {
|
||||||
return NewJob(c.jobController, "Fetching table info…", func(ctx context.Context) (*models.TableInfo, error) {
|
return NewJob(c.jobController, "Fetching table info…", func(ctx context.Context) (*models.TableInfo, error) {
|
||||||
tableInfo, err := c.tableService.Describe(context.Background(), viewSnapshot.Details.TableName)
|
tableInfo, err := c.tableService.Describe(context.Background(), viewSnapshot.Details.TableName)
|
||||||
|
@ -333,16 +356,16 @@ func (c *TableReadController) updateViewToSnapshot(viewSnapshot *serialisable.Vi
|
||||||
}
|
}
|
||||||
return tableInfo, nil
|
return tableInfo, nil
|
||||||
}).OnDone(func(tableInfo *models.TableInfo) tea.Msg {
|
}).OnDone(func(tableInfo *models.TableInfo) tea.Msg {
|
||||||
return c.runQuery(tableInfo, viewSnapshot.Details.Query, viewSnapshot.Details.Filter, false)
|
return c.runQuery(tableInfo, query, viewSnapshot.Details.Filter, false)
|
||||||
}).Submit()
|
}).Submit()
|
||||||
}
|
}
|
||||||
|
|
||||||
var currentQueryExpr string
|
queryEqualsCurrentQuery := false
|
||||||
if currentResultSet.Query != nil {
|
if q, ok := currentResultSet.Query.(*queryexpr.QueryExpr); ok && q != nil {
|
||||||
currentQueryExpr = currentResultSet.Query.String()
|
queryEqualsCurrentQuery = q.Equal(query)
|
||||||
}
|
}
|
||||||
|
|
||||||
if viewSnapshot.Details.TableName == currentResultSet.TableInfo.Name && viewSnapshot.Details.Query == currentQueryExpr {
|
if viewSnapshot.Details.TableName == currentResultSet.TableInfo.Name && queryEqualsCurrentQuery {
|
||||||
return NewJob(c.jobController, "Applying filter…", func(ctx context.Context) (*models.ResultSet, error) {
|
return NewJob(c.jobController, "Applying filter…", func(ctx context.Context) (*models.ResultSet, error) {
|
||||||
return c.tableService.Filter(currentResultSet, viewSnapshot.Details.Filter), nil
|
return c.tableService.Filter(currentResultSet, viewSnapshot.Details.Filter), nil
|
||||||
}).OnEither(c.handleResultSetFromJobResult(viewSnapshot.Details.Filter, false, resultSetUpdateSnapshotRestore)).Submit()
|
}).OnEither(c.handleResultSetFromJobResult(viewSnapshot.Details.Filter, false, resultSetUpdateSnapshotRestore)).Submit()
|
||||||
|
@ -357,7 +380,7 @@ func (c *TableReadController) updateViewToSnapshot(viewSnapshot *serialisable.Vi
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return c.runQuery(tableInfo, viewSnapshot.Details.Query, viewSnapshot.Details.Filter, false), nil
|
return c.runQuery(tableInfo, query, viewSnapshot.Details.Filter, false), nil
|
||||||
}).OnDone(func(m tea.Msg) tea.Msg {
|
}).OnDone(func(m tea.Msg) tea.Msg {
|
||||||
return m
|
return m
|
||||||
}).Submit()
|
}).Submit()
|
||||||
|
|
|
@ -8,8 +8,10 @@ import (
|
||||||
"github.com/lmika/audax/internal/common/sliceutils"
|
"github.com/lmika/audax/internal/common/sliceutils"
|
||||||
"github.com/lmika/audax/internal/common/ui/events"
|
"github.com/lmika/audax/internal/common/ui/events"
|
||||||
"github.com/lmika/audax/internal/dynamo-browse/models"
|
"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/audax/internal/dynamo-browse/services/tables"
|
||||||
"github.com/pkg/errors"
|
"github.com/pkg/errors"
|
||||||
|
"log"
|
||||||
"strconv"
|
"strconv"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -81,48 +83,57 @@ func (twc *TableWriteController) NewItem() tea.Msg {
|
||||||
}
|
}
|
||||||
|
|
||||||
func (twc *TableWriteController) SetAttributeValue(idx int, itemType models.ItemType, key string) tea.Msg {
|
func (twc *TableWriteController) SetAttributeValue(idx int, itemType models.ItemType, key string) tea.Msg {
|
||||||
apPath := newAttrPath(key)
|
path, err := queryexpr.Parse(key)
|
||||||
|
if err != nil {
|
||||||
|
return events.Error(err)
|
||||||
|
}
|
||||||
|
|
||||||
var attrValue types.AttributeValue
|
var attrValue types.AttributeValue
|
||||||
if err := twc.state.withResultSetReturningError(func(set *models.ResultSet) (err error) {
|
if err := twc.state.withResultSetReturningError(func(set *models.ResultSet) (err error) {
|
||||||
attrValue, err = apPath.follow(set.Items()[idx])
|
if !path.IsModifiablePath(set.Items()[idx]) {
|
||||||
|
return errors.Errorf("path cannot be used to set attribute value")
|
||||||
|
}
|
||||||
|
|
||||||
|
attrValue, err = path.EvalItem(set.Items()[idx])
|
||||||
return err
|
return err
|
||||||
}); err != nil {
|
}); err != nil {
|
||||||
return events.Error(err)
|
return events.Error(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
log.Printf("sa attribute value = %v", attrValue)
|
||||||
|
|
||||||
switch itemType {
|
switch itemType {
|
||||||
case models.UnsetItemType:
|
case models.UnsetItemType:
|
||||||
switch attrValue.(type) {
|
switch attrValue.(type) {
|
||||||
case *types.AttributeValueMemberS:
|
case *types.AttributeValueMemberS:
|
||||||
return twc.setStringValue(idx, apPath)
|
return twc.setStringValue(idx, path)
|
||||||
case *types.AttributeValueMemberN:
|
case *types.AttributeValueMemberN:
|
||||||
return twc.setNumberValue(idx, apPath)
|
return twc.setNumberValue(idx, path)
|
||||||
case *types.AttributeValueMemberBOOL:
|
case *types.AttributeValueMemberBOOL:
|
||||||
return twc.setBoolValue(idx, apPath)
|
return twc.setBoolValue(idx, path)
|
||||||
default:
|
default:
|
||||||
return events.Error(errors.New("attribute type for key must be set"))
|
return events.Error(errors.New("attribute type for key must be set"))
|
||||||
}
|
}
|
||||||
case models.StringItemType:
|
case models.StringItemType:
|
||||||
return twc.setStringValue(idx, apPath)
|
return twc.setStringValue(idx, path)
|
||||||
case models.NumberItemType:
|
case models.NumberItemType:
|
||||||
return twc.setNumberValue(idx, apPath)
|
return twc.setNumberValue(idx, path)
|
||||||
case models.BoolItemType:
|
case models.BoolItemType:
|
||||||
return twc.setBoolValue(idx, apPath)
|
return twc.setBoolValue(idx, path)
|
||||||
case models.NullItemType:
|
case models.NullItemType:
|
||||||
return twc.setNullValue(idx, apPath)
|
return twc.setNullValue(idx, path)
|
||||||
default:
|
default:
|
||||||
return events.Error(errors.New("unsupported attribute type"))
|
return events.Error(errors.New("unsupported attribute type"))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (twc *TableWriteController) setStringValue(idx int, attr attrPath) tea.Msg {
|
func (twc *TableWriteController) setStringValue(idx int, attr *queryexpr.QueryExpr) tea.Msg {
|
||||||
return events.PromptForInputMsg{
|
return events.PromptForInputMsg{
|
||||||
Prompt: "string value: ",
|
Prompt: "string value: ",
|
||||||
OnDone: func(value string) tea.Msg {
|
OnDone: func(value string) tea.Msg {
|
||||||
if err := twc.state.withResultSetReturningError(func(set *models.ResultSet) error {
|
if err := twc.state.withResultSetReturningError(func(set *models.ResultSet) error {
|
||||||
if err := applyToMarkedItems(set, idx, func(idx int, item models.Item) error {
|
if err := applyToMarkedItems(set, idx, func(idx int, item models.Item) error {
|
||||||
if err := attr.setAt(item, &types.AttributeValueMemberS{Value: value}); err != nil {
|
if err := attr.SetEvalItem(item, &types.AttributeValueMemberS{Value: value}); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
set.SetDirty(idx, true)
|
set.SetDirty(idx, true)
|
||||||
|
@ -140,13 +151,13 @@ func (twc *TableWriteController) setStringValue(idx int, attr attrPath) tea.Msg
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (twc *TableWriteController) setNumberValue(idx int, attr attrPath) tea.Msg {
|
func (twc *TableWriteController) setNumberValue(idx int, attr *queryexpr.QueryExpr) tea.Msg {
|
||||||
return events.PromptForInputMsg{
|
return events.PromptForInputMsg{
|
||||||
Prompt: "number value: ",
|
Prompt: "number value: ",
|
||||||
OnDone: func(value string) tea.Msg {
|
OnDone: func(value string) tea.Msg {
|
||||||
if err := twc.state.withResultSetReturningError(func(set *models.ResultSet) error {
|
if err := twc.state.withResultSetReturningError(func(set *models.ResultSet) error {
|
||||||
if err := applyToMarkedItems(set, idx, func(idx int, item models.Item) error {
|
if err := applyToMarkedItems(set, idx, func(idx int, item models.Item) error {
|
||||||
if err := attr.setAt(item, &types.AttributeValueMemberN{Value: value}); err != nil {
|
if err := attr.SetEvalItem(item, &types.AttributeValueMemberN{Value: value}); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
set.SetDirty(idx, true)
|
set.SetDirty(idx, true)
|
||||||
|
@ -164,7 +175,7 @@ func (twc *TableWriteController) setNumberValue(idx int, attr attrPath) tea.Msg
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (twc *TableWriteController) setBoolValue(idx int, attr attrPath) tea.Msg {
|
func (twc *TableWriteController) setBoolValue(idx int, attr *queryexpr.QueryExpr) tea.Msg {
|
||||||
return events.PromptForInputMsg{
|
return events.PromptForInputMsg{
|
||||||
Prompt: "bool value: ",
|
Prompt: "bool value: ",
|
||||||
OnDone: func(value string) tea.Msg {
|
OnDone: func(value string) tea.Msg {
|
||||||
|
@ -175,7 +186,7 @@ func (twc *TableWriteController) setBoolValue(idx int, attr attrPath) tea.Msg {
|
||||||
|
|
||||||
if err := twc.state.withResultSetReturningError(func(set *models.ResultSet) error {
|
if err := twc.state.withResultSetReturningError(func(set *models.ResultSet) error {
|
||||||
if err := applyToMarkedItems(set, idx, func(idx int, item models.Item) error {
|
if err := applyToMarkedItems(set, idx, func(idx int, item models.Item) error {
|
||||||
if err := attr.setAt(item, &types.AttributeValueMemberBOOL{Value: b}); err != nil {
|
if err := attr.SetEvalItem(item, &types.AttributeValueMemberBOOL{Value: b}); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
set.SetDirty(idx, true)
|
set.SetDirty(idx, true)
|
||||||
|
@ -193,10 +204,10 @@ func (twc *TableWriteController) setBoolValue(idx int, attr attrPath) tea.Msg {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (twc *TableWriteController) setNullValue(idx int, attr attrPath) tea.Msg {
|
func (twc *TableWriteController) setNullValue(idx int, attr *queryexpr.QueryExpr) tea.Msg {
|
||||||
if err := twc.state.withResultSetReturningError(func(set *models.ResultSet) error {
|
if err := twc.state.withResultSetReturningError(func(set *models.ResultSet) error {
|
||||||
if err := applyToMarkedItems(set, idx, func(idx int, item models.Item) error {
|
if err := applyToMarkedItems(set, idx, func(idx int, item models.Item) error {
|
||||||
if err := attr.setAt(item, &types.AttributeValueMemberNULL{Value: true}); err != nil {
|
if err := attr.SetEvalItem(item, &types.AttributeValueMemberNULL{Value: true}); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
set.SetDirty(idx, true)
|
set.SetDirty(idx, true)
|
||||||
|
@ -213,18 +224,22 @@ func (twc *TableWriteController) setNullValue(idx int, attr attrPath) tea.Msg {
|
||||||
}
|
}
|
||||||
|
|
||||||
func (twc *TableWriteController) DeleteAttribute(idx int, key string) tea.Msg {
|
func (twc *TableWriteController) DeleteAttribute(idx int, key string) tea.Msg {
|
||||||
// Verify that the expression is valid
|
path, err := queryexpr.Parse(key)
|
||||||
apPath := newAttrPath(key)
|
if err != nil {
|
||||||
|
return events.Error(err)
|
||||||
|
}
|
||||||
|
|
||||||
if err := twc.state.withResultSetReturningError(func(set *models.ResultSet) error {
|
if err := twc.state.withResultSetReturningError(func(set *models.ResultSet) error {
|
||||||
_, err := apPath.follow(set.Items()[idx])
|
if !path.IsModifiablePath(set.Items()[idx]) {
|
||||||
return err
|
return errors.Errorf("path cannot be used to set attribute value")
|
||||||
|
}
|
||||||
|
return nil
|
||||||
}); err != nil {
|
}); err != nil {
|
||||||
return events.Error(err)
|
return events.Error(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := twc.state.withResultSetReturningError(func(set *models.ResultSet) error {
|
if err := twc.state.withResultSetReturningError(func(set *models.ResultSet) error {
|
||||||
err := apPath.deleteAt(set.Items()[idx])
|
err := path.DeleteAttribute(set.Items()[idx])
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
|
@ -3,6 +3,8 @@ package controllers_test
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"github.com/aws/aws-sdk-go-v2/service/dynamodb/types"
|
"github.com/aws/aws-sdk-go-v2/service/dynamodb/types"
|
||||||
|
tea "github.com/charmbracelet/bubbletea"
|
||||||
|
"github.com/lmika/audax/internal/common/ui/commandctrl"
|
||||||
"github.com/lmika/audax/internal/dynamo-browse/controllers"
|
"github.com/lmika/audax/internal/dynamo-browse/controllers"
|
||||||
"github.com/lmika/audax/internal/dynamo-browse/models"
|
"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/dynamo"
|
||||||
|
@ -10,13 +12,18 @@ import (
|
||||||
"github.com/lmika/audax/internal/dynamo-browse/providers/workspacestore"
|
"github.com/lmika/audax/internal/dynamo-browse/providers/workspacestore"
|
||||||
"github.com/lmika/audax/internal/dynamo-browse/services/itemrenderer"
|
"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/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/tables"
|
||||||
"github.com/lmika/audax/internal/dynamo-browse/services/viewsnapshot"
|
"github.com/lmika/audax/internal/dynamo-browse/services/viewsnapshot"
|
||||||
"github.com/lmika/audax/test/testdynamo"
|
"github.com/lmika/audax/test/testdynamo"
|
||||||
"github.com/lmika/audax/test/testworkspace"
|
"github.com/lmika/audax/test/testworkspace"
|
||||||
bus "github.com/lmika/events"
|
bus "github.com/lmika/events"
|
||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
|
"io/fs"
|
||||||
|
"sync"
|
||||||
"testing"
|
"testing"
|
||||||
|
"testing/fstest"
|
||||||
|
"time"
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestTableWriteController_NewItem(t *testing.T) {
|
func TestTableWriteController_NewItem(t *testing.T) {
|
||||||
|
@ -569,6 +576,7 @@ func TestTableWriteController_DeleteMarked(t *testing.T) {
|
||||||
}
|
}
|
||||||
|
|
||||||
type services struct {
|
type services struct {
|
||||||
|
msgSender *msgSender
|
||||||
state *controllers.State
|
state *controllers.State
|
||||||
settingProvider controllers.SettingsProvider
|
settingProvider controllers.SettingsProvider
|
||||||
readController *controllers.TableReadController
|
readController *controllers.TableReadController
|
||||||
|
@ -576,11 +584,14 @@ type services struct {
|
||||||
settingsController *controllers.SettingsController
|
settingsController *controllers.SettingsController
|
||||||
columnsController *controllers.ColumnsController
|
columnsController *controllers.ColumnsController
|
||||||
exportController *controllers.ExportController
|
exportController *controllers.ExportController
|
||||||
|
scriptController *controllers.ScriptController
|
||||||
|
commandController *commandctrl.CommandController
|
||||||
}
|
}
|
||||||
|
|
||||||
type serviceConfig struct {
|
type serviceConfig struct {
|
||||||
tableName string
|
tableName string
|
||||||
isReadOnly bool
|
isReadOnly bool
|
||||||
|
scriptFS fs.FS
|
||||||
}
|
}
|
||||||
|
|
||||||
func newService(t *testing.T, cfg serviceConfig) *services {
|
func newService(t *testing.T, cfg serviceConfig) *services {
|
||||||
|
@ -590,6 +601,7 @@ func newService(t *testing.T, cfg serviceConfig) *services {
|
||||||
settingStore := settingstore.New(ws)
|
settingStore := settingstore.New(ws)
|
||||||
workspaceService := viewsnapshot.NewService(resultSetSnapshotStore)
|
workspaceService := viewsnapshot.NewService(resultSetSnapshotStore)
|
||||||
itemRendererService := itemrenderer.NewService(itemrenderer.PlainTextRenderer(), itemrenderer.PlainTextRenderer())
|
itemRendererService := itemrenderer.NewService(itemrenderer.PlainTextRenderer(), itemrenderer.PlainTextRenderer())
|
||||||
|
scriptService := scriptmanager.New()
|
||||||
|
|
||||||
client := testdynamo.SetupTestTable(t, testData)
|
client := testdynamo.SetupTestTable(t, testData)
|
||||||
|
|
||||||
|
@ -601,9 +613,13 @@ func newService(t *testing.T, cfg serviceConfig) *services {
|
||||||
jobsController := controllers.NewJobsController(jobs.NewService(eventBus), eventBus, true)
|
jobsController := controllers.NewJobsController(jobs.NewService(eventBus), eventBus, true)
|
||||||
readController := controllers.NewTableReadController(state, service, workspaceService, itemRendererService, jobsController, eventBus, cfg.tableName)
|
readController := controllers.NewTableReadController(state, service, workspaceService, itemRendererService, jobsController, eventBus, cfg.tableName)
|
||||||
writeController := controllers.NewTableWriteController(state, service, jobsController, readController, settingStore)
|
writeController := controllers.NewTableWriteController(state, service, jobsController, readController, settingStore)
|
||||||
settingsController := controllers.NewSettingsController(settingStore)
|
settingsController := controllers.NewSettingsController(settingStore, eventBus)
|
||||||
columnsController := controllers.NewColumnsController(eventBus)
|
columnsController := controllers.NewColumnsController(eventBus)
|
||||||
exportController := controllers.NewExportController(state, columnsController)
|
exportController := controllers.NewExportController(state, columnsController)
|
||||||
|
scriptController := controllers.NewScriptController(scriptService, readController, settingsController, eventBus)
|
||||||
|
|
||||||
|
commandController := commandctrl.NewCommandController()
|
||||||
|
commandController.AddCommandLookupExtension(scriptController)
|
||||||
|
|
||||||
if cfg.isReadOnly {
|
if cfg.isReadOnly {
|
||||||
if err := settingStore.SetReadOnly(cfg.isReadOnly); err != nil {
|
if err := settingStore.SetReadOnly(cfg.isReadOnly); err != nil {
|
||||||
|
@ -611,6 +627,13 @@ func newService(t *testing.T, cfg serviceConfig) *services {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
msgSender := &msgSender{}
|
||||||
|
scriptController.Init()
|
||||||
|
scriptController.SetMessageSender(msgSender.send)
|
||||||
|
|
||||||
|
// Initting will setup the default script lookup paths, so revert them to the test ones
|
||||||
|
scriptService.SetLookupPaths([]fs.FS{cfg.scriptFS})
|
||||||
|
|
||||||
return &services{
|
return &services{
|
||||||
state: state,
|
state: state,
|
||||||
settingProvider: settingStore,
|
settingProvider: settingStore,
|
||||||
|
@ -619,5 +642,69 @@ func newService(t *testing.T, cfg serviceConfig) *services {
|
||||||
settingsController: settingsController,
|
settingsController: settingsController,
|
||||||
columnsController: columnsController,
|
columnsController: columnsController,
|
||||||
exportController: exportController,
|
exportController: exportController,
|
||||||
|
scriptController: scriptController,
|
||||||
|
commandController: commandController,
|
||||||
|
msgSender: msgSender,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func testScriptFile(t *testing.T, filename, code string) fs.FS {
|
||||||
|
t.Helper()
|
||||||
|
|
||||||
|
testFs := fstest.MapFS{
|
||||||
|
filename: &fstest.MapFile{
|
||||||
|
Data: []byte(code),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
return testFs
|
||||||
|
}
|
||||||
|
|
||||||
|
type msgSender struct {
|
||||||
|
mutex sync.Mutex
|
||||||
|
msgs []tea.Msg
|
||||||
|
waitChan chan struct{}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *msgSender) send(msg tea.Msg) {
|
||||||
|
s.mutex.Lock()
|
||||||
|
defer s.mutex.Unlock()
|
||||||
|
|
||||||
|
s.msgs = append(s.msgs, msg)
|
||||||
|
if s.waitChan != nil {
|
||||||
|
close(s.waitChan)
|
||||||
|
s.waitChan = nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *msgSender) waitForAtLeastOneMessages(t *testing.T, d time.Duration) {
|
||||||
|
t.Helper()
|
||||||
|
|
||||||
|
s.mutex.Lock()
|
||||||
|
msgLen := len(s.msgs)
|
||||||
|
s.mutex.Unlock()
|
||||||
|
|
||||||
|
if msgLen > 0 {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Wait for a message
|
||||||
|
waitChan := s.afterNextMessage()
|
||||||
|
|
||||||
|
select {
|
||||||
|
case <-waitChan:
|
||||||
|
case <-time.After(d):
|
||||||
|
t.Fatalf("timeout waiting for next message")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *msgSender) afterNextMessage() chan struct{} {
|
||||||
|
s.mutex.Lock()
|
||||||
|
defer s.mutex.Unlock()
|
||||||
|
|
||||||
|
if s.waitChan != nil {
|
||||||
|
panic("More than one wait chan")
|
||||||
|
}
|
||||||
|
newWaitChan := make(chan struct{})
|
||||||
|
s.waitChan = newWaitChan
|
||||||
|
return newWaitChan
|
||||||
|
}
|
||||||
|
|
5
internal/dynamo-browse/controllers/uistate.go
Normal file
5
internal/dynamo-browse/controllers/uistate.go
Normal file
|
@ -0,0 +1,5 @@
|
||||||
|
package controllers
|
||||||
|
|
||||||
|
type UIStateProvider interface {
|
||||||
|
SelectedRowIndex() int
|
||||||
|
}
|
111
internal/dynamo-browse/models/attrcodec/codec_test.go
Normal file
111
internal/dynamo-browse/models/attrcodec/codec_test.go
Normal file
|
@ -0,0 +1,111 @@
|
||||||
|
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/stretchr/testify/assert"
|
||||||
|
"strings"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestCodec(t *testing.T) {
|
||||||
|
t.Run("should be able to encode and decode", func(t *testing.T) {
|
||||||
|
scenarios := []struct {
|
||||||
|
name string
|
||||||
|
val types.AttributeValue
|
||||||
|
}{
|
||||||
|
{name: "string", val: &types.AttributeValueMemberS{Value: "Hello world"}},
|
||||||
|
{name: "empty string", val: &types.AttributeValueMemberS{Value: ""}},
|
||||||
|
{name: "large string", val: &types.AttributeValueMemberS{Value: strings.Repeat("DynamoDB", 256)}},
|
||||||
|
|
||||||
|
{name: "number", val: &types.AttributeValueMemberN{Value: "12345"}},
|
||||||
|
{name: "large number", val: &types.AttributeValueMemberN{Value: "123456789012345678901234567890"}},
|
||||||
|
|
||||||
|
{name: "true bool", val: &types.AttributeValueMemberBOOL{Value: true}},
|
||||||
|
{name: "false bool", val: &types.AttributeValueMemberBOOL{Value: false}},
|
||||||
|
|
||||||
|
{name: "true null", val: &types.AttributeValueMemberNULL{Value: true}},
|
||||||
|
{name: "false null", val: &types.AttributeValueMemberNULL{Value: false}},
|
||||||
|
|
||||||
|
{name: "bytes", val: &types.AttributeValueMemberB{Value: []byte{1, 2, 3, 4, 5}}},
|
||||||
|
|
||||||
|
{name: "simple list", val: &types.AttributeValueMemberL{Value: []types.AttributeValue{
|
||||||
|
&types.AttributeValueMemberS{Value: "apple"},
|
||||||
|
&types.AttributeValueMemberS{Value: "banana"},
|
||||||
|
&types.AttributeValueMemberS{Value: "cherry"},
|
||||||
|
}}},
|
||||||
|
{name: "nested lists", val: &types.AttributeValueMemberL{Value: []types.AttributeValue{
|
||||||
|
&types.AttributeValueMemberL{Value: []types.AttributeValue{
|
||||||
|
&types.AttributeValueMemberS{Value: "red apple"},
|
||||||
|
&types.AttributeValueMemberS{Value: "green apple"},
|
||||||
|
}},
|
||||||
|
&types.AttributeValueMemberL{Value: []types.AttributeValue{
|
||||||
|
&types.AttributeValueMemberS{Value: "banana"},
|
||||||
|
&types.AttributeValueMemberS{Value: "banana bread"},
|
||||||
|
&types.AttributeValueMemberS{Value: "banana cake"},
|
||||||
|
}},
|
||||||
|
&types.AttributeValueMemberS{Value: "cherry"},
|
||||||
|
&types.AttributeValueMemberS{Value: "can't make anything with cherries"},
|
||||||
|
}}},
|
||||||
|
|
||||||
|
{name: "simple map", val: &types.AttributeValueMemberM{Value: map[string]types.AttributeValue{
|
||||||
|
"alpha": &types.AttributeValueMemberS{Value: "I am an apple"},
|
||||||
|
"bravo": &types.AttributeValueMemberN{Value: "123.45"},
|
||||||
|
"charlie": &types.AttributeValueMemberS{Value: "things go here"},
|
||||||
|
}}},
|
||||||
|
{name: "nested maps", val: &types.AttributeValueMemberM{Value: map[string]types.AttributeValue{
|
||||||
|
"alpha": &types.AttributeValueMemberL{Value: []types.AttributeValue{
|
||||||
|
&types.AttributeValueMemberS{Value: "red apple"},
|
||||||
|
&types.AttributeValueMemberS{Value: "green apple"},
|
||||||
|
}},
|
||||||
|
"bravo": &types.AttributeValueMemberM{Value: map[string]types.AttributeValue{
|
||||||
|
"good": &types.AttributeValueMemberS{Value: "stuff"},
|
||||||
|
"is": &types.AttributeValueMemberS{Value: "written"},
|
||||||
|
"in": &types.AttributeValueMemberS{Value: "the unit tests"},
|
||||||
|
}},
|
||||||
|
"coords": &types.AttributeValueMemberL{Value: []types.AttributeValue{
|
||||||
|
&types.AttributeValueMemberM{Value: map[string]types.AttributeValue{
|
||||||
|
"lat": &types.AttributeValueMemberN{Value: "12.34"},
|
||||||
|
"long": &types.AttributeValueMemberN{Value: "45.78"},
|
||||||
|
}},
|
||||||
|
&types.AttributeValueMemberM{Value: map[string]types.AttributeValue{
|
||||||
|
"lat": &types.AttributeValueMemberN{Value: "11.22"},
|
||||||
|
"long": &types.AttributeValueMemberN{Value: "33.44"},
|
||||||
|
}},
|
||||||
|
}},
|
||||||
|
}}},
|
||||||
|
|
||||||
|
{name: "binary set", val: &types.AttributeValueMemberBS{Value: [][]byte{
|
||||||
|
{1, 2, 3},
|
||||||
|
{4, 5, 6},
|
||||||
|
{7, 8, 9},
|
||||||
|
}}},
|
||||||
|
{name: "number set", val: &types.AttributeValueMemberNS{Value: []string{
|
||||||
|
"123",
|
||||||
|
"456",
|
||||||
|
"789",
|
||||||
|
}}},
|
||||||
|
{name: "string set", val: &types.AttributeValueMemberSS{Value: []string{
|
||||||
|
"more",
|
||||||
|
"string",
|
||||||
|
"stuff",
|
||||||
|
}}},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, scenario := range scenarios {
|
||||||
|
t.Run(scenario.name, func(t *testing.T) {
|
||||||
|
bfr := new(bytes.Buffer)
|
||||||
|
|
||||||
|
err := attrcodec.NewEncoder(bfr).Encode(scenario.val)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
|
||||||
|
t.Logf("length = %v", bfr.Len())
|
||||||
|
|
||||||
|
otherVal, err := attrcodec.NewDecoder(bfr).Decode()
|
||||||
|
assert.NoError(t, err)
|
||||||
|
assert.Equal(t, scenario.val, otherVal)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
165
internal/dynamo-browse/models/attrcodec/decoder.go
Normal file
165
internal/dynamo-browse/models/attrcodec/decoder.go
Normal file
|
@ -0,0 +1,165 @@
|
||||||
|
package attrcodec
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/binary"
|
||||||
|
"github.com/aws/aws-sdk-go-v2/service/dynamodb/types"
|
||||||
|
"github.com/pkg/errors"
|
||||||
|
"io"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Decoder struct {
|
||||||
|
r io.Reader
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewDecoder(r io.Reader) *Decoder {
|
||||||
|
return &Decoder{r: r}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *Decoder) Decode() (types.AttributeValue, error) {
|
||||||
|
return d.decode()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *Decoder) decode() (types.AttributeValue, error) {
|
||||||
|
fr, err := d.readFrame()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
switch fr.typeID {
|
||||||
|
case typeString:
|
||||||
|
return &types.AttributeValueMemberS{Value: string(fr.data)}, nil
|
||||||
|
case typeNumber:
|
||||||
|
return &types.AttributeValueMemberN{Value: string(fr.data)}, nil
|
||||||
|
case typeBoolean:
|
||||||
|
return &types.AttributeValueMemberBOOL{Value: fr.flags&flagsAlternative != 0}, nil
|
||||||
|
case typeNull:
|
||||||
|
return &types.AttributeValueMemberNULL{Value: fr.flags&flagsAlternative == 0}, nil
|
||||||
|
case typeBytes:
|
||||||
|
return &types.AttributeValueMemberB{Value: fr.data}, nil
|
||||||
|
case typeList:
|
||||||
|
vals := make([]types.AttributeValue, fr.length)
|
||||||
|
for i := range vals {
|
||||||
|
v, err := d.decode()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
vals[i] = v
|
||||||
|
}
|
||||||
|
return &types.AttributeValueMemberL{Value: vals}, nil
|
||||||
|
case typeMap:
|
||||||
|
vals := make(map[string]types.AttributeValue)
|
||||||
|
for i := 0; i < fr.length; i++ {
|
||||||
|
// key
|
||||||
|
keyFrame, err := d.readFrame()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
} else if keyFrame.typeID != typeString {
|
||||||
|
return nil, errors.Errorf("key of %v must be string, but is ID %v", i, keyFrame.typeID)
|
||||||
|
}
|
||||||
|
|
||||||
|
// value
|
||||||
|
v, err := d.decode()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
vals[string(keyFrame.data)] = v
|
||||||
|
}
|
||||||
|
return &types.AttributeValueMemberM{Value: vals}, nil
|
||||||
|
case typeByteSet:
|
||||||
|
vals := make([][]byte, fr.length)
|
||||||
|
for i := range vals {
|
||||||
|
itemFrame, err := d.readFrame()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
} else if itemFrame.typeID != typeBytes {
|
||||||
|
return nil, errors.Errorf("item %v of byte-set must be bytes, but is ID %v", i, itemFrame.typeID)
|
||||||
|
}
|
||||||
|
|
||||||
|
vals[i] = itemFrame.data
|
||||||
|
}
|
||||||
|
return &types.AttributeValueMemberBS{Value: vals}, nil
|
||||||
|
case typeNumberSet:
|
||||||
|
vals := make([]string, fr.length)
|
||||||
|
for i := range vals {
|
||||||
|
itemFrame, err := d.readFrame()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
} else if itemFrame.typeID != typeNumber {
|
||||||
|
return nil, errors.Errorf("item %v of number-set must be number, but is ID %v", i, itemFrame.typeID)
|
||||||
|
}
|
||||||
|
|
||||||
|
vals[i] = string(itemFrame.data)
|
||||||
|
}
|
||||||
|
return &types.AttributeValueMemberNS{Value: vals}, nil
|
||||||
|
case typeStringSet:
|
||||||
|
vals := make([]string, fr.length)
|
||||||
|
for i := range vals {
|
||||||
|
itemFrame, err := d.readFrame()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
} else if itemFrame.typeID != typeString {
|
||||||
|
return nil, errors.Errorf("item %v of string-set must be number, but is ID %v", i, itemFrame.typeID)
|
||||||
|
}
|
||||||
|
|
||||||
|
vals[i] = string(itemFrame.data)
|
||||||
|
}
|
||||||
|
return &types.AttributeValueMemberSS{Value: vals}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil, errors.Errorf("unrecognised type ID: %x", fr.typeID)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *Decoder) readFrame() (frame, error) {
|
||||||
|
var typeBfr [1]byte
|
||||||
|
|
||||||
|
n, err := d.r.Read(typeBfr[:])
|
||||||
|
if err != nil {
|
||||||
|
return frame{}, err
|
||||||
|
} else if n != 1 {
|
||||||
|
return frame{}, errors.New("expected frame typeID")
|
||||||
|
}
|
||||||
|
|
||||||
|
typeID := typeBfr[0] &^ flagMask
|
||||||
|
flags := typeBfr[0] & flagMask
|
||||||
|
|
||||||
|
typeInfo, hasTypeInfo := typeFrameInfos[typeID]
|
||||||
|
if !hasTypeInfo {
|
||||||
|
return frame{}, errors.Errorf("unrecognised typeID: %x", typeID)
|
||||||
|
}
|
||||||
|
|
||||||
|
if typeInfo.isNilLength {
|
||||||
|
return frame{typeID: typeID, flags: flags, data: nil}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: this needs to depend on the type
|
||||||
|
var l int64
|
||||||
|
if flags&flagsAlternative != 0 {
|
||||||
|
if err := binary.Read(d.r, byteOrder, &l); err != nil {
|
||||||
|
return frame{}, errors.Wrap(err, "cannot encode alt length")
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
var lenBfr [1]byte
|
||||||
|
|
||||||
|
n, err := d.r.Read(lenBfr[:])
|
||||||
|
if err != nil {
|
||||||
|
return frame{}, err
|
||||||
|
} else if n != 1 {
|
||||||
|
return frame{}, errors.New("expected frame typeID")
|
||||||
|
}
|
||||||
|
l = int64(lenBfr[0])
|
||||||
|
}
|
||||||
|
|
||||||
|
if typeInfo.lengthOnly {
|
||||||
|
return frame{typeID: typeID, flags: flags, length: int(l)}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
bs := make([]byte, l)
|
||||||
|
n, err = d.r.Read(bs)
|
||||||
|
if err != nil {
|
||||||
|
return frame{}, err
|
||||||
|
} else if n != int(l) {
|
||||||
|
return frame{}, errors.Errorf("expected %v bytes but received %v", l, n)
|
||||||
|
}
|
||||||
|
|
||||||
|
return frame{typeID: typeID, flags: flags, data: bs}, nil
|
||||||
|
}
|
139
internal/dynamo-browse/models/attrcodec/encoder.go
Normal file
139
internal/dynamo-browse/models/attrcodec/encoder.go
Normal file
|
@ -0,0 +1,139 @@
|
||||||
|
package attrcodec
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/binary"
|
||||||
|
"github.com/aws/aws-sdk-go-v2/service/dynamodb/types"
|
||||||
|
"github.com/pkg/errors"
|
||||||
|
"io"
|
||||||
|
)
|
||||||
|
|
||||||
|
var byteOrder = binary.LittleEndian
|
||||||
|
|
||||||
|
type Encoder struct {
|
||||||
|
w io.Writer
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewEncoder(w io.Writer) *Encoder {
|
||||||
|
return &Encoder{w: w}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e *Encoder) Encode(val types.AttributeValue) error {
|
||||||
|
return e.encode(val)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e *Encoder) encode(val types.AttributeValue) error {
|
||||||
|
switch v := val.(type) {
|
||||||
|
case *types.AttributeValueMemberS:
|
||||||
|
return e.writeFrame(typeString, []byte(v.Value))
|
||||||
|
case *types.AttributeValueMemberN:
|
||||||
|
return e.writeFrame(typeNumber, []byte(v.Value))
|
||||||
|
case *types.AttributeValueMemberBOOL:
|
||||||
|
if v.Value {
|
||||||
|
return e.writeNilLengthFrame(typeBoolean, flagsAlternative)
|
||||||
|
} else {
|
||||||
|
return e.writeNilLengthFrame(typeBoolean, 0x0)
|
||||||
|
}
|
||||||
|
case *types.AttributeValueMemberNULL:
|
||||||
|
if !v.Value {
|
||||||
|
return e.writeNilLengthFrame(typeNull, flagsAlternative)
|
||||||
|
} else {
|
||||||
|
return e.writeNilLengthFrame(typeNull, 0x0)
|
||||||
|
}
|
||||||
|
case *types.AttributeValueMemberB:
|
||||||
|
return e.writeFrame(typeBytes, v.Value)
|
||||||
|
case *types.AttributeValueMemberL:
|
||||||
|
if err := e.writeFrameHeader(typeList, len(v.Value)); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
for _, nv := range v.Value {
|
||||||
|
if err := e.encode(nv); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
case *types.AttributeValueMemberM:
|
||||||
|
if err := e.writeFrameHeader(typeMap, len(v.Value)); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
for k, kv := range v.Value {
|
||||||
|
// Keys are always strings
|
||||||
|
if err := e.writeFrame(typeString, []byte(k)); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if err := e.encode(kv); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
case *types.AttributeValueMemberBS:
|
||||||
|
if err := e.writeFrameHeader(typeByteSet, len(v.Value)); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
for _, nv := range v.Value {
|
||||||
|
if err := e.writeFrame(typeBytes, nv); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
case *types.AttributeValueMemberNS:
|
||||||
|
if err := e.writeFrameHeader(typeNumberSet, len(v.Value)); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
for _, nv := range v.Value {
|
||||||
|
if err := e.writeFrame(typeNumber, []byte(nv)); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
case *types.AttributeValueMemberSS:
|
||||||
|
if err := e.writeFrameHeader(typeStringSet, len(v.Value)); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
for _, nv := range v.Value {
|
||||||
|
if err := e.writeFrame(typeString, []byte(nv)); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
|
||||||
|
}
|
||||||
|
return errors.New("unhandled type")
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e *Encoder) writeNilLengthFrame(typeID byte, flags byte) error {
|
||||||
|
if _, err := e.w.Write([]byte{typeID | flags}); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e *Encoder) writeFrameHeader(typeID byte, length int) error {
|
||||||
|
if length <= 255 {
|
||||||
|
if _, err := e.w.Write([]byte{typeID, byte(length)}); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Length longer than a byte, use a int32
|
||||||
|
if _, err := e.w.Write([]byte{typeID | flagsAlternative}); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := binary.Write(e.w, byteOrder, int64(length)); err != nil {
|
||||||
|
return errors.Wrap(err, "cannot encode alt length")
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e *Encoder) writeFrame(typeID byte, bts []byte) error {
|
||||||
|
if err := e.writeFrameHeader(typeID, len(bts)); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
_, err := e.w.Write(bts)
|
||||||
|
return err
|
||||||
|
}
|
43
internal/dynamo-browse/models/attrcodec/frames.go
Normal file
43
internal/dynamo-browse/models/attrcodec/frames.go
Normal file
|
@ -0,0 +1,43 @@
|
||||||
|
package attrcodec
|
||||||
|
|
||||||
|
const (
|
||||||
|
typeString byte = 0x01
|
||||||
|
typeNumber byte = 0x02
|
||||||
|
typeBoolean byte = 0x03
|
||||||
|
typeNull byte = 0x04
|
||||||
|
typeList byte = 0x05
|
||||||
|
typeMap byte = 0x06
|
||||||
|
typeBytes byte = 0x07
|
||||||
|
typeByteSet byte = 0x08
|
||||||
|
typeNumberSet byte = 0x09
|
||||||
|
typeStringSet byte = 0x0A
|
||||||
|
|
||||||
|
flagMask = 0x80
|
||||||
|
|
||||||
|
flagsAlternative = 0x80
|
||||||
|
)
|
||||||
|
|
||||||
|
type frame struct {
|
||||||
|
typeID byte
|
||||||
|
flags byte
|
||||||
|
length int
|
||||||
|
data []byte
|
||||||
|
}
|
||||||
|
|
||||||
|
type typeFrameInfo struct {
|
||||||
|
isNilLength bool
|
||||||
|
lengthOnly bool
|
||||||
|
}
|
||||||
|
|
||||||
|
var typeFrameInfos = map[byte]typeFrameInfo{
|
||||||
|
typeString: {},
|
||||||
|
typeNumber: {},
|
||||||
|
typeBoolean: {isNilLength: true},
|
||||||
|
typeNull: {isNilLength: true},
|
||||||
|
typeList: {lengthOnly: true},
|
||||||
|
typeMap: {lengthOnly: true},
|
||||||
|
typeBytes: {},
|
||||||
|
typeByteSet: {lengthOnly: true},
|
||||||
|
typeNumberSet: {lengthOnly: true},
|
||||||
|
typeStringSet: {lengthOnly: true},
|
||||||
|
}
|
53
internal/dynamo-browse/models/attrutils/equals.go
Normal file
53
internal/dynamo-browse/models/attrutils/equals.go
Normal file
|
@ -0,0 +1,53 @@
|
||||||
|
package attrutils
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/aws/aws-sdk-go-v2/service/dynamodb/types"
|
||||||
|
"golang.org/x/exp/maps"
|
||||||
|
"golang.org/x/exp/slices"
|
||||||
|
)
|
||||||
|
|
||||||
|
func Equals(x, y types.AttributeValue) bool {
|
||||||
|
switch xVal := x.(type) {
|
||||||
|
case *types.AttributeValueMemberS:
|
||||||
|
c, ok := CompareScalarAttributes(x, y)
|
||||||
|
return ok && c == 0
|
||||||
|
case *types.AttributeValueMemberN:
|
||||||
|
c, ok := CompareScalarAttributes(x, y)
|
||||||
|
return ok && c == 0
|
||||||
|
case *types.AttributeValueMemberBOOL:
|
||||||
|
c, ok := CompareScalarAttributes(x, y)
|
||||||
|
return ok && c == 0
|
||||||
|
case *types.AttributeValueMemberB:
|
||||||
|
if yVal, ok := y.(*types.AttributeValueMemberB); ok {
|
||||||
|
return slices.Equal(xVal.Value, yVal.Value)
|
||||||
|
}
|
||||||
|
case *types.AttributeValueMemberNULL:
|
||||||
|
if yVal, ok := y.(*types.AttributeValueMemberNULL); ok {
|
||||||
|
return xVal.Value == yVal.Value
|
||||||
|
}
|
||||||
|
case *types.AttributeValueMemberL:
|
||||||
|
if yVal, ok := y.(*types.AttributeValueMemberL); ok {
|
||||||
|
return slices.EqualFunc(xVal.Value, yVal.Value, Equals)
|
||||||
|
}
|
||||||
|
case *types.AttributeValueMemberM:
|
||||||
|
if yVal, ok := y.(*types.AttributeValueMemberM); ok {
|
||||||
|
return maps.EqualFunc(xVal.Value, yVal.Value, Equals)
|
||||||
|
}
|
||||||
|
case *types.AttributeValueMemberBS:
|
||||||
|
if yVal, ok := y.(*types.AttributeValueMemberBS); ok {
|
||||||
|
return slices.EqualFunc(xVal.Value, yVal.Value, func(xs, ys []byte) bool {
|
||||||
|
return slices.Equal(xs, ys)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
case *types.AttributeValueMemberNS:
|
||||||
|
if yVal, ok := y.(*types.AttributeValueMemberNS); ok {
|
||||||
|
return slices.Equal(xVal.Value, yVal.Value)
|
||||||
|
}
|
||||||
|
case *types.AttributeValueMemberSS:
|
||||||
|
if yVal, ok := y.(*types.AttributeValueMemberSS); ok {
|
||||||
|
return slices.Equal(xVal.Value, yVal.Value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return false
|
||||||
|
}
|
68
internal/dynamo-browse/models/attrutils/hash.go
Normal file
68
internal/dynamo-browse/models/attrutils/hash.go
Normal file
|
@ -0,0 +1,68 @@
|
||||||
|
package attrutils
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/aws/aws-sdk-go-v2/service/dynamodb/types"
|
||||||
|
"golang.org/x/exp/maps"
|
||||||
|
"golang.org/x/exp/slices"
|
||||||
|
"hash"
|
||||||
|
"hash/fnv"
|
||||||
|
)
|
||||||
|
|
||||||
|
func HashCode(x types.AttributeValue) uint64 {
|
||||||
|
h := fnv.New64a()
|
||||||
|
doHash(x, h)
|
||||||
|
return h.Sum64()
|
||||||
|
}
|
||||||
|
|
||||||
|
func HashTo(h hash.Hash, x types.AttributeValue) {
|
||||||
|
doHash(x, h)
|
||||||
|
}
|
||||||
|
|
||||||
|
func doHash(x types.AttributeValue, h hash.Hash) {
|
||||||
|
switch xVal := x.(type) {
|
||||||
|
case *types.AttributeValueMemberS:
|
||||||
|
h.Write([]byte(xVal.Value))
|
||||||
|
case *types.AttributeValueMemberN:
|
||||||
|
h.Write([]byte(xVal.Value))
|
||||||
|
case *types.AttributeValueMemberBOOL:
|
||||||
|
if xVal.Value {
|
||||||
|
h.Write([]byte{0})
|
||||||
|
} else {
|
||||||
|
h.Write([]byte{1})
|
||||||
|
}
|
||||||
|
case *types.AttributeValueMemberB:
|
||||||
|
h.Write(xVal.Value)
|
||||||
|
case *types.AttributeValueMemberNULL:
|
||||||
|
if xVal.Value {
|
||||||
|
h.Write([]byte{0})
|
||||||
|
} else {
|
||||||
|
h.Write([]byte{1})
|
||||||
|
}
|
||||||
|
case *types.AttributeValueMemberL:
|
||||||
|
for _, v := range xVal.Value {
|
||||||
|
doHash(v, h)
|
||||||
|
}
|
||||||
|
case *types.AttributeValueMemberM:
|
||||||
|
// To keep this consistent, this will need to be in key sorted order
|
||||||
|
sortedKeys := make([]string, len(xVal.Value))
|
||||||
|
copy(sortedKeys, maps.Keys(xVal.Value))
|
||||||
|
slices.Sort(sortedKeys)
|
||||||
|
|
||||||
|
for _, k := range sortedKeys {
|
||||||
|
h.Write([]byte(k))
|
||||||
|
doHash(xVal.Value[k], h)
|
||||||
|
}
|
||||||
|
case *types.AttributeValueMemberBS:
|
||||||
|
for _, v := range xVal.Value {
|
||||||
|
h.Write(v)
|
||||||
|
}
|
||||||
|
case *types.AttributeValueMemberNS:
|
||||||
|
for _, v := range xVal.Value {
|
||||||
|
h.Write([]byte(v))
|
||||||
|
}
|
||||||
|
case *types.AttributeValueMemberSS:
|
||||||
|
for _, v := range xVal.Value {
|
||||||
|
h.Write([]byte(v))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -13,6 +13,8 @@ type ResultSet struct {
|
||||||
|
|
||||||
type Queryable interface {
|
type Queryable interface {
|
||||||
String() string
|
String() string
|
||||||
|
SerializeToBytes() ([]byte, error)
|
||||||
|
HashCode() uint64
|
||||||
Plan(tableInfo *TableInfo) (*QueryExecutionPlan, error)
|
Plan(tableInfo *TableInfo) (*QueryExecutionPlan, error)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -49,9 +49,14 @@ type astEqualityOp struct {
|
||||||
}
|
}
|
||||||
|
|
||||||
type astIsOp struct {
|
type astIsOp struct {
|
||||||
Ref *astFunctionCall `parser:"@@ ( 'is' "`
|
Ref *astSubRef `parser:"@@ ( 'is' "`
|
||||||
HasNot bool `parser:"@'not'?"`
|
HasNot bool `parser:"@'not'?"`
|
||||||
Value *astFunctionCall `parser:"@@ )?"`
|
Value *astSubRef `parser:"@@ )?"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type astSubRef struct {
|
||||||
|
Ref *astFunctionCall `parser:"@@"`
|
||||||
|
Quals []string `parser:"('.' @Ident)*"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type astFunctionCall struct {
|
type astFunctionCall struct {
|
||||||
|
@ -61,14 +66,18 @@ type astFunctionCall struct {
|
||||||
}
|
}
|
||||||
|
|
||||||
type astAtom struct {
|
type astAtom struct {
|
||||||
Ref *astDot `parser:"@@ | "`
|
Ref *astRef `parser:"@@ | "`
|
||||||
Literal *astLiteralValue `parser:"@@ | "`
|
Literal *astLiteralValue `parser:"@@ | "`
|
||||||
|
Placeholder *astPlaceholder `parser:"@@ | "`
|
||||||
Paren *astExpr `parser:"'(' @@ ')'"`
|
Paren *astExpr `parser:"'(' @@ ')'"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type astDot struct {
|
type astRef struct {
|
||||||
Name string `parser:"@Ident"`
|
Name string `parser:"@Ident"`
|
||||||
Quals []string `parser:"('.' @Ident)*"`
|
}
|
||||||
|
|
||||||
|
type astPlaceholder struct {
|
||||||
|
Placeholder string `parser:"@PlaceholderIdent"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type astLiteralValue struct {
|
type astLiteralValue struct {
|
||||||
|
@ -83,6 +92,7 @@ var scanner = lexer.MustSimple([]lexer.SimpleRule{
|
||||||
{Name: "Int", Pattern: `[-+]?(\d*\.)?\d+`},
|
{Name: "Int", Pattern: `[-+]?(\d*\.)?\d+`},
|
||||||
{Name: "Number", Pattern: `[-+]?(\d*\.)?\d+`},
|
{Name: "Number", Pattern: `[-+]?(\d*\.)?\d+`},
|
||||||
{Name: "Ident", Pattern: `[a-zA-Z_][a-zA-Z0-9_-]*`},
|
{Name: "Ident", Pattern: `[a-zA-Z_][a-zA-Z0-9_-]*`},
|
||||||
|
{Name: "PlaceholderIdent", Pattern: `[$:][a-zA-Z0-9_-][a-zA-Z0-9_-]*`},
|
||||||
{Name: "Punct", Pattern: `[-[!@#$%^&*()+_={}\|:;"'<,>.?/]|][=]?`},
|
{Name: "Punct", Pattern: `[-[!@#$%^&*()+_={}\|:;"'<,>.?/]|][=]?`},
|
||||||
{Name: "EOL", Pattern: `[\n\r]+`},
|
{Name: "EOL", Pattern: `[\n\r]+`},
|
||||||
{Name: "whitespace", Pattern: `[ \t]+`},
|
{Name: "whitespace", Pattern: `[ \t]+`},
|
||||||
|
@ -100,8 +110,8 @@ func Parse(expr string) (*QueryExpr, error) {
|
||||||
return &QueryExpr{ast: ast}, nil
|
return &QueryExpr{ast: ast}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (a *astExpr) calcQuery(info *models.TableInfo) (*models.QueryExecutionPlan, error) {
|
func (a *astExpr) calcQuery(ctx *evalContext, info *models.TableInfo) (*models.QueryExecutionPlan, error) {
|
||||||
ir, err := a.evalToIR(info)
|
ir, err := a.evalToIR(ctx, info)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
@ -146,10 +156,22 @@ func (a *astExpr) calcQuery(info *models.TableInfo) (*models.QueryExecutionPlan,
|
||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (a *astExpr) evalToIR(tableInfo *models.TableInfo) (irAtom, error) {
|
func (a *astExpr) evalToIR(ctx *evalContext, tableInfo *models.TableInfo) (irAtom, error) {
|
||||||
return a.Root.evalToIR(tableInfo)
|
return a.Root.evalToIR(ctx, tableInfo)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (a *astExpr) evalItem(item models.Item) (types.AttributeValue, error) {
|
func (a *astExpr) evalItem(ctx *evalContext, item models.Item) (types.AttributeValue, error) {
|
||||||
return a.Root.evalItem(item)
|
return a.Root.evalItem(ctx, item)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *astExpr) setEvalItem(ctx *evalContext, item models.Item, value types.AttributeValue) error {
|
||||||
|
return a.Root.setEvalItem(ctx, item, value)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *astExpr) deleteAttribute(ctx *evalContext, item models.Item) error {
|
||||||
|
return a.Root.deleteAttribute(ctx, item)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (md *astExpr) canModifyItem(ctx *evalContext, item models.Item) bool {
|
||||||
|
return md.Root.canModifyItem(ctx, item)
|
||||||
}
|
}
|
||||||
|
|
|
@ -6,14 +6,16 @@ import (
|
||||||
"github.com/pkg/errors"
|
"github.com/pkg/errors"
|
||||||
)
|
)
|
||||||
|
|
||||||
func (a *astAtom) evalToIR(info *models.TableInfo) (irAtom, error) {
|
func (a *astAtom) evalToIR(ctx *evalContext, info *models.TableInfo) (irAtom, error) {
|
||||||
switch {
|
switch {
|
||||||
case a.Ref != nil:
|
case a.Ref != nil:
|
||||||
return a.Ref.evalToIR(info)
|
return a.Ref.evalToIR(ctx, info)
|
||||||
case a.Literal != nil:
|
case a.Literal != nil:
|
||||||
return a.Literal.evalToIR(info)
|
return a.Literal.evalToIR(ctx, info)
|
||||||
|
case a.Placeholder != nil:
|
||||||
|
return a.Placeholder.evalToIR(ctx, info)
|
||||||
case a.Paren != nil:
|
case a.Paren != nil:
|
||||||
return a.Paren.evalToIR(info)
|
return a.Paren.evalToIR(ctx, info)
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil, errors.New("unhandled atom case")
|
return nil, errors.New("unhandled atom case")
|
||||||
|
@ -37,19 +39,57 @@ func (a *astAtom) unqualifiedName() (string, bool) {
|
||||||
return "", false
|
return "", false
|
||||||
}
|
}
|
||||||
|
|
||||||
func (a *astAtom) evalItem(item models.Item) (types.AttributeValue, error) {
|
func (a *astAtom) evalItem(ctx *evalContext, item models.Item) (types.AttributeValue, error) {
|
||||||
switch {
|
switch {
|
||||||
case a.Ref != nil:
|
case a.Ref != nil:
|
||||||
return a.Ref.evalItem(item)
|
return a.Ref.evalItem(ctx, item)
|
||||||
case a.Literal != nil:
|
case a.Literal != nil:
|
||||||
return a.Literal.dynamoValue()
|
return a.Literal.dynamoValue()
|
||||||
|
case a.Placeholder != nil:
|
||||||
|
return a.Placeholder.evalItem(ctx, item)
|
||||||
case a.Paren != nil:
|
case a.Paren != nil:
|
||||||
return a.Paren.evalItem(item)
|
return a.Paren.evalItem(ctx, item)
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil, errors.New("unhandled atom case")
|
return nil, errors.New("unhandled atom case")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (a *astAtom) canModifyItem(ctx *evalContext, item models.Item) bool {
|
||||||
|
switch {
|
||||||
|
case a.Ref != nil:
|
||||||
|
return a.Ref.canModifyItem(ctx, item)
|
||||||
|
case a.Placeholder != nil:
|
||||||
|
return a.Placeholder.canModifyItem(ctx, item)
|
||||||
|
case a.Paren != nil:
|
||||||
|
return a.Paren.canModifyItem(ctx, item)
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *astAtom) setEvalItem(ctx *evalContext, item models.Item, value types.AttributeValue) error {
|
||||||
|
switch {
|
||||||
|
case a.Ref != nil:
|
||||||
|
return a.Ref.setEvalItem(ctx, item, value)
|
||||||
|
case a.Placeholder != nil:
|
||||||
|
return a.Placeholder.setEvalItem(ctx, item, value)
|
||||||
|
case a.Paren != nil:
|
||||||
|
return a.Paren.setEvalItem(ctx, item, value)
|
||||||
|
}
|
||||||
|
return PathNotSettableError{}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *astAtom) deleteAttribute(ctx *evalContext, item models.Item) error {
|
||||||
|
switch {
|
||||||
|
case a.Ref != nil:
|
||||||
|
return a.Ref.deleteAttribute(ctx, item)
|
||||||
|
case a.Paren != nil:
|
||||||
|
return a.Paren.deleteAttribute(ctx, item)
|
||||||
|
case a.Placeholder != nil:
|
||||||
|
return a.Placeholder.deleteAttribute(ctx, item)
|
||||||
|
}
|
||||||
|
return PathNotSettableError{}
|
||||||
|
}
|
||||||
|
|
||||||
func (a *astAtom) String() string {
|
func (a *astAtom) String() string {
|
||||||
switch {
|
switch {
|
||||||
case a.Ref != nil:
|
case a.Ref != nil:
|
||||||
|
@ -58,6 +98,8 @@ func (a *astAtom) String() string {
|
||||||
return a.Literal.String()
|
return a.Literal.String()
|
||||||
case a.Paren != nil:
|
case a.Paren != nil:
|
||||||
return "(" + a.Paren.String() + ")"
|
return "(" + a.Paren.String() + ")"
|
||||||
|
case a.Placeholder != nil:
|
||||||
|
return a.Placeholder.String()
|
||||||
}
|
}
|
||||||
return ""
|
return ""
|
||||||
}
|
}
|
||||||
|
|
|
@ -7,8 +7,8 @@ import (
|
||||||
"strings"
|
"strings"
|
||||||
)
|
)
|
||||||
|
|
||||||
func (a *astBooleanNot) evalToIR(tableInfo *models.TableInfo) (irAtom, error) {
|
func (a *astBooleanNot) evalToIR(ctx *evalContext, tableInfo *models.TableInfo) (irAtom, error) {
|
||||||
irNode, err := a.Operand.evalToIR(tableInfo)
|
irNode, err := a.Operand.evalToIR(ctx, tableInfo)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
@ -20,8 +20,8 @@ func (a *astBooleanNot) evalToIR(tableInfo *models.TableInfo) (irAtom, error) {
|
||||||
return &irBoolNot{atom: irNode}, nil
|
return &irBoolNot{atom: irNode}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (a *astBooleanNot) evalItem(item models.Item) (types.AttributeValue, error) {
|
func (a *astBooleanNot) evalItem(ctx *evalContext, item models.Item) (types.AttributeValue, error) {
|
||||||
val, err := a.Operand.evalItem(item)
|
val, err := a.Operand.evalItem(ctx, item)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
@ -33,6 +33,27 @@ func (a *astBooleanNot) evalItem(item models.Item) (types.AttributeValue, error)
|
||||||
return &types.AttributeValueMemberBOOL{Value: !isAttributeTrue(val)}, nil
|
return &types.AttributeValueMemberBOOL{Value: !isAttributeTrue(val)}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (a *astBooleanNot) canModifyItem(ctx *evalContext, item models.Item) bool {
|
||||||
|
if a.HasNot {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
return a.Operand.canModifyItem(ctx, item)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *astBooleanNot) setEvalItem(ctx *evalContext, item models.Item, value types.AttributeValue) error {
|
||||||
|
if a.HasNot {
|
||||||
|
return PathNotSettableError{}
|
||||||
|
}
|
||||||
|
return a.Operand.setEvalItem(ctx, item, value)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *astBooleanNot) deleteAttribute(ctx *evalContext, item models.Item) error {
|
||||||
|
if a.HasNot {
|
||||||
|
return PathNotSettableError{}
|
||||||
|
}
|
||||||
|
return a.Operand.deleteAttribute(ctx, item)
|
||||||
|
}
|
||||||
|
|
||||||
func (d *astBooleanNot) String() string {
|
func (d *astBooleanNot) String() string {
|
||||||
sb := new(strings.Builder)
|
sb := new(strings.Builder)
|
||||||
if d.HasNot {
|
if d.HasNot {
|
||||||
|
|
|
@ -8,8 +8,8 @@ import (
|
||||||
"github.com/pkg/errors"
|
"github.com/pkg/errors"
|
||||||
)
|
)
|
||||||
|
|
||||||
func (a *astComparisonOp) evalToIR(info *models.TableInfo) (irAtom, error) {
|
func (a *astComparisonOp) evalToIR(ctx *evalContext, info *models.TableInfo) (irAtom, error) {
|
||||||
leftIR, err := a.Ref.evalToIR(info)
|
leftIR, err := a.Ref.evalToIR(ctx, info)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
@ -28,7 +28,7 @@ func (a *astComparisonOp) evalToIR(info *models.TableInfo) (irAtom, error) {
|
||||||
return nil, OperandNotAnOperandError{}
|
return nil, OperandNotAnOperandError{}
|
||||||
}
|
}
|
||||||
|
|
||||||
rightIR, err := a.Value.evalToIR(info)
|
rightIR, err := a.Value.evalToIR(ctx, info)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
@ -47,8 +47,8 @@ func (a *astComparisonOp) evalToIR(info *models.TableInfo) (irAtom, error) {
|
||||||
return irGenericCmp{leftOpr, rightOpr, cmpType}, nil
|
return irGenericCmp{leftOpr, rightOpr, cmpType}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (a *astComparisonOp) evalItem(item models.Item) (types.AttributeValue, error) {
|
func (a *astComparisonOp) evalItem(ctx *evalContext, item models.Item) (types.AttributeValue, error) {
|
||||||
left, err := a.Ref.evalItem(item)
|
left, err := a.Ref.evalItem(ctx, item)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
@ -56,7 +56,7 @@ func (a *astComparisonOp) evalItem(item models.Item) (types.AttributeValue, erro
|
||||||
return left, nil
|
return left, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
right, err := a.Value.evalItem(item)
|
right, err := a.Value.evalItem(ctx, item)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
@ -79,6 +79,28 @@ func (a *astComparisonOp) evalItem(item models.Item) (types.AttributeValue, erro
|
||||||
return nil, errors.Errorf("unrecognised operator: %v", a.Op)
|
return nil, errors.Errorf("unrecognised operator: %v", a.Op)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (a *astComparisonOp) canModifyItem(ctx *evalContext, item models.Item) bool {
|
||||||
|
if a.Op != "" {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
return a.Ref.canModifyItem(ctx, item)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *astComparisonOp) setEvalItem(ctx *evalContext, item models.Item, value types.AttributeValue) error {
|
||||||
|
if a.Op != "" {
|
||||||
|
return PathNotSettableError{}
|
||||||
|
}
|
||||||
|
return a.Ref.setEvalItem(ctx, item, value)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *astComparisonOp) deleteAttribute(ctx *evalContext, item models.Item) error {
|
||||||
|
if a.Op != "" {
|
||||||
|
return PathNotSettableError{}
|
||||||
|
}
|
||||||
|
return a.Ref.deleteAttribute(ctx, item)
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
func (a *astComparisonOp) String() string {
|
func (a *astComparisonOp) String() string {
|
||||||
if a.Op == "" {
|
if a.Op == "" {
|
||||||
return a.Ref.String()
|
return a.Ref.String()
|
||||||
|
|
|
@ -7,16 +7,16 @@ import (
|
||||||
"strings"
|
"strings"
|
||||||
)
|
)
|
||||||
|
|
||||||
func (a *astConjunction) evalToIR(tableInfo *models.TableInfo) (irAtom, error) {
|
func (a *astConjunction) evalToIR(ctx *evalContext, tableInfo *models.TableInfo) (irAtom, error) {
|
||||||
if len(a.Operands) == 1 {
|
if len(a.Operands) == 1 {
|
||||||
return a.Operands[0].evalToIR(tableInfo)
|
return a.Operands[0].evalToIR(ctx, tableInfo)
|
||||||
} else if len(a.Operands) == 2 {
|
} else if len(a.Operands) == 2 {
|
||||||
left, err := a.Operands[0].evalToIR(tableInfo)
|
left, err := a.Operands[0].evalToIR(ctx, tableInfo)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
right, err := a.Operands[1].evalToIR(tableInfo)
|
right, err := a.Operands[1].evalToIR(ctx, tableInfo)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
@ -27,7 +27,7 @@ func (a *astConjunction) evalToIR(tableInfo *models.TableInfo) (irAtom, error) {
|
||||||
atoms := make([]irAtom, len(a.Operands))
|
atoms := make([]irAtom, len(a.Operands))
|
||||||
for i, op := range a.Operands {
|
for i, op := range a.Operands {
|
||||||
var err error
|
var err error
|
||||||
atoms[i], err = op.evalToIR(tableInfo)
|
atoms[i], err = op.evalToIR(ctx, tableInfo)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
@ -36,8 +36,8 @@ func (a *astConjunction) evalToIR(tableInfo *models.TableInfo) (irAtom, error) {
|
||||||
return &irMultiConjunction{atoms: atoms}, nil
|
return &irMultiConjunction{atoms: atoms}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (a *astConjunction) evalItem(item models.Item) (types.AttributeValue, error) {
|
func (a *astConjunction) evalItem(ctx *evalContext, item models.Item) (types.AttributeValue, error) {
|
||||||
val, err := a.Operands[0].evalItem(item)
|
val, err := a.Operands[0].evalItem(ctx, item)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
@ -50,7 +50,7 @@ func (a *astConjunction) evalItem(item models.Item) (types.AttributeValue, error
|
||||||
return &types.AttributeValueMemberBOOL{Value: false}, nil
|
return &types.AttributeValueMemberBOOL{Value: false}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
val, err = opr.evalItem(item)
|
val, err = opr.evalItem(ctx, item)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
@ -59,6 +59,30 @@ func (a *astConjunction) evalItem(item models.Item) (types.AttributeValue, error
|
||||||
return &types.AttributeValueMemberBOOL{Value: isAttributeTrue(val)}, nil
|
return &types.AttributeValueMemberBOOL{Value: isAttributeTrue(val)}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (a *astConjunction) canModifyItem(ctx *evalContext, item models.Item) bool {
|
||||||
|
if len(a.Operands) == 1 {
|
||||||
|
return a.Operands[0].canModifyItem(ctx, item)
|
||||||
|
}
|
||||||
|
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *astConjunction) setEvalItem(ctx *evalContext, item models.Item, value types.AttributeValue) error {
|
||||||
|
if len(a.Operands) == 1 {
|
||||||
|
return a.Operands[0].setEvalItem(ctx, item, value)
|
||||||
|
}
|
||||||
|
|
||||||
|
return PathNotSettableError{}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *astConjunction) deleteAttribute(ctx *evalContext, item models.Item) error {
|
||||||
|
if len(a.Operands) == 1 {
|
||||||
|
return a.Operands[0].deleteAttribute(ctx, item)
|
||||||
|
}
|
||||||
|
|
||||||
|
return PathNotSettableError{}
|
||||||
|
}
|
||||||
|
|
||||||
func (d *astConjunction) String() string {
|
func (d *astConjunction) String() string {
|
||||||
sb := new(strings.Builder)
|
sb := new(strings.Builder)
|
||||||
for i, operand := range d.Operands {
|
for i, operand := range d.Operands {
|
||||||
|
|
|
@ -7,15 +7,15 @@ import (
|
||||||
"strings"
|
"strings"
|
||||||
)
|
)
|
||||||
|
|
||||||
func (a *astDisjunction) evalToIR(tableInfo *models.TableInfo) (irAtom, error) {
|
func (a *astDisjunction) evalToIR(ctx *evalContext, tableInfo *models.TableInfo) (irAtom, error) {
|
||||||
if len(a.Operands) == 1 {
|
if len(a.Operands) == 1 {
|
||||||
return a.Operands[0].evalToIR(tableInfo)
|
return a.Operands[0].evalToIR(ctx, tableInfo)
|
||||||
}
|
}
|
||||||
|
|
||||||
conj := make([]irAtom, len(a.Operands))
|
conj := make([]irAtom, len(a.Operands))
|
||||||
for i, op := range a.Operands {
|
for i, op := range a.Operands {
|
||||||
var err error
|
var err error
|
||||||
conj[i], err = op.evalToIR(tableInfo)
|
conj[i], err = op.evalToIR(ctx, tableInfo)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
@ -24,8 +24,8 @@ func (a *astDisjunction) evalToIR(tableInfo *models.TableInfo) (irAtom, error) {
|
||||||
return &irDisjunction{conj: conj}, nil
|
return &irDisjunction{conj: conj}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (a *astDisjunction) evalItem(item models.Item) (types.AttributeValue, error) {
|
func (a *astDisjunction) evalItem(ctx *evalContext, item models.Item) (types.AttributeValue, error) {
|
||||||
val, err := a.Operands[0].evalItem(item)
|
val, err := a.Operands[0].evalItem(ctx, item)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
@ -38,7 +38,7 @@ func (a *astDisjunction) evalItem(item models.Item) (types.AttributeValue, error
|
||||||
return &types.AttributeValueMemberBOOL{Value: true}, nil
|
return &types.AttributeValueMemberBOOL{Value: true}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
val, err = opr.evalItem(item)
|
val, err = opr.evalItem(ctx, item)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
@ -47,6 +47,30 @@ func (a *astDisjunction) evalItem(item models.Item) (types.AttributeValue, error
|
||||||
return &types.AttributeValueMemberBOOL{Value: isAttributeTrue(val)}, nil
|
return &types.AttributeValueMemberBOOL{Value: isAttributeTrue(val)}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (a *astDisjunction) canModifyItem(ctx *evalContext, item models.Item) bool {
|
||||||
|
if len(a.Operands) == 1 {
|
||||||
|
return a.Operands[0].canModifyItem(ctx, item)
|
||||||
|
}
|
||||||
|
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *astDisjunction) setEvalItem(ctx *evalContext, item models.Item, value types.AttributeValue) error {
|
||||||
|
if len(a.Operands) == 1 {
|
||||||
|
return a.Operands[0].setEvalItem(ctx, item, value)
|
||||||
|
}
|
||||||
|
|
||||||
|
return PathNotSettableError{}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *astDisjunction) deleteAttribute(ctx *evalContext, item models.Item) error {
|
||||||
|
if len(a.Operands) == 1 {
|
||||||
|
return a.Operands[0].deleteAttribute(ctx, item)
|
||||||
|
}
|
||||||
|
|
||||||
|
return PathNotSettableError{}
|
||||||
|
}
|
||||||
|
|
||||||
func (d *astDisjunction) String() string {
|
func (d *astDisjunction) String() string {
|
||||||
sb := new(strings.Builder)
|
sb := new(strings.Builder)
|
||||||
for i, operand := range d.Operands {
|
for i, operand := range d.Operands {
|
||||||
|
|
|
@ -4,51 +4,41 @@ import (
|
||||||
"github.com/aws/aws-sdk-go-v2/feature/dynamodb/expression"
|
"github.com/aws/aws-sdk-go-v2/feature/dynamodb/expression"
|
||||||
"github.com/aws/aws-sdk-go-v2/service/dynamodb/types"
|
"github.com/aws/aws-sdk-go-v2/service/dynamodb/types"
|
||||||
"github.com/lmika/audax/internal/dynamo-browse/models"
|
"github.com/lmika/audax/internal/dynamo-browse/models"
|
||||||
"strings"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
func (dt *astDot) evalToIR(info *models.TableInfo) (irAtom, error) {
|
func (dt *astRef) evalToIR(ctx *evalContext, info *models.TableInfo) (irAtom, error) {
|
||||||
return irNamePath{dt.Name, dt.Quals}, nil
|
return irNamePath{name: dt.Name}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (dt *astDot) unqualifiedName() (string, bool) {
|
func (dt *astRef) unqualifiedName() (string, bool) {
|
||||||
if len(dt.Quals) == 0 {
|
|
||||||
return dt.Name, true
|
return dt.Name, true
|
||||||
}
|
}
|
||||||
return "", false
|
|
||||||
}
|
|
||||||
|
|
||||||
func (dt *astDot) evalItem(item models.Item) (types.AttributeValue, error) {
|
func (dt *astRef) evalItem(ctx *evalContext, item models.Item) (types.AttributeValue, error) {
|
||||||
res, hasV := item[dt.Name]
|
res, hasV := item[dt.Name]
|
||||||
if !hasV {
|
if !hasV {
|
||||||
return nil, nil
|
return nil, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
for i, qualName := range dt.Quals {
|
|
||||||
mapRes, isMapRes := res.(*types.AttributeValueMemberM)
|
|
||||||
if !isMapRes {
|
|
||||||
return nil, ValueNotAMapError(append([]string{dt.Name}, dt.Quals[:i+1]...))
|
|
||||||
}
|
|
||||||
|
|
||||||
res, hasV = mapRes.Value[qualName]
|
|
||||||
if !hasV {
|
|
||||||
return nil, nil
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return res, nil
|
return res, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (a *astDot) String() string {
|
func (dt *astRef) canModifyItem(ctx *evalContext, item models.Item) bool {
|
||||||
var sb strings.Builder
|
return true
|
||||||
|
|
||||||
sb.WriteString(a.Name)
|
|
||||||
for _, q := range a.Quals {
|
|
||||||
sb.WriteRune('.')
|
|
||||||
sb.WriteString(q)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return sb.String()
|
func (dt *astRef) setEvalItem(ctx *evalContext, item models.Item, value types.AttributeValue) error {
|
||||||
|
item[dt.Name] = value
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (dt *astRef) deleteAttribute(ctx *evalContext, item models.Item) error {
|
||||||
|
delete(item, dt.Name)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *astRef) String() string {
|
||||||
|
return a.Name
|
||||||
}
|
}
|
||||||
|
|
||||||
type irNamePath struct {
|
type irNamePath struct {
|
||||||
|
|
|
@ -9,8 +9,8 @@ import (
|
||||||
"strings"
|
"strings"
|
||||||
)
|
)
|
||||||
|
|
||||||
func (a *astEqualityOp) evalToIR(info *models.TableInfo) (irAtom, error) {
|
func (a *astEqualityOp) evalToIR(ctx *evalContext, info *models.TableInfo) (irAtom, error) {
|
||||||
leftIR, err := a.Ref.evalToIR(info)
|
leftIR, err := a.Ref.evalToIR(ctx, info)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
@ -24,7 +24,7 @@ func (a *astEqualityOp) evalToIR(info *models.TableInfo) (irAtom, error) {
|
||||||
return nil, OperandNotAnOperandError{}
|
return nil, OperandNotAnOperandError{}
|
||||||
}
|
}
|
||||||
|
|
||||||
rightIR, err := a.Value.evalToIR(info)
|
rightIR, err := a.Value.evalToIR(ctx, info)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
@ -59,8 +59,8 @@ func (a *astEqualityOp) evalToIR(info *models.TableInfo) (irAtom, error) {
|
||||||
return nil, errors.Errorf("unrecognised operator: %v", a.Op)
|
return nil, errors.Errorf("unrecognised operator: %v", a.Op)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (a *astEqualityOp) evalItem(item models.Item) (types.AttributeValue, error) {
|
func (a *astEqualityOp) evalItem(ctx *evalContext, item models.Item) (types.AttributeValue, error) {
|
||||||
left, err := a.Ref.evalItem(item)
|
left, err := a.Ref.evalItem(ctx, item)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
@ -69,7 +69,7 @@ func (a *astEqualityOp) evalItem(item models.Item) (types.AttributeValue, error)
|
||||||
return left, nil
|
return left, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
right, err := a.Value.evalItem(item)
|
right, err := a.Value.evalItem(ctx, item)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
@ -103,6 +103,27 @@ func (a *astEqualityOp) evalItem(item models.Item) (types.AttributeValue, error)
|
||||||
return nil, errors.Errorf("unrecognised operator: %v", a.Op)
|
return nil, errors.Errorf("unrecognised operator: %v", a.Op)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (a *astEqualityOp) canModifyItem(ctx *evalContext, item models.Item) bool {
|
||||||
|
if a.Op != "" {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
return a.Ref.canModifyItem(ctx, item)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *astEqualityOp) setEvalItem(ctx *evalContext, item models.Item, value types.AttributeValue) error {
|
||||||
|
if a.Op != "" {
|
||||||
|
return PathNotSettableError{}
|
||||||
|
}
|
||||||
|
return a.Ref.setEvalItem(ctx, item, value)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *astEqualityOp) deleteAttribute(ctx *evalContext, item models.Item) error {
|
||||||
|
if a.Op != "" {
|
||||||
|
return PathNotSettableError{}
|
||||||
|
}
|
||||||
|
return a.Ref.deleteAttribute(ctx, item)
|
||||||
|
}
|
||||||
|
|
||||||
func (a *astEqualityOp) String() string {
|
func (a *astEqualityOp) String() string {
|
||||||
if a.Op == "" {
|
if a.Op == "" {
|
||||||
return a.Ref.String()
|
return a.Ref.String()
|
||||||
|
|
|
@ -108,3 +108,18 @@ type UnrecognisedFunctionError struct {
|
||||||
func (e UnrecognisedFunctionError) Error() string {
|
func (e UnrecognisedFunctionError) Error() string {
|
||||||
return "unrecognised function '" + e.Name + "'"
|
return "unrecognised function '" + e.Name + "'"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type PathNotSettableError struct {
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e PathNotSettableError) Error() string {
|
||||||
|
return "path cannot be set a value"
|
||||||
|
}
|
||||||
|
|
||||||
|
type MissingPlaceholderError struct {
|
||||||
|
Placeholder string
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e MissingPlaceholderError) Error() string {
|
||||||
|
return "undefined placeholder '" + e.Placeholder + "'"
|
||||||
|
}
|
||||||
|
|
|
@ -1,20 +1,193 @@
|
||||||
package queryexpr
|
package queryexpr
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"bytes"
|
||||||
|
"encoding/gob"
|
||||||
"github.com/aws/aws-sdk-go-v2/service/dynamodb/types"
|
"github.com/aws/aws-sdk-go-v2/service/dynamodb/types"
|
||||||
"github.com/lmika/audax/internal/dynamo-browse/models"
|
"github.com/lmika/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/pkg/errors"
|
||||||
|
"golang.org/x/exp/maps"
|
||||||
|
"golang.org/x/exp/slices"
|
||||||
|
"hash/fnv"
|
||||||
|
"io"
|
||||||
)
|
)
|
||||||
|
|
||||||
type QueryExpr struct {
|
type QueryExpr struct {
|
||||||
ast *astExpr
|
ast *astExpr
|
||||||
|
names map[string]string
|
||||||
|
values map[string]types.AttributeValue
|
||||||
|
}
|
||||||
|
|
||||||
|
type serializedExpr struct {
|
||||||
|
Expr string
|
||||||
|
Names map[string]string
|
||||||
|
Values []byte
|
||||||
|
}
|
||||||
|
|
||||||
|
func DeserializeFrom(r io.Reader) (*QueryExpr, error) {
|
||||||
|
var se serializedExpr
|
||||||
|
|
||||||
|
if err := gob.NewDecoder(r).Decode(&se); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
qe, err := Parse(se.Expr)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
qe.names = se.Names
|
||||||
|
|
||||||
|
if len(se.Values) > 0 {
|
||||||
|
vals, err := attrcodec.NewDecoder(bytes.NewReader(se.Values)).Decode()
|
||||||
|
if err != nil {
|
||||||
|
return nil, errors.Wrap(err, "unable to marshal placeholder values")
|
||||||
|
}
|
||||||
|
mvals, ok := vals.(*types.AttributeValueMemberM)
|
||||||
|
if !ok {
|
||||||
|
return nil, errors.Errorf("expected marshaled placeholder values to be map, but was %T", vals)
|
||||||
|
}
|
||||||
|
qe.values = mvals.Value
|
||||||
|
}
|
||||||
|
|
||||||
|
return qe, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (md *QueryExpr) SerializeTo(w io.Writer) error {
|
||||||
|
se := serializedExpr{Expr: md.String(), Names: md.names}
|
||||||
|
if md.values != nil {
|
||||||
|
var bts bytes.Buffer
|
||||||
|
if err := attrcodec.NewEncoder(&bts).Encode(&types.AttributeValueMemberM{Value: md.values}); err != nil {
|
||||||
|
return errors.Wrap(err, "unable to unmarshal placeholder values")
|
||||||
|
}
|
||||||
|
se.Values = bts.Bytes()
|
||||||
|
}
|
||||||
|
|
||||||
|
return gob.NewEncoder(w).Encode(se)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (md *QueryExpr) SerializeToBytes() ([]byte, error) {
|
||||||
|
if md == nil {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
var bfr bytes.Buffer
|
||||||
|
|
||||||
|
if err := md.SerializeTo(&bfr); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return bfr.Bytes(), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Equal returns true if a query expression is equal another one. Two query expressions are equal if they
|
||||||
|
// have the same query and placeholder values. This is resistant to map ordering.
|
||||||
|
func (md *QueryExpr) Equal(other *QueryExpr) bool {
|
||||||
|
if md == nil {
|
||||||
|
return other == nil
|
||||||
|
} else if other == nil {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
return md.ast.String() == other.ast.String() &&
|
||||||
|
maps.Equal(md.names, other.names) &&
|
||||||
|
maps.EqualFunc(md.values, md.values, attrutils.Equals)
|
||||||
|
}
|
||||||
|
|
||||||
|
// HashCode will return a hash-code for this query expression. This is to assist with determine whether two
|
||||||
|
// queries are the same. If two queries have the same hash code, they may be equals (this will need to be
|
||||||
|
// confirmed by calling Equal()). Otherwise, the queries cannot be equals.
|
||||||
|
func (md *QueryExpr) HashCode() uint64 {
|
||||||
|
if md == nil {
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
h := fnv.New64a()
|
||||||
|
h.Write([]byte(md.ast.String()))
|
||||||
|
|
||||||
|
// the names must be in sorted order to maintain consistant key ordering
|
||||||
|
if len(md.names) > 0 {
|
||||||
|
sortedKeys := make([]string, len(md.names))
|
||||||
|
copy(sortedKeys, maps.Keys(md.names))
|
||||||
|
slices.Sort(sortedKeys)
|
||||||
|
|
||||||
|
for _, k := range sortedKeys {
|
||||||
|
h.Write([]byte(k))
|
||||||
|
h.Write([]byte(md.names[k]))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(md.values) > 0 {
|
||||||
|
sortedKeys := make([]string, len(md.values))
|
||||||
|
copy(sortedKeys, maps.Keys(md.values))
|
||||||
|
slices.Sort(sortedKeys)
|
||||||
|
|
||||||
|
for _, k := range sortedKeys {
|
||||||
|
h.Write([]byte(k))
|
||||||
|
attrutils.HashTo(h, md.values[k])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return h.Sum64()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (md *QueryExpr) WithNameParams(value map[string]string) *QueryExpr {
|
||||||
|
return &QueryExpr{
|
||||||
|
ast: md.ast,
|
||||||
|
names: value,
|
||||||
|
values: md.values,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (md *QueryExpr) NameParam(name string) (string, bool) {
|
||||||
|
return md.evalContext().lookupName(name)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (md *QueryExpr) ValueParam(name string) (types.AttributeValue, bool) {
|
||||||
|
return md.evalContext().lookupValue(name)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (md *QueryExpr) ValueParamOrNil(name string) types.AttributeValue {
|
||||||
|
v, ok := md.ValueParam(name)
|
||||||
|
if !ok {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return v
|
||||||
|
}
|
||||||
|
|
||||||
|
func (md *QueryExpr) WithValueParams(value map[string]types.AttributeValue) *QueryExpr {
|
||||||
|
return &QueryExpr{
|
||||||
|
ast: md.ast,
|
||||||
|
names: md.names,
|
||||||
|
values: value,
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (md *QueryExpr) Plan(tableInfo *models.TableInfo) (*models.QueryExecutionPlan, error) {
|
func (md *QueryExpr) Plan(tableInfo *models.TableInfo) (*models.QueryExecutionPlan, error) {
|
||||||
return md.ast.calcQuery(tableInfo)
|
return md.ast.calcQuery(md.evalContext(), tableInfo)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (md *QueryExpr) EvalItem(item models.Item) (types.AttributeValue, error) {
|
func (md *QueryExpr) EvalItem(item models.Item) (types.AttributeValue, error) {
|
||||||
return md.ast.evalItem(item)
|
return md.ast.evalItem(md.evalContext(), item)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (md *QueryExpr) DeleteAttribute(item models.Item) error {
|
||||||
|
return md.ast.deleteAttribute(md.evalContext(), item)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (md *QueryExpr) SetEvalItem(item models.Item, newValue types.AttributeValue) error {
|
||||||
|
return md.ast.setEvalItem(md.evalContext(), item, newValue)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (md *QueryExpr) IsModifiablePath(item models.Item) bool {
|
||||||
|
return md.ast.canModifyItem(md.evalContext(), item)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (md *QueryExpr) evalContext() *evalContext {
|
||||||
|
return &evalContext{
|
||||||
|
namePlaceholders: md.names,
|
||||||
|
valuePlaceholders: md.values,
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (md *QueryExpr) String() string {
|
func (md *QueryExpr) String() string {
|
||||||
|
@ -57,3 +230,36 @@ func (qc *queryCalcInfo) addKey(tableInfo *models.TableInfo, key string) bool {
|
||||||
qc.seenKeys[key] = struct{}{}
|
qc.seenKeys[key] = struct{}{}
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type evalContext struct {
|
||||||
|
namePlaceholders map[string]string
|
||||||
|
nameLookup func(string) (string, bool)
|
||||||
|
valuePlaceholders map[string]types.AttributeValue
|
||||||
|
valueLookup func(string) (types.AttributeValue, bool)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ec *evalContext) lookupName(name string) (string, bool) {
|
||||||
|
val, hasVal := ec.namePlaceholders[name]
|
||||||
|
if hasVal {
|
||||||
|
return val, true
|
||||||
|
}
|
||||||
|
|
||||||
|
if fn := ec.nameLookup; fn != nil {
|
||||||
|
return fn(name)
|
||||||
|
}
|
||||||
|
|
||||||
|
return "", false
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ec *evalContext) lookupValue(name string) (types.AttributeValue, bool) {
|
||||||
|
val, hasVal := ec.valuePlaceholders[name]
|
||||||
|
if hasVal {
|
||||||
|
return val, true
|
||||||
|
}
|
||||||
|
|
||||||
|
if fn := ec.valueLookup; fn != nil {
|
||||||
|
return fn(name)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil, false
|
||||||
|
}
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
package queryexpr_test
|
package queryexpr_test
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"bytes"
|
||||||
"fmt"
|
"fmt"
|
||||||
"github.com/aws/aws-sdk-go-v2/aws"
|
"github.com/aws/aws-sdk-go-v2/aws"
|
||||||
"github.com/aws/aws-sdk-go-v2/service/dynamodb/types"
|
"github.com/aws/aws-sdk-go-v2/service/dynamodb/types"
|
||||||
|
@ -95,6 +96,21 @@ func TestModExpr_Query(t *testing.T) {
|
||||||
exprNameIsString(0, 0, "pk", "prefix"),
|
exprNameIsString(0, 0, "pk", "prefix"),
|
||||||
exprNameIsNumber(1, 1, "sk", "100"),
|
exprNameIsNumber(1, 1, "sk", "100"),
|
||||||
),
|
),
|
||||||
|
|
||||||
|
scanCase("with placeholders",
|
||||||
|
`:partition=$valuePrefix and :sort=$valueAnother`,
|
||||||
|
`(#0 = :0) AND (#1 = :1)`,
|
||||||
|
placeholderNames(map[string]string{
|
||||||
|
"partition": "pk",
|
||||||
|
"sort": "sk",
|
||||||
|
}),
|
||||||
|
placeholderValues(map[string]types.AttributeValue{
|
||||||
|
"valuePrefix": &types.AttributeValueMemberS{Value: "prefix"},
|
||||||
|
"valueAnother": &types.AttributeValueMemberS{Value: "another"},
|
||||||
|
}),
|
||||||
|
exprNameIsString(0, 0, "pk", "prefix"),
|
||||||
|
exprNameIsString(1, 1, "sk", "another"),
|
||||||
|
),
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, scenario := range scenarios {
|
for _, scenario := range scenarios {
|
||||||
|
@ -102,6 +118,8 @@ func TestModExpr_Query(t *testing.T) {
|
||||||
modExpr, err := queryexpr.Parse(scenario.expression)
|
modExpr, err := queryexpr.Parse(scenario.expression)
|
||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
|
|
||||||
|
modExpr = modExpr.WithNameParams(scenario.placeholderNames).WithValueParams(scenario.placeholderValues)
|
||||||
|
|
||||||
plan, err := modExpr.Plan(tableInfo)
|
plan, err := modExpr.Plan(tableInfo)
|
||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
|
|
||||||
|
@ -242,7 +260,37 @@ func TestModExpr_Query(t *testing.T) {
|
||||||
exprValueIsNumber(0, "131"),
|
exprValueIsNumber(0, "131"),
|
||||||
),
|
),
|
||||||
|
|
||||||
|
// Dots
|
||||||
|
scanCase("with the dot", `this.value = "something"`, `#0.#1 = :0`,
|
||||||
|
exprName(0, "this"),
|
||||||
|
exprName(1, "value"),
|
||||||
|
exprValueIsString(0, "something"),
|
||||||
|
),
|
||||||
|
scanCase("with multiple dots", `this.that.other.value = "else"`, `#0.#1.#2.#3 = :0`,
|
||||||
|
exprName(0, "this"),
|
||||||
|
exprName(1, "that"),
|
||||||
|
exprName(2, "other"),
|
||||||
|
exprName(3, "value"),
|
||||||
|
exprValueIsString(0, "else"),
|
||||||
|
),
|
||||||
|
|
||||||
// TODO: the contains function
|
// TODO: the contains function
|
||||||
|
|
||||||
|
// Placeholders
|
||||||
|
scanCase("with placeholders",
|
||||||
|
`:partition=$valuePrefix or :sort=$valueAnother`,
|
||||||
|
`(#0 = :0) OR (#1 = :1)`,
|
||||||
|
placeholderNames(map[string]string{
|
||||||
|
"partition": "pk",
|
||||||
|
"sort": "sk",
|
||||||
|
}),
|
||||||
|
placeholderValues(map[string]types.AttributeValue{
|
||||||
|
"valuePrefix": &types.AttributeValueMemberS{Value: "prefix"},
|
||||||
|
"valueAnother": &types.AttributeValueMemberS{Value: "another"},
|
||||||
|
}),
|
||||||
|
exprNameIsString(0, 0, "pk", "prefix"),
|
||||||
|
exprNameIsString(1, 1, "sk", "another"),
|
||||||
|
),
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, scenario := range scenarios {
|
for _, scenario := range scenarios {
|
||||||
|
@ -250,6 +298,8 @@ func TestModExpr_Query(t *testing.T) {
|
||||||
modExpr, err := queryexpr.Parse(scenario.expression)
|
modExpr, err := queryexpr.Parse(scenario.expression)
|
||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
|
|
||||||
|
modExpr = modExpr.WithNameParams(scenario.placeholderNames).WithValueParams(scenario.placeholderValues)
|
||||||
|
|
||||||
plan, err := modExpr.Plan(tableInfo)
|
plan, err := modExpr.Plan(tableInfo)
|
||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
|
|
||||||
|
@ -359,6 +409,7 @@ func TestQueryExpr_EvalItem(t *testing.T) {
|
||||||
|
|
||||||
// Dot values
|
// Dot values
|
||||||
{expr: `charlie.door`, expected: &types.AttributeValueMemberS{Value: "red"}},
|
{expr: `charlie.door`, expected: &types.AttributeValueMemberS{Value: "red"}},
|
||||||
|
{expr: `(charlie).door`, expected: &types.AttributeValueMemberS{Value: "red"}},
|
||||||
{expr: `charlie.tree`, expected: &types.AttributeValueMemberS{Value: "green"}},
|
{expr: `charlie.tree`, expected: &types.AttributeValueMemberS{Value: "green"}},
|
||||||
|
|
||||||
// Conjunction
|
// Conjunction
|
||||||
|
@ -434,6 +485,311 @@ func TestQueryExpr_EvalItem(t *testing.T) {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
t.Run("name and value placeholders", func(t *testing.T) {
|
||||||
|
scenarios := []struct {
|
||||||
|
expr string
|
||||||
|
expected types.AttributeValue
|
||||||
|
}{
|
||||||
|
{expr: `alpha = $a`, expected: &types.AttributeValueMemberBOOL{Value: true}},
|
||||||
|
{expr: `:theBName = 123`, expected: &types.AttributeValueMemberBOOL{Value: true}},
|
||||||
|
{expr: `:theCMap.door`, expected: &types.AttributeValueMemberS{Value: "red"}},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, scenario := range scenarios {
|
||||||
|
t.Run(scenario.expr, func(t *testing.T) {
|
||||||
|
modExpr, err := queryexpr.Parse(scenario.expr)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
|
||||||
|
modExpr = modExpr.WithValueParams(map[string]types.AttributeValue{
|
||||||
|
"a": &types.AttributeValueMemberS{Value: "alpha"},
|
||||||
|
}).WithNameParams(map[string]string{
|
||||||
|
"theBName": "bravo",
|
||||||
|
"theCMap": "charlie",
|
||||||
|
})
|
||||||
|
|
||||||
|
res, err := modExpr.EvalItem(item)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
|
||||||
|
assert.Equal(t, scenario.expected, res)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestQueryExpr_SetEvalItem(t *testing.T) {
|
||||||
|
var templateItem = func() models.Item {
|
||||||
|
return models.Item{
|
||||||
|
"alpha": &types.AttributeValueMemberS{Value: "alpha"},
|
||||||
|
"bravo": &types.AttributeValueMemberN{Value: "123"},
|
||||||
|
"charlie": &types.AttributeValueMemberM{
|
||||||
|
Value: map[string]types.AttributeValue{
|
||||||
|
"door": &types.AttributeValueMemberS{Value: "red"},
|
||||||
|
"tree": &types.AttributeValueMemberS{Value: "green"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"prime": &types.AttributeValueMemberL{
|
||||||
|
Value: []types.AttributeValue{
|
||||||
|
&types.AttributeValueMemberN{Value: "2"},
|
||||||
|
&types.AttributeValueMemberN{Value: "3"},
|
||||||
|
&types.AttributeValueMemberN{Value: "5"},
|
||||||
|
&types.AttributeValueMemberN{Value: "7"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"three": &types.AttributeValueMemberN{Value: "3"},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
t.Run("simple values", func(t *testing.T) {
|
||||||
|
item := templateItem()
|
||||||
|
|
||||||
|
modExpr, err := queryexpr.Parse("alpha")
|
||||||
|
assert.NoError(t, err)
|
||||||
|
assert.True(t, modExpr.IsModifiablePath(item))
|
||||||
|
|
||||||
|
err = modExpr.SetEvalItem(item, &types.AttributeValueMemberS{Value: "not alpha"})
|
||||||
|
assert.NoError(t, err)
|
||||||
|
assert.Equal(t, "not alpha", item["alpha"].(*types.AttributeValueMemberS).Value)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("dot values", func(t *testing.T) {
|
||||||
|
item := templateItem()
|
||||||
|
|
||||||
|
modExpr, err := queryexpr.Parse("charlie.tree")
|
||||||
|
assert.NoError(t, err)
|
||||||
|
assert.True(t, modExpr.IsModifiablePath(item))
|
||||||
|
|
||||||
|
err = modExpr.SetEvalItem(item, &types.AttributeValueMemberS{Value: "Birch"})
|
||||||
|
assert.NoError(t, err)
|
||||||
|
assert.Equal(t, "Birch", item["charlie"].(*types.AttributeValueMemberM).Value["tree"].(*types.AttributeValueMemberS).Value)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestQueryExpr_DeleteAttribute(t *testing.T) {
|
||||||
|
var templateItem = func() models.Item {
|
||||||
|
return models.Item{
|
||||||
|
"alpha": &types.AttributeValueMemberS{Value: "alpha"},
|
||||||
|
"bravo": &types.AttributeValueMemberN{Value: "123"},
|
||||||
|
"charlie": &types.AttributeValueMemberM{
|
||||||
|
Value: map[string]types.AttributeValue{
|
||||||
|
"door": &types.AttributeValueMemberS{Value: "red"},
|
||||||
|
"tree": &types.AttributeValueMemberS{Value: "green"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"prime": &types.AttributeValueMemberL{
|
||||||
|
Value: []types.AttributeValue{
|
||||||
|
&types.AttributeValueMemberN{Value: "2"},
|
||||||
|
&types.AttributeValueMemberN{Value: "3"},
|
||||||
|
&types.AttributeValueMemberN{Value: "5"},
|
||||||
|
&types.AttributeValueMemberN{Value: "7"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"three": &types.AttributeValueMemberN{Value: "3"},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
t.Run("simple values", func(t *testing.T) {
|
||||||
|
item := templateItem()
|
||||||
|
|
||||||
|
modExpr, err := queryexpr.Parse("alpha")
|
||||||
|
assert.NoError(t, err)
|
||||||
|
|
||||||
|
err = modExpr.DeleteAttribute(item)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
|
||||||
|
_, hasKey := item["alpha"]
|
||||||
|
assert.False(t, hasKey)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("placeholder values", func(t *testing.T) {
|
||||||
|
item := templateItem()
|
||||||
|
|
||||||
|
modExpr, err := queryexpr.Parse(":a")
|
||||||
|
assert.NoError(t, err)
|
||||||
|
|
||||||
|
modExpr = modExpr.WithNameParams(map[string]string{"a": "alpha"})
|
||||||
|
|
||||||
|
err = modExpr.DeleteAttribute(item)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
|
||||||
|
_, hasKey := item["alpha"]
|
||||||
|
assert.False(t, hasKey)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("dot values", func(t *testing.T) {
|
||||||
|
item := templateItem()
|
||||||
|
|
||||||
|
modExpr, err := queryexpr.Parse("charlie.tree")
|
||||||
|
assert.NoError(t, err)
|
||||||
|
|
||||||
|
err = modExpr.DeleteAttribute(item)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
|
||||||
|
_, hasKey := item["charlie"].(*types.AttributeValueMemberM).Value["tree"]
|
||||||
|
assert.False(t, hasKey)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("dot values with placeholders", func(t *testing.T) {
|
||||||
|
item := templateItem()
|
||||||
|
|
||||||
|
modExpr, err := queryexpr.Parse(":c.tree")
|
||||||
|
assert.NoError(t, err)
|
||||||
|
|
||||||
|
modExpr = modExpr.WithNameParams(map[string]string{"c": "charlie"})
|
||||||
|
|
||||||
|
err = modExpr.DeleteAttribute(item)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
|
||||||
|
_, hasKey := item["charlie"].(*types.AttributeValueMemberM).Value["tree"]
|
||||||
|
assert.False(t, hasKey)
|
||||||
|
})
|
||||||
|
|
||||||
|
//t.Run("dot values with multiple placeholders", func(t *testing.T) {
|
||||||
|
// item := templateItem()
|
||||||
|
//
|
||||||
|
// modExpr, err := queryexpr.Parse(":c.:t")
|
||||||
|
// assert.NoError(t, err)
|
||||||
|
//
|
||||||
|
// modExpr = modExpr.WithNameParams(map[string]string{
|
||||||
|
// "c": "charlie",
|
||||||
|
// "t": "tree",
|
||||||
|
// })
|
||||||
|
//
|
||||||
|
// err = modExpr.DeleteAttribute(item)
|
||||||
|
// assert.NoError(t, err)
|
||||||
|
//
|
||||||
|
// _, hasKey := item["charlie"].(*types.AttributeValueMemberM).Value["tree"]
|
||||||
|
// assert.False(t, hasKey)
|
||||||
|
//})
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestQueryExpr_SerializeTo(t *testing.T) {
|
||||||
|
t.Run("should be able to serialized and deseralize the parsed expression", func(t *testing.T) {
|
||||||
|
exprStr := `something = $value and :placeholder = "something else" and thirdThing in (1,2,3)`
|
||||||
|
|
||||||
|
bts := new(bytes.Buffer)
|
||||||
|
|
||||||
|
modExpr, err := queryexpr.Parse(exprStr)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
|
||||||
|
modExpr = modExpr.WithNameParams(map[string]string{
|
||||||
|
"placeholder": "some name",
|
||||||
|
}).WithValueParams(map[string]types.AttributeValue{
|
||||||
|
"value": &types.AttributeValueMemberS{Value: "some value"},
|
||||||
|
"num": &types.AttributeValueMemberN{Value: "12345"},
|
||||||
|
"veryLargeNumber": &types.AttributeValueMemberN{Value: "123456789012345678901234567890"},
|
||||||
|
"numberSet": &types.AttributeValueMemberNS{Value: []string{"123", "234", "345"}},
|
||||||
|
"bool": &types.AttributeValueMemberBOOL{Value: true},
|
||||||
|
"list": &types.AttributeValueMemberL{Value: []types.AttributeValue{
|
||||||
|
&types.AttributeValueMemberN{Value: "1"},
|
||||||
|
&types.AttributeValueMemberN{Value: "2"},
|
||||||
|
&types.AttributeValueMemberN{Value: "3"},
|
||||||
|
}},
|
||||||
|
"dict": &types.AttributeValueMemberM{
|
||||||
|
Value: map[string]types.AttributeValue{
|
||||||
|
"alpha": &types.AttributeValueMemberS{Value: "apple"},
|
||||||
|
"bravo": &types.AttributeValueMemberS{Value: "banana"},
|
||||||
|
"charlie": &types.AttributeValueMemberS{Value: "cherry"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
assert.NoError(t, modExpr.SerializeTo(bts))
|
||||||
|
|
||||||
|
newExpr, err := queryexpr.DeserializeFrom(bts)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
assert.Equal(t, modExpr.String(), newExpr.String())
|
||||||
|
|
||||||
|
name, hasName := newExpr.NameParam("placeholder")
|
||||||
|
assert.Equal(t, "some name", name)
|
||||||
|
assert.True(t, hasName)
|
||||||
|
|
||||||
|
assert.Equal(t, "some value", newExpr.ValueParamOrNil("value").(*types.AttributeValueMemberS).Value)
|
||||||
|
assert.Equal(t, "12345", newExpr.ValueParamOrNil("num").(*types.AttributeValueMemberN).Value)
|
||||||
|
assert.Equal(t, "123456789012345678901234567890", newExpr.ValueParamOrNil("veryLargeNumber").(*types.AttributeValueMemberN).Value)
|
||||||
|
assert.Equal(t, []string{"123", "234", "345"}, newExpr.ValueParamOrNil("numberSet").(*types.AttributeValueMemberNS).Value)
|
||||||
|
assert.Equal(t, true, newExpr.ValueParamOrNil("bool").(*types.AttributeValueMemberBOOL).Value)
|
||||||
|
assert.Equal(t, "1", newExpr.ValueParamOrNil("list").(*types.AttributeValueMemberL).Value[0].(*types.AttributeValueMemberN).Value)
|
||||||
|
assert.Equal(t, "2", newExpr.ValueParamOrNil("list").(*types.AttributeValueMemberL).Value[1].(*types.AttributeValueMemberN).Value)
|
||||||
|
assert.Equal(t, "3", newExpr.ValueParamOrNil("list").(*types.AttributeValueMemberL).Value[2].(*types.AttributeValueMemberN).Value)
|
||||||
|
assert.Equal(t, "apple", newExpr.ValueParamOrNil("dict").(*types.AttributeValueMemberM).Value["alpha"].(*types.AttributeValueMemberS).Value)
|
||||||
|
assert.Equal(t, "banana", newExpr.ValueParamOrNil("dict").(*types.AttributeValueMemberM).Value["bravo"].(*types.AttributeValueMemberS).Value)
|
||||||
|
assert.Equal(t, "cherry", newExpr.ValueParamOrNil("dict").(*types.AttributeValueMemberM).Value["charlie"].(*types.AttributeValueMemberS).Value)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestQueryExpr_Equals(t *testing.T) {
|
||||||
|
t.Run("should perform equals correctly", func(t *testing.T) {
|
||||||
|
exprStr := `something = $value and :placeholder = "something else" and thirdThing in (1,2,3)`
|
||||||
|
|
||||||
|
modExpr, _ := queryexpr.Parse(exprStr)
|
||||||
|
modExpr = modExpr.WithNameParams(map[string]string{
|
||||||
|
"placeholder": "some name",
|
||||||
|
"another": "name",
|
||||||
|
"more": "names",
|
||||||
|
}).WithValueParams(map[string]types.AttributeValue{
|
||||||
|
"value": &types.AttributeValueMemberS{Value: "some value"},
|
||||||
|
"num": &types.AttributeValueMemberN{Value: "12345"},
|
||||||
|
"veryLargeNumber": &types.AttributeValueMemberN{Value: "123456789012345678901234567890"},
|
||||||
|
"numberSet": &types.AttributeValueMemberNS{Value: []string{"123", "234", "345"}},
|
||||||
|
"bool": &types.AttributeValueMemberBOOL{Value: true},
|
||||||
|
"list": &types.AttributeValueMemberL{Value: []types.AttributeValue{
|
||||||
|
&types.AttributeValueMemberN{Value: "1"},
|
||||||
|
&types.AttributeValueMemberN{Value: "2"},
|
||||||
|
&types.AttributeValueMemberN{Value: "3"},
|
||||||
|
}},
|
||||||
|
"dict": &types.AttributeValueMemberM{
|
||||||
|
Value: map[string]types.AttributeValue{
|
||||||
|
"alpha": &types.AttributeValueMemberS{Value: "apple"},
|
||||||
|
"bravo": &types.AttributeValueMemberS{Value: "banana"},
|
||||||
|
"charlie": &types.AttributeValueMemberS{Value: "cherry"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
differentExpr, _ := queryexpr.Parse(`abc = :bla`)
|
||||||
|
differentExpr = modExpr.WithNameParams(map[string]string{
|
||||||
|
"fla": "some name",
|
||||||
|
}).WithValueParams(map[string]types.AttributeValue{
|
||||||
|
"value": &types.AttributeValueMemberS{Value: "some value"},
|
||||||
|
})
|
||||||
|
|
||||||
|
bts1, err := modExpr.SerializeToBytes()
|
||||||
|
assert.NoError(t, err)
|
||||||
|
|
||||||
|
expr2, err := queryexpr.DeserializeFrom(bytes.NewReader(bts1))
|
||||||
|
assert.NoError(t, err)
|
||||||
|
|
||||||
|
bts2, err := expr2.SerializeToBytes()
|
||||||
|
assert.NoError(t, err)
|
||||||
|
|
||||||
|
expr3, err := queryexpr.DeserializeFrom(bytes.NewReader(bts2))
|
||||||
|
assert.NoError(t, err)
|
||||||
|
|
||||||
|
_, err = expr3.SerializeToBytes()
|
||||||
|
assert.NoError(t, err)
|
||||||
|
|
||||||
|
var nilQE *queryexpr.QueryExpr
|
||||||
|
assert.True(t, nilQE.Equal(nil))
|
||||||
|
assert.True(t, modExpr.Equal(expr2))
|
||||||
|
assert.True(t, expr2.Equal(expr3))
|
||||||
|
assert.True(t, expr3.Equal(modExpr))
|
||||||
|
|
||||||
|
assert.False(t, nilQE.Equal(differentExpr))
|
||||||
|
assert.False(t, modExpr.Equal(differentExpr))
|
||||||
|
assert.False(t, expr2.Equal(differentExpr))
|
||||||
|
assert.False(t, expr3.Equal(differentExpr))
|
||||||
|
|
||||||
|
assert.Equal(t, uint64(0), nilQE.HashCode())
|
||||||
|
assert.Equal(t, modExpr.HashCode(), expr2.HashCode())
|
||||||
|
assert.Equal(t, expr2.HashCode(), expr3.HashCode())
|
||||||
|
assert.Equal(t, expr3.HashCode(), modExpr.HashCode())
|
||||||
|
|
||||||
|
assert.NotEqual(t, differentExpr.HashCode(), nilQE.HashCode())
|
||||||
|
assert.NotEqual(t, differentExpr.HashCode(), expr2.HashCode())
|
||||||
|
assert.NotEqual(t, differentExpr.HashCode(), expr3.HashCode())
|
||||||
|
assert.NotEqual(t, differentExpr.HashCode(), modExpr.HashCode())
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
type scanScenario struct {
|
type scanScenario struct {
|
||||||
|
@ -442,6 +798,8 @@ type scanScenario struct {
|
||||||
expectedFilter string
|
expectedFilter string
|
||||||
expectedNames map[string]string
|
expectedNames map[string]string
|
||||||
expectedValues map[string]types.AttributeValue
|
expectedValues map[string]types.AttributeValue
|
||||||
|
placeholderNames map[string]string
|
||||||
|
placeholderValues map[string]types.AttributeValue
|
||||||
}
|
}
|
||||||
|
|
||||||
func scanCase(description, expression, expectedFilter string, options ...func(ss *scanScenario)) scanScenario {
|
func scanCase(description, expression, expectedFilter string, options ...func(ss *scanScenario)) scanScenario {
|
||||||
|
@ -458,6 +816,18 @@ func scanCase(description, expression, expectedFilter string, options ...func(ss
|
||||||
return ss
|
return ss
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func placeholderNames(placeholderNames map[string]string) func(ss *scanScenario) {
|
||||||
|
return func(ss *scanScenario) {
|
||||||
|
ss.placeholderNames = placeholderNames
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func placeholderValues(placeholderValues map[string]types.AttributeValue) func(ss *scanScenario) {
|
||||||
|
return func(ss *scanScenario) {
|
||||||
|
ss.placeholderValues = placeholderValues
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func exprName(idx int, name string) func(ss *scanScenario) {
|
func exprName(idx int, name string) func(ss *scanScenario) {
|
||||||
return func(ss *scanScenario) {
|
return func(ss *scanScenario) {
|
||||||
ss.expectedNames[fmt.Sprintf("#%d", idx)] = name
|
ss.expectedNames[fmt.Sprintf("#%d", idx)] = name
|
||||||
|
|
|
@ -10,8 +10,8 @@ import (
|
||||||
"strings"
|
"strings"
|
||||||
)
|
)
|
||||||
|
|
||||||
func (a *astFunctionCall) evalToIR(info *models.TableInfo) (irAtom, error) {
|
func (a *astFunctionCall) evalToIR(ctx *evalContext, info *models.TableInfo) (irAtom, error) {
|
||||||
callerIr, err := a.Caller.evalToIR(info)
|
callerIr, err := a.Caller.evalToIR(ctx, info)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
@ -24,7 +24,7 @@ func (a *astFunctionCall) evalToIR(info *models.TableInfo) (irAtom, error) {
|
||||||
return nil, OperandNotANameError("")
|
return nil, OperandNotANameError("")
|
||||||
}
|
}
|
||||||
|
|
||||||
irNodes, err := sliceutils.MapWithError(a.Args, func(x *astExpr) (irAtom, error) { return x.evalToIR(info) })
|
irNodes, err := sliceutils.MapWithError(a.Args, func(x *astExpr) (irAtom, error) { return x.evalToIR(ctx, info) })
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
@ -53,9 +53,9 @@ func (a *astFunctionCall) evalToIR(info *models.TableInfo) (irAtom, error) {
|
||||||
return nil, UnrecognisedFunctionError{Name: nameIr.keyName()}
|
return nil, UnrecognisedFunctionError{Name: nameIr.keyName()}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (a *astFunctionCall) evalItem(item models.Item) (types.AttributeValue, error) {
|
func (a *astFunctionCall) evalItem(ctx *evalContext, item models.Item) (types.AttributeValue, error) {
|
||||||
if !a.IsCall {
|
if !a.IsCall {
|
||||||
return a.Caller.evalItem(item)
|
return a.Caller.evalItem(ctx, item)
|
||||||
}
|
}
|
||||||
|
|
||||||
name, isName := a.Caller.unqualifiedName()
|
name, isName := a.Caller.unqualifiedName()
|
||||||
|
@ -68,7 +68,7 @@ func (a *astFunctionCall) evalItem(item models.Item) (types.AttributeValue, erro
|
||||||
}
|
}
|
||||||
|
|
||||||
args, err := sliceutils.MapWithError(a.Args, func(a *astExpr) (types.AttributeValue, error) {
|
args, err := sliceutils.MapWithError(a.Args, func(a *astExpr) (types.AttributeValue, error) {
|
||||||
return a.evalItem(item)
|
return a.evalItem(ctx, item)
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
|
@ -77,6 +77,30 @@ func (a *astFunctionCall) evalItem(item models.Item) (types.AttributeValue, erro
|
||||||
return fn(context.Background(), args)
|
return fn(context.Background(), args)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (a *astFunctionCall) canModifyItem(ctx *evalContext, item models.Item) bool {
|
||||||
|
// TODO: Should a function vall return an item?
|
||||||
|
if a.IsCall {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
return a.Caller.canModifyItem(ctx, item)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *astFunctionCall) setEvalItem(ctx *evalContext, item models.Item, value types.AttributeValue) error {
|
||||||
|
// TODO: Should a function vall return an item?
|
||||||
|
if a.IsCall {
|
||||||
|
return PathNotSettableError{}
|
||||||
|
}
|
||||||
|
return a.Caller.setEvalItem(ctx, item, value)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *astFunctionCall) deleteAttribute(ctx *evalContext, item models.Item) error {
|
||||||
|
// TODO: Should a function vall return an item?
|
||||||
|
if a.IsCall {
|
||||||
|
return PathNotSettableError{}
|
||||||
|
}
|
||||||
|
return a.Caller.deleteAttribute(ctx, item)
|
||||||
|
}
|
||||||
|
|
||||||
func (a *astFunctionCall) String() string {
|
func (a *astFunctionCall) String() string {
|
||||||
var sb strings.Builder
|
var sb strings.Builder
|
||||||
|
|
||||||
|
|
|
@ -12,8 +12,8 @@ import (
|
||||||
"strings"
|
"strings"
|
||||||
)
|
)
|
||||||
|
|
||||||
func (a *astIn) evalToIR(info *models.TableInfo) (irAtom, error) {
|
func (a *astIn) evalToIR(ctx *evalContext, info *models.TableInfo) (irAtom, error) {
|
||||||
leftIR, err := a.Ref.evalToIR(info)
|
leftIR, err := a.Ref.evalToIR(ctx, info)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
@ -32,7 +32,7 @@ func (a *astIn) evalToIR(info *models.TableInfo) (irAtom, error) {
|
||||||
|
|
||||||
oprValues := make([]oprIRAtom, len(a.Operand))
|
oprValues := make([]oprIRAtom, len(a.Operand))
|
||||||
for i, o := range a.Operand {
|
for i, o := range a.Operand {
|
||||||
v, err := o.evalToIR(info)
|
v, err := o.evalToIR(ctx, info)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
@ -59,7 +59,7 @@ func (a *astIn) evalToIR(info *models.TableInfo) (irAtom, error) {
|
||||||
|
|
||||||
ir = irIn{name: nameIR, values: oprValues}
|
ir = irIn{name: nameIR, values: oprValues}
|
||||||
case a.SingleOperand != nil:
|
case a.SingleOperand != nil:
|
||||||
oprs, err := a.SingleOperand.evalToIR(info)
|
oprs, err := a.SingleOperand.evalToIR(ctx, info)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
@ -96,8 +96,8 @@ func (a *astIn) evalToIR(info *models.TableInfo) (irAtom, error) {
|
||||||
return ir, nil
|
return ir, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (a *astIn) evalItem(item models.Item) (types.AttributeValue, error) {
|
func (a *astIn) evalItem(ctx *evalContext, item models.Item) (types.AttributeValue, error) {
|
||||||
val, err := a.Ref.evalItem(item)
|
val, err := a.Ref.evalItem(ctx, item)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
@ -108,7 +108,7 @@ func (a *astIn) evalItem(item models.Item) (types.AttributeValue, error) {
|
||||||
switch {
|
switch {
|
||||||
case len(a.Operand) > 0:
|
case len(a.Operand) > 0:
|
||||||
for _, opr := range a.Operand {
|
for _, opr := range a.Operand {
|
||||||
evalOp, err := opr.evalItem(item)
|
evalOp, err := opr.evalItem(ctx, item)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
@ -121,7 +121,7 @@ func (a *astIn) evalItem(item models.Item) (types.AttributeValue, error) {
|
||||||
}
|
}
|
||||||
return &types.AttributeValueMemberBOOL{Value: false}, nil
|
return &types.AttributeValueMemberBOOL{Value: false}, nil
|
||||||
case a.SingleOperand != nil:
|
case a.SingleOperand != nil:
|
||||||
evalOp, err := a.SingleOperand.evalItem(item)
|
evalOp, err := a.SingleOperand.evalItem(ctx, item)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
@ -194,6 +194,28 @@ func (a *astIn) evalItem(item models.Item) (types.AttributeValue, error) {
|
||||||
return nil, errors.New("internal error: unhandled 'in' case")
|
return nil, errors.New("internal error: unhandled 'in' case")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (a *astIn) canModifyItem(ctx *evalContext, item models.Item) bool {
|
||||||
|
if len(a.Operand) != 0 || a.SingleOperand != nil {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
return a.Ref.canModifyItem(ctx, item)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *astIn) setEvalItem(ctx *evalContext, item models.Item, value types.AttributeValue) error {
|
||||||
|
if len(a.Operand) != 0 || a.SingleOperand != nil {
|
||||||
|
return PathNotSettableError{}
|
||||||
|
}
|
||||||
|
return a.Ref.setEvalItem(ctx, item, value)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *astIn) deleteAttribute(ctx *evalContext, item models.Item) error {
|
||||||
|
if len(a.Operand) != 0 || a.SingleOperand != nil {
|
||||||
|
return PathNotSettableError{}
|
||||||
|
}
|
||||||
|
return a.Ref.deleteAttribute(ctx, item)
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
func (a *astIn) String() string {
|
func (a *astIn) String() string {
|
||||||
if len(a.Operand) == 0 && a.SingleOperand == nil {
|
if len(a.Operand) == 0 && a.SingleOperand == nil {
|
||||||
return a.Ref.String()
|
return a.Ref.String()
|
||||||
|
|
|
@ -59,8 +59,8 @@ var validIsTypeNames = map[string]isTypeInfo{
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
func (a *astIsOp) evalToIR(info *models.TableInfo) (irAtom, error) {
|
func (a *astIsOp) evalToIR(ctx *evalContext, info *models.TableInfo) (irAtom, error) {
|
||||||
leftIR, err := a.Ref.evalToIR(info)
|
leftIR, err := a.Ref.evalToIR(ctx, info)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
@ -74,7 +74,7 @@ func (a *astIsOp) evalToIR(info *models.TableInfo) (irAtom, error) {
|
||||||
return nil, OperandNotANameError(a.Ref.String())
|
return nil, OperandNotANameError(a.Ref.String())
|
||||||
}
|
}
|
||||||
|
|
||||||
rightIR, err := a.Value.evalToIR(info)
|
rightIR, err := a.Value.evalToIR(ctx, info)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
@ -104,8 +104,8 @@ func (a *astIsOp) evalToIR(info *models.TableInfo) (irAtom, error) {
|
||||||
return ir, nil
|
return ir, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (a *astIsOp) evalItem(item models.Item) (types.AttributeValue, error) {
|
func (a *astIsOp) evalItem(ctx *evalContext, item models.Item) (types.AttributeValue, error) {
|
||||||
ref, err := a.Ref.evalItem(item)
|
ref, err := a.Ref.evalItem(ctx, item)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
@ -114,7 +114,7 @@ func (a *astIsOp) evalItem(item models.Item) (types.AttributeValue, error) {
|
||||||
return ref, nil
|
return ref, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
expTypeVal, err := a.Value.evalItem(item)
|
expTypeVal, err := a.Value.evalItem(ctx, item)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
@ -140,6 +140,27 @@ func (a *astIsOp) evalItem(item models.Item) (types.AttributeValue, error) {
|
||||||
return &types.AttributeValueMemberBOOL{Value: resultOfIs}, nil
|
return &types.AttributeValueMemberBOOL{Value: resultOfIs}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (a *astIsOp) canModifyItem(ctx *evalContext, item models.Item) bool {
|
||||||
|
if a.Value != nil {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
return a.Ref.canModifyItem(ctx, item)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *astIsOp) setEvalItem(ctx *evalContext, item models.Item, value types.AttributeValue) error {
|
||||||
|
if a.Value != nil {
|
||||||
|
return PathNotSettableError{}
|
||||||
|
}
|
||||||
|
return a.Ref.setEvalItem(ctx, item, value)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *astIsOp) deleteAttribute(ctx *evalContext, item models.Item) error {
|
||||||
|
if a.Value != nil {
|
||||||
|
return PathNotSettableError{}
|
||||||
|
}
|
||||||
|
return a.Ref.deleteAttribute(ctx, item)
|
||||||
|
}
|
||||||
|
|
||||||
func (a *astIsOp) String() string {
|
func (a *astIsOp) String() string {
|
||||||
var sb strings.Builder
|
var sb strings.Builder
|
||||||
|
|
||||||
|
|
109
internal/dynamo-browse/models/queryexpr/placeholder.go
Normal file
109
internal/dynamo-browse/models/queryexpr/placeholder.go
Normal file
|
@ -0,0 +1,109 @@
|
||||||
|
package queryexpr
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/aws/aws-sdk-go-v2/service/dynamodb/types"
|
||||||
|
"github.com/lmika/audax/internal/dynamo-browse/models"
|
||||||
|
"github.com/pkg/errors"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
valuePlaceholderPrefix = '$'
|
||||||
|
namePlaceholderPrefix = ':'
|
||||||
|
)
|
||||||
|
|
||||||
|
func (p *astPlaceholder) evalToIR(ctx *evalContext, info *models.TableInfo) (irAtom, error) {
|
||||||
|
placeholderType := p.Placeholder[0]
|
||||||
|
placeholder := p.Placeholder[1:]
|
||||||
|
|
||||||
|
if placeholderType == valuePlaceholderPrefix {
|
||||||
|
val, hasVal := ctx.lookupValue(placeholder)
|
||||||
|
if !hasVal {
|
||||||
|
return nil, MissingPlaceholderError{Placeholder: p.Placeholder}
|
||||||
|
}
|
||||||
|
|
||||||
|
return irValue{value: val}, nil
|
||||||
|
} else if placeholderType == namePlaceholderPrefix {
|
||||||
|
name, hasName := ctx.lookupName(placeholder)
|
||||||
|
if !hasName {
|
||||||
|
return nil, MissingPlaceholderError{Placeholder: p.Placeholder}
|
||||||
|
}
|
||||||
|
|
||||||
|
return irNamePath{name, nil}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil, errors.New("unrecognised placeholder")
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *astPlaceholder) evalItem(ctx *evalContext, item models.Item) (types.AttributeValue, error) {
|
||||||
|
placeholderType := p.Placeholder[0]
|
||||||
|
placeholder := p.Placeholder[1:]
|
||||||
|
|
||||||
|
if placeholderType == valuePlaceholderPrefix {
|
||||||
|
val, hasVal := ctx.lookupValue(placeholder)
|
||||||
|
if !hasVal {
|
||||||
|
return nil, MissingPlaceholderError{Placeholder: p.Placeholder}
|
||||||
|
}
|
||||||
|
return val, nil
|
||||||
|
} else if placeholderType == namePlaceholderPrefix {
|
||||||
|
name, hasName := ctx.lookupName(placeholder)
|
||||||
|
if !hasName {
|
||||||
|
return nil, MissingPlaceholderError{Placeholder: p.Placeholder}
|
||||||
|
}
|
||||||
|
|
||||||
|
res, hasV := item[name]
|
||||||
|
if !hasV {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return res, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil, errors.New("unrecognised placeholder")
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *astPlaceholder) canModifyItem(ctx *evalContext, item models.Item) bool {
|
||||||
|
placeholderType := p.Placeholder[0]
|
||||||
|
return placeholderType == namePlaceholderPrefix
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *astPlaceholder) setEvalItem(ctx *evalContext, item models.Item, value types.AttributeValue) error {
|
||||||
|
placeholderType := p.Placeholder[0]
|
||||||
|
placeholder := p.Placeholder[1:]
|
||||||
|
|
||||||
|
if placeholderType == valuePlaceholderPrefix {
|
||||||
|
return PathNotSettableError{}
|
||||||
|
} else if placeholderType == namePlaceholderPrefix {
|
||||||
|
name, hasName := ctx.lookupName(placeholder)
|
||||||
|
if !hasName {
|
||||||
|
return MissingPlaceholderError{Placeholder: p.Placeholder}
|
||||||
|
}
|
||||||
|
|
||||||
|
item[name] = value
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return errors.New("unrecognised placeholder")
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *astPlaceholder) deleteAttribute(ctx *evalContext, item models.Item) error {
|
||||||
|
placeholderType := p.Placeholder[0]
|
||||||
|
placeholder := p.Placeholder[1:]
|
||||||
|
|
||||||
|
if placeholderType == valuePlaceholderPrefix {
|
||||||
|
return PathNotSettableError{}
|
||||||
|
} else if placeholderType == namePlaceholderPrefix {
|
||||||
|
name, hasName := ctx.lookupName(placeholder)
|
||||||
|
if !hasName {
|
||||||
|
return MissingPlaceholderError{Placeholder: p.Placeholder}
|
||||||
|
}
|
||||||
|
|
||||||
|
delete(item, name)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return errors.New("unrecognised placeholder")
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *astPlaceholder) String() string {
|
||||||
|
return p.Placeholder
|
||||||
|
}
|
118
internal/dynamo-browse/models/queryexpr/subref.go
Normal file
118
internal/dynamo-browse/models/queryexpr/subref.go
Normal file
|
@ -0,0 +1,118 @@
|
||||||
|
package queryexpr
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/aws/aws-sdk-go-v2/service/dynamodb/types"
|
||||||
|
"github.com/lmika/audax/internal/dynamo-browse/models"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
func (r *astSubRef) evalToIR(ctx *evalContext, info *models.TableInfo) (irAtom, error) {
|
||||||
|
refIR, err := r.Ref.evalToIR(ctx, info)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if len(r.Quals) == 0 {
|
||||||
|
return refIR, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// This node has subrefs
|
||||||
|
namePath, isNamePath := refIR.(irNamePath)
|
||||||
|
if !isNamePath {
|
||||||
|
return nil, OperandNotANameError(r.String())
|
||||||
|
}
|
||||||
|
|
||||||
|
quals := make([]string, 0)
|
||||||
|
for _, sr := range r.Quals {
|
||||||
|
quals = append(quals, sr)
|
||||||
|
}
|
||||||
|
return irNamePath{name: namePath.name, quals: quals}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *astSubRef) evalItem(ctx *evalContext, item models.Item) (types.AttributeValue, error) {
|
||||||
|
res, err := r.Ref.evalItem(ctx, item)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
for i, qualName := range r.Quals {
|
||||||
|
var hasV bool
|
||||||
|
|
||||||
|
mapRes, isMapRes := res.(*types.AttributeValueMemberM)
|
||||||
|
if !isMapRes {
|
||||||
|
return nil, ValueNotAMapError(append([]string{r.Ref.String()}, r.Quals[:i+1]...))
|
||||||
|
}
|
||||||
|
|
||||||
|
res, hasV = mapRes.Value[qualName]
|
||||||
|
if !hasV {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return res, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
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 {
|
||||||
|
if len(r.Quals) == 0 {
|
||||||
|
return r.Ref.setEvalItem(ctx, item, value)
|
||||||
|
}
|
||||||
|
|
||||||
|
parentItem, err := r.Ref.evalItem(ctx, item)
|
||||||
|
if err != nil {
|
||||||
|
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 {
|
||||||
|
mapItem.Value[key] = value
|
||||||
|
} else {
|
||||||
|
parentItem = mapItem.Value[key]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *astSubRef) deleteAttribute(ctx *evalContext, item models.Item) error {
|
||||||
|
if len(r.Quals) == 0 {
|
||||||
|
return r.Ref.deleteAttribute(ctx, item)
|
||||||
|
}
|
||||||
|
|
||||||
|
parentItem, err := r.Ref.evalItem(ctx, item)
|
||||||
|
if err != nil {
|
||||||
|
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]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *astSubRef) String() string {
|
||||||
|
var sb strings.Builder
|
||||||
|
|
||||||
|
sb.WriteString(r.Ref.String())
|
||||||
|
for _, q := range r.Quals {
|
||||||
|
sb.WriteRune('.')
|
||||||
|
sb.WriteString(q)
|
||||||
|
}
|
||||||
|
|
||||||
|
return sb.String()
|
||||||
|
}
|
|
@ -9,7 +9,7 @@ import (
|
||||||
"github.com/pkg/errors"
|
"github.com/pkg/errors"
|
||||||
)
|
)
|
||||||
|
|
||||||
func (a *astLiteralValue) evalToIR(info *models.TableInfo) (irAtom, error) {
|
func (a *astLiteralValue) evalToIR(ctx *evalContext, info *models.TableInfo) (irAtom, error) {
|
||||||
v, err := a.goValue()
|
v, err := a.goValue()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
|
|
|
@ -1,6 +1,8 @@
|
||||||
package serialisable
|
package serialisable
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"bytes"
|
||||||
|
"github.com/lmika/audax/internal/dynamo-browse/models/queryexpr"
|
||||||
"time"
|
"time"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -14,6 +16,32 @@ type ViewSnapshot struct {
|
||||||
|
|
||||||
type ViewSnapshotDetails struct {
|
type ViewSnapshotDetails struct {
|
||||||
TableName string
|
TableName string
|
||||||
Query string
|
Query []byte
|
||||||
|
QueryHash uint64
|
||||||
Filter string
|
Filter string
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (d ViewSnapshotDetails) Equals(other ViewSnapshotDetails, compareHashesOnly bool) bool {
|
||||||
|
return d.TableName == other.TableName &&
|
||||||
|
d.Filter == other.Filter &&
|
||||||
|
d.compareQueries(other, compareHashesOnly)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d ViewSnapshotDetails) compareQueries(other ViewSnapshotDetails, compareHashesOnly bool) bool {
|
||||||
|
if d.QueryHash != other.QueryHash {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
if compareHashesOnly {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
expr1, err := queryexpr.DeserializeFrom(bytes.NewReader(d.Query))
|
||||||
|
if err != nil {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
expr2, err := queryexpr.DeserializeFrom(bytes.NewReader(other.Query))
|
||||||
|
if err != nil {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
return expr1.Equal(expr2)
|
||||||
|
}
|
||||||
|
|
|
@ -4,7 +4,11 @@ import (
|
||||||
"github.com/asdine/storm"
|
"github.com/asdine/storm"
|
||||||
"github.com/lmika/audax/internal/common/workspaces"
|
"github.com/lmika/audax/internal/common/workspaces"
|
||||||
"github.com/pkg/errors"
|
"github.com/pkg/errors"
|
||||||
|
"io/fs"
|
||||||
"log"
|
"log"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"strings"
|
||||||
)
|
)
|
||||||
|
|
||||||
const settingBucket = "Settings"
|
const settingBucket = "Settings"
|
||||||
|
@ -12,8 +16,10 @@ const settingBucket = "Settings"
|
||||||
const (
|
const (
|
||||||
keyTableReadOnly = "ro"
|
keyTableReadOnly = "ro"
|
||||||
keyTableDefaultLimit = "default_limit"
|
keyTableDefaultLimit = "default_limit"
|
||||||
|
keyScriptLookupPath = "script_lookup_path"
|
||||||
|
|
||||||
defaultsDefaultLimit = 1000
|
defaultsDefaultLimit = 1000
|
||||||
|
defaultScriptLookupPaths = "${HOME}/.config/audax/dynamo-browse/scripts"
|
||||||
)
|
)
|
||||||
|
|
||||||
type SettingStore struct {
|
type SettingStore struct {
|
||||||
|
@ -26,6 +32,48 @@ func New(ws *workspaces.Workspace) *SettingStore {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (c *SettingStore) SetScriptLookupPaths(value string) error {
|
||||||
|
return c.ws.Set(settingBucket, keyTableReadOnly, value)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *SettingStore) ScriptLookupFS() ([]fs.FS, error) {
|
||||||
|
paths, err := c.getStringValue(keyScriptLookupPath, defaultScriptLookupPaths)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if paths == "" {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
fs := make([]fs.FS, 0, len(paths))
|
||||||
|
for _, path := range strings.Split(paths, string(os.PathListSeparator)) {
|
||||||
|
expandedPath := os.ExpandEnv(path)
|
||||||
|
|
||||||
|
var absPath string
|
||||||
|
if filepath.IsAbs(path) {
|
||||||
|
absPath = expandedPath
|
||||||
|
} else {
|
||||||
|
absPath, err = filepath.Abs(expandedPath)
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("warn: cannot include script lookup path '%v': %v", expandedPath, err)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if stat, err := os.Stat(absPath); err != nil {
|
||||||
|
log.Printf("warn: cannot stat script lookup path '%v': %v", expandedPath, err)
|
||||||
|
} else if stat.IsDir() {
|
||||||
|
log.Printf("warn: script lookup path '%v' is not a directory", expandedPath)
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Printf("adding script lookup path: %v", absPath)
|
||||||
|
fs = append(fs, os.DirFS(absPath))
|
||||||
|
}
|
||||||
|
|
||||||
|
return fs, nil
|
||||||
|
}
|
||||||
|
|
||||||
func (c *SettingStore) IsReadOnly() (b bool, err error) {
|
func (c *SettingStore) IsReadOnly() (b bool, err error) {
|
||||||
if err := c.ws.Get(settingBucket, keyTableReadOnly, &b); err != nil {
|
if err := c.ws.Get(settingBucket, keyTableReadOnly, &b); err != nil {
|
||||||
if errors.Is(err, storm.ErrNotFound) {
|
if errors.Is(err, storm.ErrNotFound) {
|
||||||
|
@ -54,3 +102,14 @@ func (c *SettingStore) DefaultLimit() (limit int) {
|
||||||
func (c *SettingStore) SetDefaultLimit(limit int) error {
|
func (c *SettingStore) SetDefaultLimit(limit int) error {
|
||||||
return errors.Wrapf(c.ws.Set(settingBucket, keyTableDefaultLimit, &limit), "cannot set default limit to %v", limit)
|
return errors.Wrapf(c.ws.Set(settingBucket, keyTableDefaultLimit, &limit), "cannot set default limit to %v", limit)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (c *SettingStore) getStringValue(key string, def string) (string, error) {
|
||||||
|
var val string
|
||||||
|
if err := c.ws.Get(settingBucket, keyTableReadOnly, &val); err != nil {
|
||||||
|
if errors.Is(err, storm.ErrNotFound) {
|
||||||
|
return def, nil
|
||||||
|
}
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
return val, nil
|
||||||
|
}
|
||||||
|
|
36
internal/dynamo-browse/services/scriptmanager/iface.go
Normal file
36
internal/dynamo-browse/services/scriptmanager/iface.go
Normal file
|
@ -0,0 +1,36 @@
|
||||||
|
package scriptmanager
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"github.com/aws/aws-sdk-go-v2/service/dynamodb/types"
|
||||||
|
"github.com/lmika/audax/internal/dynamo-browse/models"
|
||||||
|
)
|
||||||
|
|
||||||
|
//go:generate mockery --with-expecter --name UIService
|
||||||
|
//go:generate mockery --with-expecter --name SessionService
|
||||||
|
|
||||||
|
type Ifaces struct {
|
||||||
|
UI UIService
|
||||||
|
Session SessionService
|
||||||
|
}
|
||||||
|
|
||||||
|
type UIService interface {
|
||||||
|
PrintMessage(ctx context.Context, msg string)
|
||||||
|
|
||||||
|
// Prompt should return a channel which will provide the input from the user. If the user
|
||||||
|
// provides no input, prompt should close the channel without providing anything.
|
||||||
|
Prompt(ctx context.Context, msg string) chan string
|
||||||
|
}
|
||||||
|
|
||||||
|
type SessionService interface {
|
||||||
|
Query(ctx context.Context, expr string, queryOptions QueryOptions) (*models.ResultSet, error)
|
||||||
|
|
||||||
|
ResultSet(ctx context.Context) *models.ResultSet
|
||||||
|
SelectedItemIndex(ctx context.Context) int
|
||||||
|
SetResultSet(ctx context.Context, newResultSet *models.ResultSet)
|
||||||
|
}
|
||||||
|
|
||||||
|
type QueryOptions struct {
|
||||||
|
NamePlaceholders map[string]string
|
||||||
|
ValuePlaceholders map[string]types.AttributeValue
|
||||||
|
}
|
|
@ -0,0 +1,193 @@
|
||||||
|
// Code generated by mockery v2.16.0. DO NOT EDIT.
|
||||||
|
|
||||||
|
package mocks
|
||||||
|
|
||||||
|
import (
|
||||||
|
context "context"
|
||||||
|
|
||||||
|
models "github.com/lmika/audax/internal/dynamo-browse/models"
|
||||||
|
mock "github.com/stretchr/testify/mock"
|
||||||
|
|
||||||
|
scriptmanager "github.com/lmika/audax/internal/dynamo-browse/services/scriptmanager"
|
||||||
|
)
|
||||||
|
|
||||||
|
// SessionService is an autogenerated mock type for the SessionService type
|
||||||
|
type SessionService struct {
|
||||||
|
mock.Mock
|
||||||
|
}
|
||||||
|
|
||||||
|
type SessionService_Expecter struct {
|
||||||
|
mock *mock.Mock
|
||||||
|
}
|
||||||
|
|
||||||
|
func (_m *SessionService) EXPECT() *SessionService_Expecter {
|
||||||
|
return &SessionService_Expecter{mock: &_m.Mock}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Query provides a mock function with given fields: ctx, expr, queryOptions
|
||||||
|
func (_m *SessionService) Query(ctx context.Context, expr string, queryOptions scriptmanager.QueryOptions) (*models.ResultSet, error) {
|
||||||
|
ret := _m.Called(ctx, expr, queryOptions)
|
||||||
|
|
||||||
|
var r0 *models.ResultSet
|
||||||
|
if rf, ok := ret.Get(0).(func(context.Context, string, scriptmanager.QueryOptions) *models.ResultSet); ok {
|
||||||
|
r0 = rf(ctx, expr, queryOptions)
|
||||||
|
} else {
|
||||||
|
if ret.Get(0) != nil {
|
||||||
|
r0 = ret.Get(0).(*models.ResultSet)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var r1 error
|
||||||
|
if rf, ok := ret.Get(1).(func(context.Context, string, scriptmanager.QueryOptions) error); ok {
|
||||||
|
r1 = rf(ctx, expr, queryOptions)
|
||||||
|
} else {
|
||||||
|
r1 = ret.Error(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
return r0, r1
|
||||||
|
}
|
||||||
|
|
||||||
|
// SessionService_Query_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Query'
|
||||||
|
type SessionService_Query_Call struct {
|
||||||
|
*mock.Call
|
||||||
|
}
|
||||||
|
|
||||||
|
// Query is a helper method to define mock.On call
|
||||||
|
// - ctx context.Context
|
||||||
|
// - expr string
|
||||||
|
// - queryOptions scriptmanager.QueryOptions
|
||||||
|
func (_e *SessionService_Expecter) Query(ctx interface{}, expr interface{}, queryOptions interface{}) *SessionService_Query_Call {
|
||||||
|
return &SessionService_Query_Call{Call: _e.mock.On("Query", ctx, expr, queryOptions)}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (_c *SessionService_Query_Call) Run(run func(ctx context.Context, expr string, queryOptions scriptmanager.QueryOptions)) *SessionService_Query_Call {
|
||||||
|
_c.Call.Run(func(args mock.Arguments) {
|
||||||
|
run(args[0].(context.Context), args[1].(string), args[2].(scriptmanager.QueryOptions))
|
||||||
|
})
|
||||||
|
return _c
|
||||||
|
}
|
||||||
|
|
||||||
|
func (_c *SessionService_Query_Call) Return(_a0 *models.ResultSet, _a1 error) *SessionService_Query_Call {
|
||||||
|
_c.Call.Return(_a0, _a1)
|
||||||
|
return _c
|
||||||
|
}
|
||||||
|
|
||||||
|
// ResultSet provides a mock function with given fields: ctx
|
||||||
|
func (_m *SessionService) ResultSet(ctx context.Context) *models.ResultSet {
|
||||||
|
ret := _m.Called(ctx)
|
||||||
|
|
||||||
|
var r0 *models.ResultSet
|
||||||
|
if rf, ok := ret.Get(0).(func(context.Context) *models.ResultSet); ok {
|
||||||
|
r0 = rf(ctx)
|
||||||
|
} else {
|
||||||
|
if ret.Get(0) != nil {
|
||||||
|
r0 = ret.Get(0).(*models.ResultSet)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return r0
|
||||||
|
}
|
||||||
|
|
||||||
|
// SessionService_ResultSet_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'ResultSet'
|
||||||
|
type SessionService_ResultSet_Call struct {
|
||||||
|
*mock.Call
|
||||||
|
}
|
||||||
|
|
||||||
|
// ResultSet is a helper method to define mock.On call
|
||||||
|
// - ctx context.Context
|
||||||
|
func (_e *SessionService_Expecter) ResultSet(ctx interface{}) *SessionService_ResultSet_Call {
|
||||||
|
return &SessionService_ResultSet_Call{Call: _e.mock.On("ResultSet", ctx)}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (_c *SessionService_ResultSet_Call) Run(run func(ctx context.Context)) *SessionService_ResultSet_Call {
|
||||||
|
_c.Call.Run(func(args mock.Arguments) {
|
||||||
|
run(args[0].(context.Context))
|
||||||
|
})
|
||||||
|
return _c
|
||||||
|
}
|
||||||
|
|
||||||
|
func (_c *SessionService_ResultSet_Call) Return(_a0 *models.ResultSet) *SessionService_ResultSet_Call {
|
||||||
|
_c.Call.Return(_a0)
|
||||||
|
return _c
|
||||||
|
}
|
||||||
|
|
||||||
|
// SelectedItemIndex provides a mock function with given fields: ctx
|
||||||
|
func (_m *SessionService) SelectedItemIndex(ctx context.Context) int {
|
||||||
|
ret := _m.Called(ctx)
|
||||||
|
|
||||||
|
var r0 int
|
||||||
|
if rf, ok := ret.Get(0).(func(context.Context) int); ok {
|
||||||
|
r0 = rf(ctx)
|
||||||
|
} else {
|
||||||
|
r0 = ret.Get(0).(int)
|
||||||
|
}
|
||||||
|
|
||||||
|
return r0
|
||||||
|
}
|
||||||
|
|
||||||
|
// SessionService_SelectedItemIndex_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'SelectedItemIndex'
|
||||||
|
type SessionService_SelectedItemIndex_Call struct {
|
||||||
|
*mock.Call
|
||||||
|
}
|
||||||
|
|
||||||
|
// SelectedItemIndex is a helper method to define mock.On call
|
||||||
|
// - ctx context.Context
|
||||||
|
func (_e *SessionService_Expecter) SelectedItemIndex(ctx interface{}) *SessionService_SelectedItemIndex_Call {
|
||||||
|
return &SessionService_SelectedItemIndex_Call{Call: _e.mock.On("SelectedItemIndex", ctx)}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (_c *SessionService_SelectedItemIndex_Call) Run(run func(ctx context.Context)) *SessionService_SelectedItemIndex_Call {
|
||||||
|
_c.Call.Run(func(args mock.Arguments) {
|
||||||
|
run(args[0].(context.Context))
|
||||||
|
})
|
||||||
|
return _c
|
||||||
|
}
|
||||||
|
|
||||||
|
func (_c *SessionService_SelectedItemIndex_Call) Return(_a0 int) *SessionService_SelectedItemIndex_Call {
|
||||||
|
_c.Call.Return(_a0)
|
||||||
|
return _c
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetResultSet provides a mock function with given fields: ctx, newResultSet
|
||||||
|
func (_m *SessionService) SetResultSet(ctx context.Context, newResultSet *models.ResultSet) {
|
||||||
|
_m.Called(ctx, newResultSet)
|
||||||
|
}
|
||||||
|
|
||||||
|
// SessionService_SetResultSet_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'SetResultSet'
|
||||||
|
type SessionService_SetResultSet_Call struct {
|
||||||
|
*mock.Call
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetResultSet is a helper method to define mock.On call
|
||||||
|
// - ctx context.Context
|
||||||
|
// - newResultSet *models.ResultSet
|
||||||
|
func (_e *SessionService_Expecter) SetResultSet(ctx interface{}, newResultSet interface{}) *SessionService_SetResultSet_Call {
|
||||||
|
return &SessionService_SetResultSet_Call{Call: _e.mock.On("SetResultSet", ctx, newResultSet)}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (_c *SessionService_SetResultSet_Call) Run(run func(ctx context.Context, newResultSet *models.ResultSet)) *SessionService_SetResultSet_Call {
|
||||||
|
_c.Call.Run(func(args mock.Arguments) {
|
||||||
|
run(args[0].(context.Context), args[1].(*models.ResultSet))
|
||||||
|
})
|
||||||
|
return _c
|
||||||
|
}
|
||||||
|
|
||||||
|
func (_c *SessionService_SetResultSet_Call) Return() *SessionService_SetResultSet_Call {
|
||||||
|
_c.Call.Return()
|
||||||
|
return _c
|
||||||
|
}
|
||||||
|
|
||||||
|
type mockConstructorTestingTNewSessionService interface {
|
||||||
|
mock.TestingT
|
||||||
|
Cleanup(func())
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewSessionService creates a new instance of SessionService. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations.
|
||||||
|
func NewSessionService(t mockConstructorTestingTNewSessionService) *SessionService {
|
||||||
|
mock := &SessionService{}
|
||||||
|
mock.Mock.Test(t)
|
||||||
|
|
||||||
|
t.Cleanup(func() { mock.AssertExpectations(t) })
|
||||||
|
|
||||||
|
return mock
|
||||||
|
}
|
106
internal/dynamo-browse/services/scriptmanager/mocks/UIService.go
Normal file
106
internal/dynamo-browse/services/scriptmanager/mocks/UIService.go
Normal file
|
@ -0,0 +1,106 @@
|
||||||
|
// Code generated by mockery v2.16.0. DO NOT EDIT.
|
||||||
|
|
||||||
|
package mocks
|
||||||
|
|
||||||
|
import (
|
||||||
|
context "context"
|
||||||
|
|
||||||
|
mock "github.com/stretchr/testify/mock"
|
||||||
|
)
|
||||||
|
|
||||||
|
// UIService is an autogenerated mock type for the UIService type
|
||||||
|
type UIService struct {
|
||||||
|
mock.Mock
|
||||||
|
}
|
||||||
|
|
||||||
|
type UIService_Expecter struct {
|
||||||
|
mock *mock.Mock
|
||||||
|
}
|
||||||
|
|
||||||
|
func (_m *UIService) EXPECT() *UIService_Expecter {
|
||||||
|
return &UIService_Expecter{mock: &_m.Mock}
|
||||||
|
}
|
||||||
|
|
||||||
|
// PrintMessage provides a mock function with given fields: ctx, msg
|
||||||
|
func (_m *UIService) PrintMessage(ctx context.Context, msg string) {
|
||||||
|
_m.Called(ctx, msg)
|
||||||
|
}
|
||||||
|
|
||||||
|
// UIService_PrintMessage_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'PrintMessage'
|
||||||
|
type UIService_PrintMessage_Call struct {
|
||||||
|
*mock.Call
|
||||||
|
}
|
||||||
|
|
||||||
|
// PrintMessage is a helper method to define mock.On call
|
||||||
|
// - ctx context.Context
|
||||||
|
// - msg string
|
||||||
|
func (_e *UIService_Expecter) PrintMessage(ctx interface{}, msg interface{}) *UIService_PrintMessage_Call {
|
||||||
|
return &UIService_PrintMessage_Call{Call: _e.mock.On("PrintMessage", ctx, msg)}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (_c *UIService_PrintMessage_Call) Run(run func(ctx context.Context, msg string)) *UIService_PrintMessage_Call {
|
||||||
|
_c.Call.Run(func(args mock.Arguments) {
|
||||||
|
run(args[0].(context.Context), args[1].(string))
|
||||||
|
})
|
||||||
|
return _c
|
||||||
|
}
|
||||||
|
|
||||||
|
func (_c *UIService_PrintMessage_Call) Return() *UIService_PrintMessage_Call {
|
||||||
|
_c.Call.Return()
|
||||||
|
return _c
|
||||||
|
}
|
||||||
|
|
||||||
|
// Prompt provides a mock function with given fields: ctx, msg
|
||||||
|
func (_m *UIService) Prompt(ctx context.Context, msg string) chan string {
|
||||||
|
ret := _m.Called(ctx, msg)
|
||||||
|
|
||||||
|
var r0 chan string
|
||||||
|
if rf, ok := ret.Get(0).(func(context.Context, string) chan string); ok {
|
||||||
|
r0 = rf(ctx, msg)
|
||||||
|
} else {
|
||||||
|
if ret.Get(0) != nil {
|
||||||
|
r0 = ret.Get(0).(chan string)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return r0
|
||||||
|
}
|
||||||
|
|
||||||
|
// UIService_Prompt_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Prompt'
|
||||||
|
type UIService_Prompt_Call struct {
|
||||||
|
*mock.Call
|
||||||
|
}
|
||||||
|
|
||||||
|
// Prompt is a helper method to define mock.On call
|
||||||
|
// - ctx context.Context
|
||||||
|
// - msg string
|
||||||
|
func (_e *UIService_Expecter) Prompt(ctx interface{}, msg interface{}) *UIService_Prompt_Call {
|
||||||
|
return &UIService_Prompt_Call{Call: _e.mock.On("Prompt", ctx, msg)}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (_c *UIService_Prompt_Call) Run(run func(ctx context.Context, msg string)) *UIService_Prompt_Call {
|
||||||
|
_c.Call.Run(func(args mock.Arguments) {
|
||||||
|
run(args[0].(context.Context), args[1].(string))
|
||||||
|
})
|
||||||
|
return _c
|
||||||
|
}
|
||||||
|
|
||||||
|
func (_c *UIService_Prompt_Call) Return(_a0 chan string) *UIService_Prompt_Call {
|
||||||
|
_c.Call.Return(_a0)
|
||||||
|
return _c
|
||||||
|
}
|
||||||
|
|
||||||
|
type mockConstructorTestingTNewUIService interface {
|
||||||
|
mock.TestingT
|
||||||
|
Cleanup(func())
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewUIService creates a new instance of UIService. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations.
|
||||||
|
func NewUIService(t mockConstructorTestingTNewUIService) *UIService {
|
||||||
|
mock := &UIService{}
|
||||||
|
mock.Mock.Test(t)
|
||||||
|
|
||||||
|
t.Cleanup(func() { mock.AssertExpectations(t) })
|
||||||
|
|
||||||
|
return mock
|
||||||
|
}
|
67
internal/dynamo-browse/services/scriptmanager/modext.go
Normal file
67
internal/dynamo-browse/services/scriptmanager/modext.go
Normal file
|
@ -0,0 +1,67 @@
|
||||||
|
package scriptmanager
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"github.com/cloudcmds/tamarin/arg"
|
||||||
|
"github.com/cloudcmds/tamarin/object"
|
||||||
|
"github.com/cloudcmds/tamarin/scope"
|
||||||
|
"github.com/pkg/errors"
|
||||||
|
)
|
||||||
|
|
||||||
|
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),
|
||||||
|
})
|
||||||
|
|
||||||
|
scp.Declare("ext", mod, true)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *extModule) command(ctx context.Context, args ...object.Object) object.Object {
|
||||||
|
if err := arg.Require("ext.command", 2, args); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
cmdName, err := object.AsString(args[0])
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
fnRes, isFnRes := args[1].(*object.Function)
|
||||||
|
if !isFnRes {
|
||||||
|
return object.NewError(errors.New("expected second arg to be a function"))
|
||||||
|
}
|
||||||
|
|
||||||
|
callFn, hasCallFn := object.GetCallFunc(ctx)
|
||||||
|
if !hasCallFn {
|
||||||
|
return object.NewError(errors.New("no callFn found in context"))
|
||||||
|
}
|
||||||
|
|
||||||
|
// This command function will be executed by the script scheduler
|
||||||
|
newCommand := func(ctx context.Context, args []string) error {
|
||||||
|
objArgs := make([]object.Object, len(args))
|
||||||
|
for i, a := range args {
|
||||||
|
objArgs[i] = object.NewString(a)
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx = ctxWithOptions(ctx, m.scriptPlugin.scriptService.options)
|
||||||
|
|
||||||
|
res := callFn(ctx, fnRes.Scope(), fnRes, objArgs)
|
||||||
|
if object.IsError(res) {
|
||||||
|
errObj := res.(*object.Error)
|
||||||
|
return errors.Errorf("command error '%v':%v - %v", m.scriptPlugin.name, cmdName, errObj.Inspect())
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
if m.scriptPlugin.definedCommands == nil {
|
||||||
|
m.scriptPlugin.definedCommands = make(map[string]*Command)
|
||||||
|
}
|
||||||
|
m.scriptPlugin.definedCommands[cmdName] = &Command{plugin: m.scriptPlugin, cmdFn: newCommand}
|
||||||
|
return nil
|
||||||
|
}
|
47
internal/dynamo-browse/services/scriptmanager/modos.go
Normal file
47
internal/dynamo-browse/services/scriptmanager/modos.go
Normal file
|
@ -0,0 +1,47 @@
|
||||||
|
package scriptmanager
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"github.com/cloudcmds/tamarin/arg"
|
||||||
|
"github.com/cloudcmds/tamarin/object"
|
||||||
|
"github.com/cloudcmds/tamarin/scope"
|
||||||
|
"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 := optionFromCtx(ctx)
|
||||||
|
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) register(scp *scope.Scope) {
|
||||||
|
modScope := scope.New(scope.Opts{})
|
||||||
|
mod := object.NewModule("os", modScope)
|
||||||
|
|
||||||
|
modScope.AddBuiltins([]*object.Builtin{
|
||||||
|
object.NewBuiltin("exec", om.exec, mod),
|
||||||
|
})
|
||||||
|
|
||||||
|
scp.Declare("os", mod, true)
|
||||||
|
}
|
110
internal/dynamo-browse/services/scriptmanager/modos_test.go
Normal file
110
internal/dynamo-browse/services/scriptmanager/modos_test.go
Normal file
|
@ -0,0 +1,110 @@
|
||||||
|
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/stretchr/testify/assert"
|
||||||
|
"github.com/stretchr/testify/mock"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
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())
|
||||||
|
`)
|
||||||
|
|
||||||
|
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.RunAdHocScript(ctx, "test.tm")
|
||||||
|
assert.NoError(t, err)
|
||||||
|
|
||||||
|
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)
|
||||||
|
})
|
||||||
|
}
|
121
internal/dynamo-browse/services/scriptmanager/modsession.go
Normal file
121
internal/dynamo-browse/services/scriptmanager/modsession.go
Normal file
|
@ -0,0 +1,121 @@
|
||||||
|
package scriptmanager
|
||||||
|
|
||||||
|
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"
|
||||||
|
)
|
||||||
|
|
||||||
|
type sessionModule struct {
|
||||||
|
sessionService SessionService
|
||||||
|
}
|
||||||
|
|
||||||
|
func (um *sessionModule) query(ctx context.Context, args ...object.Object) object.Object {
|
||||||
|
if len(args) == 0 || len(args) > 2 {
|
||||||
|
return object.Errorf("type error: session.query takes either 1 or 2 arguments (%d given)", len(args))
|
||||||
|
}
|
||||||
|
|
||||||
|
var options QueryOptions
|
||||||
|
|
||||||
|
expr, objErr := object.AsString(args[0])
|
||||||
|
if objErr != nil {
|
||||||
|
return objErr
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(args) == 2 {
|
||||||
|
objMap, objErr := object.AsMap(args[1])
|
||||||
|
if objErr != nil {
|
||||||
|
return objErr
|
||||||
|
}
|
||||||
|
|
||||||
|
// Placeholders
|
||||||
|
if argsVal, isArgsValMap := objMap.Get("args").(*object.Map); isArgsValMap {
|
||||||
|
options.NamePlaceholders = make(map[string]string)
|
||||||
|
options.ValuePlaceholders = make(map[string]types.AttributeValue)
|
||||||
|
|
||||||
|
for k, val := range argsVal.Value() {
|
||||||
|
switch v := val.(type) {
|
||||||
|
case *object.String:
|
||||||
|
options.NamePlaceholders[k] = v.Value()
|
||||||
|
options.ValuePlaceholders[k] = &types.AttributeValueMemberS{Value: v.Value()}
|
||||||
|
case *object.Int:
|
||||||
|
options.ValuePlaceholders[k] = &types.AttributeValueMemberN{Value: fmt.Sprint(v.Value())}
|
||||||
|
case *object.Float:
|
||||||
|
options.ValuePlaceholders[k] = &types.AttributeValueMemberN{Value: fmt.Sprint(v.Value())}
|
||||||
|
case *object.Bool:
|
||||||
|
options.ValuePlaceholders[k] = &types.AttributeValueMemberBOOL{Value: v.Value()}
|
||||||
|
case *object.NilType:
|
||||||
|
options.ValuePlaceholders[k] = &types.AttributeValueMemberNULL{Value: true}
|
||||||
|
default:
|
||||||
|
return object.Errorf("type error: arg '%v' of type '%v' is not supported", k, val.Type())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
resp, err := um.sessionService.Query(ctx, expr, options)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return object.NewErrResult(object.NewError(err))
|
||||||
|
}
|
||||||
|
return object.NewOkResult(&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 {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
rs := um.sessionService.ResultSet(ctx)
|
||||||
|
if rs == nil {
|
||||||
|
return object.Nil
|
||||||
|
}
|
||||||
|
return &resultSetProxy{resultSet: rs}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (um *sessionModule) selectedItem(ctx context.Context, args ...object.Object) object.Object {
|
||||||
|
if err := arg.Require("session.result_set", 0, args); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
rs := um.sessionService.ResultSet(ctx)
|
||||||
|
idx := um.sessionService.SelectedItemIndex(ctx)
|
||||||
|
if rs == nil || idx < 0 {
|
||||||
|
return object.Nil
|
||||||
|
}
|
||||||
|
|
||||||
|
rsProxy := &resultSetProxy{resultSet: rs}
|
||||||
|
return newItemProxy(rsProxy, idx)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (um *sessionModule) setResultSet(ctx context.Context, args ...object.Object) object.Object {
|
||||||
|
if err := arg.Require("session.set_result_set", 1, args); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
resultSetProxy, isResultSetProxy := args[0].(*resultSetProxy)
|
||||||
|
if !isResultSetProxy {
|
||||||
|
return object.NewError(errors.Errorf("type error: expected a resultsset (got %v)", args[0]))
|
||||||
|
}
|
||||||
|
|
||||||
|
um.sessionService.SetResultSet(ctx, resultSetProxy.resultSet)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
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("result_set", um.resultSet, mod),
|
||||||
|
object.NewBuiltin("selected_item", um.selectedItem, mod),
|
||||||
|
object.NewBuiltin("set_result_set", um.setResultSet, mod),
|
||||||
|
})
|
||||||
|
|
||||||
|
scp.Declare("session", mod, true)
|
||||||
|
}
|
292
internal/dynamo-browse/services/scriptmanager/modsession_test.go
Normal file
292
internal/dynamo-browse/services/scriptmanager/modsession_test.go
Normal file
|
@ -0,0 +1,292 @@
|
||||||
|
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/pkg/errors"
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
"github.com/stretchr/testify/mock"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestModSession_Query(t *testing.T) {
|
||||||
|
t.Run("should successfully return query result", func(t *testing.T) {
|
||||||
|
rs := &models.ResultSet{}
|
||||||
|
rs.SetItems([]models.Item{
|
||||||
|
{"pk": &types.AttributeValueMemberS{Value: "abc"}},
|
||||||
|
{"pk": &types.AttributeValueMemberS{Value: "1232"}},
|
||||||
|
})
|
||||||
|
|
||||||
|
mockedSessionService := mocks.NewSessionService(t)
|
||||||
|
mockedSessionService.EXPECT().Query(mock.Anything, "some expr", scriptmanager.QueryOptions{}).Return(rs, nil)
|
||||||
|
|
||||||
|
mockedUIService := mocks.NewUIService(t)
|
||||||
|
mockedUIService.EXPECT().PrintMessage(mock.Anything, "2")
|
||||||
|
mockedUIService.EXPECT().PrintMessage(mock.Anything, "res[0]['pk'].S = abc")
|
||||||
|
mockedUIService.EXPECT().PrintMessage(mock.Anything, "res[1]['pk'].S = 1232")
|
||||||
|
mockedUIService.EXPECT().PrintMessage(mock.Anything, "res[1].attr('size(pk)') = 4")
|
||||||
|
|
||||||
|
testFS := testScriptFile(t, "test.tm", `
|
||||||
|
res := session.query("some expr").unwrap()
|
||||||
|
ui.print(res.length)
|
||||||
|
ui.print("res[0]['pk'].S = ", res[0].attr("pk"))
|
||||||
|
ui.print("res[1]['pk'].S = ", res[1].attr("pk"))
|
||||||
|
ui.print("res[1].attr('size(pk)') = ", res[1].attr("size(pk)"))
|
||||||
|
`)
|
||||||
|
|
||||||
|
srv := scriptmanager.New(scriptmanager.WithFS(testFS))
|
||||||
|
srv.SetIFaces(scriptmanager.Ifaces{
|
||||||
|
UI: mockedUIService,
|
||||||
|
Session: mockedSessionService,
|
||||||
|
})
|
||||||
|
|
||||||
|
ctx := context.Background()
|
||||||
|
err := <-srv.RunAdHocScript(ctx, "test.tm")
|
||||||
|
assert.NoError(t, err)
|
||||||
|
|
||||||
|
mockedUIService.AssertExpectations(t)
|
||||||
|
mockedSessionService.AssertExpectations(t)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("should return error if query returns error", func(t *testing.T) {
|
||||||
|
mockedSessionService := mocks.NewSessionService(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))
|
||||||
|
srv.SetIFaces(scriptmanager.Ifaces{
|
||||||
|
UI: mockedUIService,
|
||||||
|
Session: mockedSessionService,
|
||||||
|
})
|
||||||
|
|
||||||
|
ctx := context.Background()
|
||||||
|
err := <-srv.RunAdHocScript(ctx, "test.tm")
|
||||||
|
assert.NoError(t, err)
|
||||||
|
|
||||||
|
mockedUIService.AssertExpectations(t)
|
||||||
|
mockedSessionService.AssertExpectations(t)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("should set placeholder values", func(t *testing.T) {
|
||||||
|
rs := &models.ResultSet{}
|
||||||
|
|
||||||
|
mockedSessionService := mocks.NewSessionService(t)
|
||||||
|
mockedSessionService.EXPECT().Query(mock.Anything, ":name = $value", scriptmanager.QueryOptions{
|
||||||
|
NamePlaceholders: map[string]string{
|
||||||
|
"name": "hello",
|
||||||
|
"value": "world",
|
||||||
|
},
|
||||||
|
ValuePlaceholders: map[string]types.AttributeValue{
|
||||||
|
"name": &types.AttributeValueMemberS{Value: "hello"},
|
||||||
|
"value": &types.AttributeValueMemberS{Value: "world"},
|
||||||
|
},
|
||||||
|
}).Return(rs, nil)
|
||||||
|
|
||||||
|
mockedUIService := mocks.NewUIService(t)
|
||||||
|
|
||||||
|
testFS := testScriptFile(t, "test.tm", `
|
||||||
|
res := session.query(":name = $value", {
|
||||||
|
args: {
|
||||||
|
name: "hello",
|
||||||
|
value: "world",
|
||||||
|
},
|
||||||
|
})
|
||||||
|
assert(!res.is_err())
|
||||||
|
`)
|
||||||
|
|
||||||
|
srv := scriptmanager.New(scriptmanager.WithFS(testFS))
|
||||||
|
srv.SetIFaces(scriptmanager.Ifaces{
|
||||||
|
UI: mockedUIService,
|
||||||
|
Session: mockedSessionService,
|
||||||
|
})
|
||||||
|
|
||||||
|
ctx := context.Background()
|
||||||
|
err := <-srv.RunAdHocScript(ctx, "test.tm")
|
||||||
|
assert.NoError(t, err)
|
||||||
|
|
||||||
|
mockedUIService.AssertExpectations(t)
|
||||||
|
mockedSessionService.AssertExpectations(t)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("should support various placeholder value type", func(t *testing.T) {
|
||||||
|
rs := &models.ResultSet{}
|
||||||
|
|
||||||
|
mockedSessionService := mocks.NewSessionService(t)
|
||||||
|
mockedSessionService.EXPECT().Query(mock.Anything, ":name = $value", scriptmanager.QueryOptions{
|
||||||
|
NamePlaceholders: map[string]string{
|
||||||
|
"str": "hello",
|
||||||
|
},
|
||||||
|
ValuePlaceholders: map[string]types.AttributeValue{
|
||||||
|
"str": &types.AttributeValueMemberS{Value: "hello"},
|
||||||
|
"int": &types.AttributeValueMemberN{Value: "123"},
|
||||||
|
"float": &types.AttributeValueMemberN{Value: "3.14"},
|
||||||
|
"bool": &types.AttributeValueMemberBOOL{Value: true},
|
||||||
|
"nil": &types.AttributeValueMemberNULL{Value: true},
|
||||||
|
},
|
||||||
|
}).Return(rs, nil)
|
||||||
|
|
||||||
|
mockedUIService := mocks.NewUIService(t)
|
||||||
|
|
||||||
|
testFS := testScriptFile(t, "test.tm", `
|
||||||
|
res := session.query(":name = $value", {
|
||||||
|
args: {
|
||||||
|
"str": "hello",
|
||||||
|
"int": 123,
|
||||||
|
"float": 3.14,
|
||||||
|
"bool": true,
|
||||||
|
"nil": nil,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
assert(!res.is_err())
|
||||||
|
`)
|
||||||
|
|
||||||
|
srv := scriptmanager.New(scriptmanager.WithFS(testFS))
|
||||||
|
srv.SetIFaces(scriptmanager.Ifaces{
|
||||||
|
UI: mockedUIService,
|
||||||
|
Session: mockedSessionService,
|
||||||
|
})
|
||||||
|
|
||||||
|
ctx := context.Background()
|
||||||
|
err := <-srv.RunAdHocScript(ctx, "test.tm")
|
||||||
|
assert.NoError(t, err)
|
||||||
|
|
||||||
|
mockedUIService.AssertExpectations(t)
|
||||||
|
mockedSessionService.AssertExpectations(t)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("should return error when placeholder value type is unsupported", func(t *testing.T) {
|
||||||
|
mockedSessionService := mocks.NewSessionService(t)
|
||||||
|
mockedUIService := mocks.NewUIService(t)
|
||||||
|
|
||||||
|
testFS := testScriptFile(t, "test.tm", `
|
||||||
|
res := session.query(":name = $value", {
|
||||||
|
args: {
|
||||||
|
"bad": func() { },
|
||||||
|
},
|
||||||
|
})
|
||||||
|
assert(res.is_err())
|
||||||
|
`)
|
||||||
|
|
||||||
|
srv := scriptmanager.New(scriptmanager.WithFS(testFS))
|
||||||
|
srv.SetIFaces(scriptmanager.Ifaces{
|
||||||
|
UI: mockedUIService,
|
||||||
|
Session: mockedSessionService,
|
||||||
|
})
|
||||||
|
|
||||||
|
ctx := context.Background()
|
||||||
|
err := <-srv.RunAdHocScript(ctx, "test.tm")
|
||||||
|
assert.Error(t, err)
|
||||||
|
|
||||||
|
mockedUIService.AssertExpectations(t)
|
||||||
|
mockedSessionService.AssertExpectations(t)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestModSession_SelectedItem(t *testing.T) {
|
||||||
|
t.Run("should return selected item from service implementation", func(t *testing.T) {
|
||||||
|
rs := &models.ResultSet{}
|
||||||
|
rs.SetItems([]models.Item{
|
||||||
|
{"pk": &types.AttributeValueMemberS{Value: "abc"}},
|
||||||
|
{"pk": &types.AttributeValueMemberS{Value: "1232"}},
|
||||||
|
})
|
||||||
|
|
||||||
|
mockedSessionService := mocks.NewSessionService(t)
|
||||||
|
mockedSessionService.EXPECT().ResultSet(mock.Anything).Return(rs)
|
||||||
|
mockedSessionService.EXPECT().SelectedItemIndex(mock.Anything).Return(1)
|
||||||
|
|
||||||
|
testFS := testScriptFile(t, "test.tm", `
|
||||||
|
selItem := session.selected_item()
|
||||||
|
|
||||||
|
assert(selItem != nil, "selItem != nil")
|
||||||
|
assert(selItem.index == 1, "selItem.index")
|
||||||
|
assert(selItem.result_set == session.result_set(), "selItem.result_set")
|
||||||
|
assert(selItem.attr('pk') == '1232', "selItem.attr('pk')")
|
||||||
|
`)
|
||||||
|
|
||||||
|
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 selected item returns -1", func(t *testing.T) {
|
||||||
|
rs := &models.ResultSet{}
|
||||||
|
rs.SetItems([]models.Item{
|
||||||
|
{"pk": &types.AttributeValueMemberS{Value: "abc"}},
|
||||||
|
{"pk": &types.AttributeValueMemberS{Value: "1232"}},
|
||||||
|
})
|
||||||
|
|
||||||
|
mockedSessionService := mocks.NewSessionService(t)
|
||||||
|
mockedSessionService.EXPECT().ResultSet(mock.Anything).Return(rs)
|
||||||
|
mockedSessionService.EXPECT().SelectedItemIndex(mock.Anything).Return(-1)
|
||||||
|
|
||||||
|
testFS := testScriptFile(t, "test.tm", `
|
||||||
|
selItem := session.selected_item()
|
||||||
|
|
||||||
|
assert(selItem == nil, "selItem != 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 TestModSession_SetResultSet(t *testing.T) {
|
||||||
|
t.Run("should set the result set on the session", func(t *testing.T) {
|
||||||
|
rs := &models.ResultSet{}
|
||||||
|
rs.SetItems([]models.Item{
|
||||||
|
{"pk": &types.AttributeValueMemberS{Value: "abc"}},
|
||||||
|
{"pk": &types.AttributeValueMemberS{Value: "1232"}},
|
||||||
|
})
|
||||||
|
|
||||||
|
mockedSessionService := mocks.NewSessionService(t)
|
||||||
|
mockedSessionService.EXPECT().Query(mock.Anything, "some expr", scriptmanager.QueryOptions{}).Return(rs, nil)
|
||||||
|
mockedSessionService.EXPECT().SetResultSet(mock.Anything, rs)
|
||||||
|
|
||||||
|
mockedUIService := mocks.NewUIService(t)
|
||||||
|
|
||||||
|
testFS := testScriptFile(t, "test.tm", `
|
||||||
|
res := session.query("some expr").unwrap()
|
||||||
|
session.set_result_set(res)
|
||||||
|
`)
|
||||||
|
|
||||||
|
srv := scriptmanager.New(scriptmanager.WithFS(testFS))
|
||||||
|
srv.SetIFaces(scriptmanager.Ifaces{
|
||||||
|
UI: mockedUIService,
|
||||||
|
Session: mockedSessionService,
|
||||||
|
})
|
||||||
|
|
||||||
|
ctx := context.Background()
|
||||||
|
err := <-srv.RunAdHocScript(ctx, "test.tm")
|
||||||
|
assert.NoError(t, err)
|
||||||
|
|
||||||
|
mockedUIService.AssertExpectations(t)
|
||||||
|
mockedSessionService.AssertExpectations(t)
|
||||||
|
})
|
||||||
|
}
|
60
internal/dynamo-browse/services/scriptmanager/modui.go
Normal file
60
internal/dynamo-browse/services/scriptmanager/modui.go
Normal file
|
@ -0,0 +1,60 @@
|
||||||
|
package scriptmanager
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"github.com/cloudcmds/tamarin/arg"
|
||||||
|
"github.com/cloudcmds/tamarin/object"
|
||||||
|
"github.com/cloudcmds/tamarin/scope"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
type uiModule struct {
|
||||||
|
uiService UIService
|
||||||
|
}
|
||||||
|
|
||||||
|
func (um *uiModule) print(ctx context.Context, args ...object.Object) object.Object {
|
||||||
|
var msg strings.Builder
|
||||||
|
for _, arg := range args {
|
||||||
|
switch a := arg.(type) {
|
||||||
|
case *object.String:
|
||||||
|
msg.WriteString(a.Value())
|
||||||
|
default:
|
||||||
|
msg.WriteString(a.Inspect())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
um.uiService.PrintMessage(ctx, msg.String())
|
||||||
|
return object.Nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (um *uiModule) prompt(ctx context.Context, args ...object.Object) object.Object {
|
||||||
|
if err := arg.Require("ui.prompt", 1, args); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
msg, _ := object.AsString(args[0])
|
||||||
|
respChan := um.uiService.Prompt(ctx, msg)
|
||||||
|
|
||||||
|
select {
|
||||||
|
case resp, hasResp := <-respChan:
|
||||||
|
if hasResp {
|
||||||
|
return object.NewString(resp)
|
||||||
|
} else {
|
||||||
|
return object.NewError(ctx.Err())
|
||||||
|
}
|
||||||
|
case <-ctx.Done():
|
||||||
|
return object.NewError(ctx.Err())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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),
|
||||||
|
})
|
||||||
|
|
||||||
|
scp.Declare("ui", mod, true)
|
||||||
|
}
|
98
internal/dynamo-browse/services/scriptmanager/modui_test.go
Normal file
98
internal/dynamo-browse/services/scriptmanager/modui_test.go
Normal file
|
@ -0,0 +1,98 @@
|
||||||
|
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/stretchr/testify/assert"
|
||||||
|
"github.com/stretchr/testify/mock"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestModUI_Prompt(t *testing.T) {
|
||||||
|
t.Run("should successfully return prompt value", func(t *testing.T) {
|
||||||
|
testFS := testScriptFile(t, "test.tm", `
|
||||||
|
ui.print("Hello, world")
|
||||||
|
var name = ui.prompt("What is your name? ")
|
||||||
|
ui.print("Hello, " + name)
|
||||||
|
`)
|
||||||
|
|
||||||
|
promptChan := make(chan string)
|
||||||
|
go func() {
|
||||||
|
promptChan <- "T. Test"
|
||||||
|
}()
|
||||||
|
|
||||||
|
mockedUIService := mocks.NewUIService(t)
|
||||||
|
mockedUIService.EXPECT().PrintMessage(mock.Anything, "Hello, world")
|
||||||
|
mockedUIService.EXPECT().Prompt(mock.Anything, "What is your name? ").Return(promptChan)
|
||||||
|
mockedUIService.EXPECT().PrintMessage(mock.Anything, "Hello, T. Test")
|
||||||
|
|
||||||
|
srv := scriptmanager.New(scriptmanager.WithFS(testFS))
|
||||||
|
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 return error if prompt was cancelled", func(t *testing.T) {
|
||||||
|
testFS := testScriptFile(t, "test.tm", `
|
||||||
|
ui.print("Hello, world")
|
||||||
|
var name = ui.prompt("What is your name? ")
|
||||||
|
ui.print("After")
|
||||||
|
`)
|
||||||
|
|
||||||
|
promptChan := make(chan string)
|
||||||
|
close(promptChan)
|
||||||
|
|
||||||
|
ctx := context.Background()
|
||||||
|
|
||||||
|
mockedUIService := mocks.NewUIService(t)
|
||||||
|
mockedUIService.EXPECT().PrintMessage(mock.Anything, "Hello, world")
|
||||||
|
mockedUIService.EXPECT().Prompt(mock.Anything, "What is your name? ").Return(promptChan)
|
||||||
|
|
||||||
|
srv := scriptmanager.New(scriptmanager.WithFS(testFS))
|
||||||
|
srv.SetIFaces(scriptmanager.Ifaces{
|
||||||
|
UI: mockedUIService,
|
||||||
|
})
|
||||||
|
|
||||||
|
err := <-srv.RunAdHocScript(ctx, "test.tm")
|
||||||
|
assert.Error(t, err)
|
||||||
|
|
||||||
|
mockedUIService.AssertNotCalled(t, "Prompt", "after")
|
||||||
|
mockedUIService.AssertExpectations(t)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("should return error if context was cancelled", func(t *testing.T) {
|
||||||
|
testFS := testScriptFile(t, "test.tm", `
|
||||||
|
ui.print("Hello, world")
|
||||||
|
var name = ui.prompt("What is your name? ")
|
||||||
|
ui.print("After")
|
||||||
|
`)
|
||||||
|
|
||||||
|
promptChan := make(chan string)
|
||||||
|
ctx, cancelFn := context.WithCancel(context.Background())
|
||||||
|
defer cancelFn()
|
||||||
|
|
||||||
|
mockedUIService := mocks.NewUIService(t)
|
||||||
|
mockedUIService.EXPECT().PrintMessage(mock.Anything, "Hello, world")
|
||||||
|
mockedUIService.EXPECT().Prompt(mock.Anything, "What is your name? ").Run(func(ctx context.Context, msg string) {
|
||||||
|
cancelFn()
|
||||||
|
}).Return(promptChan)
|
||||||
|
|
||||||
|
srv := scriptmanager.New(scriptmanager.WithFS(testFS))
|
||||||
|
srv.SetIFaces(scriptmanager.Ifaces{
|
||||||
|
UI: mockedUIService,
|
||||||
|
})
|
||||||
|
|
||||||
|
err := <-srv.RunAdHocScript(ctx, "test.tm")
|
||||||
|
assert.Error(t, err)
|
||||||
|
|
||||||
|
mockedUIService.AssertNotCalled(t, "Prompt", "after")
|
||||||
|
mockedUIService.AssertExpectations(t)
|
||||||
|
})
|
||||||
|
}
|
45
internal/dynamo-browse/services/scriptmanager/opts.go
Normal file
45
internal/dynamo-browse/services/scriptmanager/opts.go
Normal file
|
@ -0,0 +1,45 @@
|
||||||
|
package scriptmanager
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"os"
|
||||||
|
)
|
||||||
|
|
||||||
|
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
|
||||||
|
}
|
||||||
|
|
||||||
|
type optionCtxKeyType struct{}
|
||||||
|
|
||||||
|
var optionCtxKey = optionCtxKeyType{}
|
||||||
|
|
||||||
|
func optionFromCtx(ctx context.Context) Options {
|
||||||
|
perms, _ := ctx.Value(optionCtxKey).(Options)
|
||||||
|
return perms
|
||||||
|
}
|
||||||
|
|
||||||
|
func ctxWithOptions(ctx context.Context, perms Options) context.Context {
|
||||||
|
return context.WithValue(ctx, optionCtxKey, perms)
|
||||||
|
}
|
240
internal/dynamo-browse/services/scriptmanager/resultsetproxy.go
Normal file
240
internal/dynamo-browse/services/scriptmanager/resultsetproxy.go
Normal file
|
@ -0,0 +1,240 @@
|
||||||
|
package scriptmanager
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"github.com/aws/aws-sdk-go-v2/service/dynamodb/types"
|
||||||
|
"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"
|
||||||
|
"github.com/pkg/errors"
|
||||||
|
"strconv"
|
||||||
|
)
|
||||||
|
|
||||||
|
type resultSetProxy struct {
|
||||||
|
resultSet *models.ResultSet
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *resultSetProxy) Interface() interface{} {
|
||||||
|
return r.resultSet
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *resultSetProxy) IsTruthy() bool {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *resultSetProxy) Type() object.Type {
|
||||||
|
return "resultset"
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *resultSetProxy) Inspect() string {
|
||||||
|
return "resultset"
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *resultSetProxy) Equals(other object.Object) object.Object {
|
||||||
|
otherRS, isOtherRS := other.(*resultSetProxy)
|
||||||
|
if !isOtherRS {
|
||||||
|
return object.False
|
||||||
|
}
|
||||||
|
|
||||||
|
return object.NewBool(r.resultSet == otherRS.resultSet)
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetItem implements the [key] operator for a container type.
|
||||||
|
func (r *resultSetProxy) GetItem(key object.Object) (object.Object, *object.Error) {
|
||||||
|
idx, err := object.AsInt(key)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
realIdx := int(idx)
|
||||||
|
if realIdx < 0 {
|
||||||
|
realIdx = len(r.resultSet.Items()) + realIdx
|
||||||
|
}
|
||||||
|
|
||||||
|
if realIdx < 0 || realIdx >= len(r.resultSet.Items()) {
|
||||||
|
return nil, object.NewError(errors.Errorf("index error: index out of range: %v", idx))
|
||||||
|
}
|
||||||
|
|
||||||
|
return newItemProxy(r, realIdx), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetSlice implements the [start:stop] operator for a container type.
|
||||||
|
func (r *resultSetProxy) GetSlice(s object.Slice) (object.Object, *object.Error) {
|
||||||
|
return nil, object.NewError(errors.New("TODO"))
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetItem implements the [key] = value operator for a container type.
|
||||||
|
func (r *resultSetProxy) SetItem(key, value object.Object) *object.Error {
|
||||||
|
return object.NewError(errors.New("TODO"))
|
||||||
|
}
|
||||||
|
|
||||||
|
// DelItem implements the del [key] operator for a container type.
|
||||||
|
func (r *resultSetProxy) DelItem(key object.Object) *object.Error {
|
||||||
|
return object.NewError(errors.New("TODO"))
|
||||||
|
}
|
||||||
|
|
||||||
|
// Contains returns true if the given item is found in this container.
|
||||||
|
func (r *resultSetProxy) Contains(item object.Object) *object.Bool {
|
||||||
|
// TODO
|
||||||
|
return object.False
|
||||||
|
}
|
||||||
|
|
||||||
|
// Len returns the number of items in this container.
|
||||||
|
func (r *resultSetProxy) Len() *object.Int {
|
||||||
|
return object.NewInt(int64(len(r.resultSet.Items())))
|
||||||
|
}
|
||||||
|
|
||||||
|
// Iter returns an iterator for this container.
|
||||||
|
func (r *resultSetProxy) Iter() object.Iterator {
|
||||||
|
// TODO
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *resultSetProxy) GetAttr(name string) (object.Object, bool) {
|
||||||
|
switch name {
|
||||||
|
case "length":
|
||||||
|
return object.NewInt(int64(len(r.resultSet.Items()))), true
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil, false
|
||||||
|
}
|
||||||
|
|
||||||
|
type itemProxy struct {
|
||||||
|
resultSetProxy *resultSetProxy
|
||||||
|
itemIndex int
|
||||||
|
item models.Item
|
||||||
|
}
|
||||||
|
|
||||||
|
func newItemProxy(rs *resultSetProxy, itemIndex int) *itemProxy {
|
||||||
|
return &itemProxy{
|
||||||
|
resultSetProxy: rs,
|
||||||
|
itemIndex: itemIndex,
|
||||||
|
item: rs.resultSet.Items()[itemIndex],
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (i *itemProxy) Interface() interface{} {
|
||||||
|
return i.item
|
||||||
|
}
|
||||||
|
|
||||||
|
func (i *itemProxy) IsTruthy() bool {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
func (i *itemProxy) Type() object.Type {
|
||||||
|
return "item"
|
||||||
|
}
|
||||||
|
|
||||||
|
func (i *itemProxy) Inspect() string {
|
||||||
|
return "item"
|
||||||
|
}
|
||||||
|
|
||||||
|
func (i *itemProxy) Equals(other object.Object) object.Object {
|
||||||
|
// TODO
|
||||||
|
return object.False
|
||||||
|
}
|
||||||
|
|
||||||
|
func (i *itemProxy) GetAttr(name string) (object.Object, bool) {
|
||||||
|
// TODO: this should implement the container interface
|
||||||
|
switch name {
|
||||||
|
case "result_set":
|
||||||
|
return i.resultSetProxy, true
|
||||||
|
case "index":
|
||||||
|
return object.NewInt(int64(i.itemIndex)), true
|
||||||
|
case "attr":
|
||||||
|
return object.NewBuiltin("attr", i.value), true
|
||||||
|
case "set_attr":
|
||||||
|
return object.NewBuiltin("set_attr", i.setValue), true
|
||||||
|
case "delete_attr":
|
||||||
|
return object.NewBuiltin("delete_attr", i.deleteAttr), true
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil, false
|
||||||
|
}
|
||||||
|
|
||||||
|
func (i *itemProxy) value(ctx context.Context, args ...object.Object) object.Object {
|
||||||
|
if objErr := arg.Require("item.attr", 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)
|
||||||
|
}
|
||||||
|
av, err := modExpr.EvalItem(i.item)
|
||||||
|
if err != nil {
|
||||||
|
return object.NewError(errors.Errorf("arg error: path expression evaluate error: %v", err))
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO
|
||||||
|
switch v := av.(type) {
|
||||||
|
case *types.AttributeValueMemberS:
|
||||||
|
return object.NewString(v.Value)
|
||||||
|
case *types.AttributeValueMemberN:
|
||||||
|
// TODO: better
|
||||||
|
f, err := strconv.ParseFloat(v.Value, 64)
|
||||||
|
if err != nil {
|
||||||
|
return object.NewError(errors.Errorf("value error: invalid N value: %v", v.Value))
|
||||||
|
}
|
||||||
|
return object.NewFloat(f)
|
||||||
|
}
|
||||||
|
return object.NewError(errors.New("TODO"))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (i *itemProxy) setValue(ctx context.Context, args ...object.Object) object.Object {
|
||||||
|
if objErr := arg.Require("item.set_attr", 2, args); objErr != nil {
|
||||||
|
return objErr
|
||||||
|
}
|
||||||
|
|
||||||
|
pathExpr, objErr := object.AsString(args[0])
|
||||||
|
if objErr != nil {
|
||||||
|
return objErr
|
||||||
|
}
|
||||||
|
|
||||||
|
path, err := queryexpr.Parse(pathExpr)
|
||||||
|
if err != nil {
|
||||||
|
return object.Errorf("arg error: invalid path expression: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO
|
||||||
|
newValue := args[1]
|
||||||
|
switch v := newValue.(type) {
|
||||||
|
case *object.String:
|
||||||
|
if err := path.SetEvalItem(i.item, &types.AttributeValueMemberS{Value: v.Value()}); err != nil {
|
||||||
|
return object.NewError(err)
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
return object.Errorf("type error: unsupported value type (got %v)", newValue.Type())
|
||||||
|
}
|
||||||
|
|
||||||
|
i.resultSetProxy.resultSet.SetDirty(i.itemIndex, true)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (i *itemProxy) deleteAttr(ctx context.Context, args ...object.Object) object.Object {
|
||||||
|
if objErr := arg.Require("item.delete_attr", 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)
|
||||||
|
}
|
||||||
|
if err := modExpr.DeleteAttribute(i.item); err != nil {
|
||||||
|
return object.NewError(errors.Errorf("arg error: path expression evaluate error: %v", err))
|
||||||
|
}
|
||||||
|
|
||||||
|
i.resultSetProxy.resultSet.SetDirty(i.itemIndex, true)
|
||||||
|
return nil
|
||||||
|
}
|
|
@ -0,0 +1,135 @@
|
||||||
|
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/stretchr/testify/assert"
|
||||||
|
"github.com/stretchr/testify/mock"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestResultSetProxy(t *testing.T) {
|
||||||
|
t.Run("should property return properties of a resultset and item", func(t *testing.T) {
|
||||||
|
rs := &models.ResultSet{}
|
||||||
|
rs.SetItems([]models.Item{
|
||||||
|
{"pk": &types.AttributeValueMemberS{Value: "abc"}},
|
||||||
|
{"pk": &types.AttributeValueMemberS{Value: "1232"}},
|
||||||
|
})
|
||||||
|
|
||||||
|
mockedSessionService := mocks.NewSessionService(t)
|
||||||
|
mockedSessionService.EXPECT().Query(mock.Anything, "some expr", scriptmanager.QueryOptions{}).Return(rs, nil)
|
||||||
|
|
||||||
|
mockedUIService := mocks.NewUIService(t)
|
||||||
|
|
||||||
|
testFS := testScriptFile(t, "test.tm", `
|
||||||
|
res := session.query("some expr").unwrap()
|
||||||
|
|
||||||
|
// Test properties of the result set
|
||||||
|
assert(res == res, "result_set.equals")
|
||||||
|
assert(res.length == 2, "result_set.length")
|
||||||
|
|
||||||
|
// Test properties of items
|
||||||
|
assert(res[0].index == 0, "res[0].index")
|
||||||
|
assert(res[0].result_set == res, "res[0].result_set")
|
||||||
|
assert(res[0].attr('pk') == 'abc', "res[0].attr('pk')")
|
||||||
|
|
||||||
|
assert(res[1].attr('pk') == '1232', "res[1].attr('pk')")
|
||||||
|
`)
|
||||||
|
|
||||||
|
srv := scriptmanager.New(scriptmanager.WithFS(testFS))
|
||||||
|
srv.SetIFaces(scriptmanager.Ifaces{
|
||||||
|
UI: mockedUIService,
|
||||||
|
Session: mockedSessionService,
|
||||||
|
})
|
||||||
|
|
||||||
|
ctx := context.Background()
|
||||||
|
err := <-srv.RunAdHocScript(ctx, "test.tm")
|
||||||
|
assert.NoError(t, err)
|
||||||
|
|
||||||
|
mockedUIService.AssertExpectations(t)
|
||||||
|
mockedSessionService.AssertExpectations(t)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestResultSetProxy_SetAttr(t *testing.T) {
|
||||||
|
t.Run("should set the value of the item within a result set", func(t *testing.T) {
|
||||||
|
rs := &models.ResultSet{}
|
||||||
|
rs.SetItems([]models.Item{
|
||||||
|
{"pk": &types.AttributeValueMemberS{Value: "abc"}},
|
||||||
|
{"pk": &types.AttributeValueMemberS{Value: "1232"}},
|
||||||
|
})
|
||||||
|
|
||||||
|
mockedSessionService := mocks.NewSessionService(t)
|
||||||
|
mockedSessionService.EXPECT().Query(mock.Anything, "some expr", scriptmanager.QueryOptions{}).Return(rs, nil)
|
||||||
|
mockedSessionService.EXPECT().SetResultSet(mock.Anything, mock.MatchedBy(func(rs *models.ResultSet) bool {
|
||||||
|
assert.Equal(t, "bla-di-bla", rs.Items()[0]["pk"].(*types.AttributeValueMemberS).Value)
|
||||||
|
assert.True(t, rs.IsDirty(0))
|
||||||
|
return true
|
||||||
|
}))
|
||||||
|
|
||||||
|
mockedUIService := mocks.NewUIService(t)
|
||||||
|
|
||||||
|
testFS := testScriptFile(t, "test.tm", `
|
||||||
|
res := session.query("some expr").unwrap()
|
||||||
|
res[0].set_attr("pk", "bla-di-bla")
|
||||||
|
session.set_result_set(res)
|
||||||
|
`)
|
||||||
|
|
||||||
|
srv := scriptmanager.New(scriptmanager.WithFS(testFS))
|
||||||
|
srv.SetIFaces(scriptmanager.Ifaces{
|
||||||
|
UI: mockedUIService,
|
||||||
|
Session: mockedSessionService,
|
||||||
|
})
|
||||||
|
|
||||||
|
ctx := context.Background()
|
||||||
|
err := <-srv.RunAdHocScript(ctx, "test.tm")
|
||||||
|
assert.NoError(t, err)
|
||||||
|
|
||||||
|
mockedUIService.AssertExpectations(t)
|
||||||
|
mockedSessionService.AssertExpectations(t)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestResultSetProxy_DeleteAttr(t *testing.T) {
|
||||||
|
t.Run("should delete the value of the item within a result set", func(t *testing.T) {
|
||||||
|
rs := &models.ResultSet{}
|
||||||
|
rs.SetItems([]models.Item{
|
||||||
|
{"pk": &types.AttributeValueMemberS{Value: "abc"}, "deleteMe": &types.AttributeValueMemberBOOL{Value: true}},
|
||||||
|
{"pk": &types.AttributeValueMemberS{Value: "1232"}},
|
||||||
|
})
|
||||||
|
|
||||||
|
mockedSessionService := mocks.NewSessionService(t)
|
||||||
|
mockedSessionService.EXPECT().Query(mock.Anything, "some expr", scriptmanager.QueryOptions{}).Return(rs, nil)
|
||||||
|
mockedSessionService.EXPECT().SetResultSet(mock.Anything, mock.MatchedBy(func(rs *models.ResultSet) bool {
|
||||||
|
assert.Equal(t, "abc", rs.Items()[0]["pk"].(*types.AttributeValueMemberS).Value)
|
||||||
|
assert.Nil(t, rs.Items()[0]["deleteMe"])
|
||||||
|
assert.True(t, rs.IsDirty(0))
|
||||||
|
return true
|
||||||
|
}))
|
||||||
|
|
||||||
|
mockedUIService := mocks.NewUIService(t)
|
||||||
|
|
||||||
|
testFS := testScriptFile(t, "test.tm", `
|
||||||
|
res := session.query("some expr").unwrap()
|
||||||
|
res[0].delete_attr("deleteMe")
|
||||||
|
session.set_result_set(res)
|
||||||
|
`)
|
||||||
|
|
||||||
|
srv := scriptmanager.New(scriptmanager.WithFS(testFS))
|
||||||
|
srv.SetIFaces(scriptmanager.Ifaces{
|
||||||
|
UI: mockedUIService,
|
||||||
|
Session: mockedSessionService,
|
||||||
|
})
|
||||||
|
|
||||||
|
ctx := context.Background()
|
||||||
|
err := <-srv.RunAdHocScript(ctx, "test.tm")
|
||||||
|
assert.NoError(t, err)
|
||||||
|
|
||||||
|
mockedUIService.AssertExpectations(t)
|
||||||
|
mockedSessionService.AssertExpectations(t)
|
||||||
|
})
|
||||||
|
|
||||||
|
}
|
52
internal/dynamo-browse/services/scriptmanager/scrsched.go
Normal file
52
internal/dynamo-browse/services/scriptmanager/scrsched.go
Normal file
|
@ -0,0 +1,52 @@
|
||||||
|
package scriptmanager
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"github.com/pkg/errors"
|
||||||
|
)
|
||||||
|
|
||||||
|
type scriptScheduler struct {
|
||||||
|
jobChan chan scriptJob
|
||||||
|
}
|
||||||
|
|
||||||
|
func newScriptScheduler() *scriptScheduler {
|
||||||
|
ss := &scriptScheduler{}
|
||||||
|
ss.start()
|
||||||
|
return ss
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ss *scriptScheduler) start() {
|
||||||
|
ss.jobChan = make(chan scriptJob)
|
||||||
|
go func() {
|
||||||
|
for job := range ss.jobChan {
|
||||||
|
job.job(job.ctx)
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
}
|
||||||
|
|
||||||
|
// startJobOnceFree will submit a script execution job. The function will wait until the scheduler is free.
|
||||||
|
// The job will then run on the script goroutine and the function will return.
|
||||||
|
func (ss *scriptScheduler) startJobOnceFree(ctx context.Context, job func(ctx context.Context)) error {
|
||||||
|
select {
|
||||||
|
case ss.jobChan <- scriptJob{ctx: ctx, job: job}:
|
||||||
|
return nil
|
||||||
|
case <-ctx.Done():
|
||||||
|
return ctx.Err()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// runNow will submit a job for immediate execution. The job will run as long as the scheduler is free.
|
||||||
|
// If the scheduler is not free, an error will be returned and the job will not run.
|
||||||
|
func (ss *scriptScheduler) runNow(ctx context.Context, job func(ctx context.Context)) error {
|
||||||
|
select {
|
||||||
|
case ss.jobChan <- scriptJob{ctx: ctx, job: job}:
|
||||||
|
return nil
|
||||||
|
default:
|
||||||
|
return errors.New("a script is already running")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
type scriptJob struct {
|
||||||
|
ctx context.Context
|
||||||
|
job func(ctx context.Context)
|
||||||
|
}
|
185
internal/dynamo-browse/services/scriptmanager/service.go
Normal file
185
internal/dynamo-browse/services/scriptmanager/service.go
Normal file
|
@ -0,0 +1,185 @@
|
||||||
|
package scriptmanager
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"github.com/cloudcmds/tamarin/exec"
|
||||||
|
"github.com/cloudcmds/tamarin/scope"
|
||||||
|
"github.com/pkg/errors"
|
||||||
|
"io/fs"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Service struct {
|
||||||
|
lookupPaths []fs.FS
|
||||||
|
ifaces Ifaces
|
||||||
|
options Options
|
||||||
|
sched *scriptScheduler
|
||||||
|
plugins []*ScriptPlugin
|
||||||
|
}
|
||||||
|
|
||||||
|
func New(opts ...ServiceOption) *Service {
|
||||||
|
srv := &Service{
|
||||||
|
lookupPaths: nil,
|
||||||
|
sched: newScriptScheduler(),
|
||||||
|
}
|
||||||
|
for _, opt := range opts {
|
||||||
|
opt(srv)
|
||||||
|
}
|
||||||
|
return srv
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Service) SetLookupPaths(fs []fs.FS) {
|
||||||
|
s.lookupPaths = fs
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Service) SetDefaultOptions(options Options) {
|
||||||
|
s.options = options
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Service) SetIFaces(ifaces Ifaces) {
|
||||||
|
s.ifaces = ifaces
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Service) LoadScript(ctx context.Context, filename string) (*ScriptPlugin, error) {
|
||||||
|
resChan := make(chan loadedScriptResult)
|
||||||
|
|
||||||
|
if err := s.sched.startJobOnceFree(ctx, func(ctx context.Context) {
|
||||||
|
s.loadScript(ctx, filename, resChan)
|
||||||
|
}); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
res := <-resChan
|
||||||
|
if res.err != nil {
|
||||||
|
return nil, res.err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Look for the previous version. If one is there, replace it, otherwise add it
|
||||||
|
// TODO: this should probably be protected by a mutex
|
||||||
|
newPlugin := res.scriptPlugin
|
||||||
|
for i, p := range s.plugins {
|
||||||
|
if p.name == newPlugin.name {
|
||||||
|
s.plugins[i] = newPlugin
|
||||||
|
return newPlugin, nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
s.plugins = append(s.plugins, newPlugin)
|
||||||
|
return newPlugin, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Service) RunAdHocScript(ctx context.Context, filename string) chan error {
|
||||||
|
errChan := make(chan error)
|
||||||
|
go s.startAdHocScript(ctx, filename, errChan)
|
||||||
|
return errChan
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Service) StartAdHocScript(ctx context.Context, filename string, errChan chan error) error {
|
||||||
|
return s.sched.startJobOnceFree(ctx, func(ctx context.Context) {
|
||||||
|
s.startAdHocScript(ctx, filename, errChan)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Service) startAdHocScript(ctx context.Context, filename string, errChan chan error) {
|
||||||
|
defer close(errChan)
|
||||||
|
|
||||||
|
code, err := s.readScript(filename)
|
||||||
|
if err != nil {
|
||||||
|
errChan <- errors.Wrapf(err, "cannot load script file %v", filename)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
scp := scope.New(scope.Opts{Parent: s.parentScope()})
|
||||||
|
|
||||||
|
ctx = ctxWithOptions(ctx, s.options)
|
||||||
|
|
||||||
|
if _, err = exec.Execute(ctx, exec.Opts{
|
||||||
|
Input: string(code),
|
||||||
|
File: filename,
|
||||||
|
Scope: scp,
|
||||||
|
}); err != nil {
|
||||||
|
errChan <- errors.Wrapf(err, "script %v", filename)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
type loadedScriptResult struct {
|
||||||
|
scriptPlugin *ScriptPlugin
|
||||||
|
err error
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Service) loadScript(ctx context.Context, filename string, resChan chan loadedScriptResult) {
|
||||||
|
defer close(resChan)
|
||||||
|
|
||||||
|
code, err := s.readScript(filename)
|
||||||
|
if err != nil {
|
||||||
|
resChan <- loadedScriptResult{err: errors.Wrapf(err, "cannot load script file %v", filename)}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
newPlugin := &ScriptPlugin{
|
||||||
|
name: filepath.Base(filename),
|
||||||
|
scriptService: s,
|
||||||
|
}
|
||||||
|
|
||||||
|
scp := scope.New(scope.Opts{Parent: s.parentScope()})
|
||||||
|
|
||||||
|
(&extModule{scriptPlugin: newPlugin}).register(scp)
|
||||||
|
|
||||||
|
ctx = ctxWithOptions(ctx, s.options)
|
||||||
|
|
||||||
|
if _, err = exec.Execute(ctx, exec.Opts{
|
||||||
|
Input: string(code),
|
||||||
|
File: filename,
|
||||||
|
Scope: scp,
|
||||||
|
}); err != nil {
|
||||||
|
resChan <- loadedScriptResult{err: errors.Wrapf(err, "script %v", filename)}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
resChan <- loadedScriptResult{scriptPlugin: newPlugin}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Service) readScript(filename string) ([]byte, error) {
|
||||||
|
for _, currFS := range s.lookupPaths {
|
||||||
|
stat, err := fs.Stat(currFS, filename)
|
||||||
|
if err != nil {
|
||||||
|
if errors.Is(err, os.ErrNotExist) {
|
||||||
|
continue
|
||||||
|
} else {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
} else if stat.IsDir() {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
code, err := fs.ReadFile(currFS, filename)
|
||||||
|
if err == nil {
|
||||||
|
return code, nil
|
||||||
|
} else {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil, os.ErrNotExist
|
||||||
|
}
|
||||||
|
|
||||||
|
// LookupCommand looks up a command defined by a script.
|
||||||
|
// TODO: Command should probably accept/return a chan error to indicate that this will run in a separate goroutine
|
||||||
|
func (s *Service) LookupCommand(name string) *Command {
|
||||||
|
for _, p := range s.plugins {
|
||||||
|
if cmd, hasCmd := p.definedCommands[name]; hasCmd {
|
||||||
|
return cmd
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Service) parentScope() *scope.Scope {
|
||||||
|
scp := scope.New(scope.Opts{})
|
||||||
|
(&uiModule{uiService: s.ifaces.UI}).register(scp)
|
||||||
|
(&sessionModule{sessionService: s.ifaces.Session}).register(scp)
|
||||||
|
(&osModule{}).register(scp)
|
||||||
|
return scp
|
||||||
|
}
|
150
internal/dynamo-browse/services/scriptmanager/service_test.go
Normal file
150
internal/dynamo-browse/services/scriptmanager/service_test.go
Normal file
|
@ -0,0 +1,150 @@
|
||||||
|
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/stretchr/testify/assert"
|
||||||
|
"github.com/stretchr/testify/mock"
|
||||||
|
"io/fs"
|
||||||
|
"testing"
|
||||||
|
"testing/fstest"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestService_RunAdHocScript(t *testing.T) {
|
||||||
|
t.Run("successfully loads and executes a script", func(t *testing.T) {
|
||||||
|
testFS := testScriptFile(t, "test.tm", `
|
||||||
|
ui.print("Hello, world")
|
||||||
|
`)
|
||||||
|
|
||||||
|
mockedUIService := mocks.NewUIService(t)
|
||||||
|
mockedUIService.EXPECT().PrintMessage(mock.Anything, "Hello, world")
|
||||||
|
|
||||||
|
srv := scriptmanager.New(scriptmanager.WithFS(testFS))
|
||||||
|
srv.SetIFaces(scriptmanager.Ifaces{
|
||||||
|
UI: mockedUIService,
|
||||||
|
})
|
||||||
|
|
||||||
|
ctx := context.Background()
|
||||||
|
err := <-srv.RunAdHocScript(ctx, "test.tm")
|
||||||
|
assert.NoError(t, err)
|
||||||
|
|
||||||
|
mockedUIService.AssertExpectations(t)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestService_LoadScript(t *testing.T) {
|
||||||
|
t.Run("successfully loads a script and exposes it as a plugin", func(t *testing.T) {
|
||||||
|
testFS := testScriptFile(t, "test.tm", `
|
||||||
|
ext.command("somewhere", func(a) {
|
||||||
|
ui.print("Hello, " + a)
|
||||||
|
})
|
||||||
|
`)
|
||||||
|
|
||||||
|
ctx := context.Background()
|
||||||
|
|
||||||
|
mockedUIService := mocks.NewUIService(t)
|
||||||
|
mockedUIService.EXPECT().PrintMessage(mock.Anything, "Hello, someone")
|
||||||
|
|
||||||
|
srv := scriptmanager.New(scriptmanager.WithFS(testFS))
|
||||||
|
srv.SetIFaces(scriptmanager.Ifaces{
|
||||||
|
UI: mockedUIService,
|
||||||
|
})
|
||||||
|
|
||||||
|
plugin, err := srv.LoadScript(ctx, "test.tm")
|
||||||
|
assert.NoError(t, err)
|
||||||
|
assert.NotNil(t, plugin)
|
||||||
|
assert.Equal(t, "test.tm", plugin.Name())
|
||||||
|
|
||||||
|
cmd := srv.LookupCommand("somewhere")
|
||||||
|
assert.NotNil(t, cmd)
|
||||||
|
|
||||||
|
errChan := make(chan error)
|
||||||
|
err = cmd.Invoke(ctx, []string{"someone"}, errChan)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
assert.NoError(t, waitForErr(t, errChan))
|
||||||
|
|
||||||
|
mockedUIService.AssertExpectations(t)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("reloading a script with the same name should remove the old one", func(t *testing.T) {
|
||||||
|
testFS := fstest.MapFS{
|
||||||
|
"test.tm": &fstest.MapFile{
|
||||||
|
Data: []byte(`
|
||||||
|
ext.command("somewhere", func(a) {
|
||||||
|
ui.print("Hello, " + a)
|
||||||
|
})
|
||||||
|
`),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx := context.Background()
|
||||||
|
|
||||||
|
mockedUIService := mocks.NewUIService(t)
|
||||||
|
mockedUIService.EXPECT().PrintMessage(mock.Anything, "Hello, someone").Once()
|
||||||
|
mockedUIService.EXPECT().PrintMessage(mock.Anything, "Goodbye, someone").Once()
|
||||||
|
|
||||||
|
srv := scriptmanager.New(scriptmanager.WithFS(testFS))
|
||||||
|
srv.SetIFaces(scriptmanager.Ifaces{
|
||||||
|
UI: mockedUIService,
|
||||||
|
})
|
||||||
|
|
||||||
|
// Execute the old script
|
||||||
|
_, err := srv.LoadScript(ctx, "test.tm")
|
||||||
|
assert.NoError(t, err)
|
||||||
|
|
||||||
|
cmd := srv.LookupCommand("somewhere")
|
||||||
|
assert.NotNil(t, cmd)
|
||||||
|
|
||||||
|
errChan := make(chan error)
|
||||||
|
err = cmd.Invoke(ctx, []string{"someone"}, errChan)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
assert.NoError(t, waitForErr(t, errChan))
|
||||||
|
|
||||||
|
// Change the script and reload
|
||||||
|
testFS["test.tm"] = &fstest.MapFile{
|
||||||
|
Data: []byte(`
|
||||||
|
ext.command("somewhere", func(a) {
|
||||||
|
ui.print("Goodbye, " + a)
|
||||||
|
})
|
||||||
|
`),
|
||||||
|
}
|
||||||
|
|
||||||
|
_, err = srv.LoadScript(ctx, "test.tm")
|
||||||
|
assert.NoError(t, err)
|
||||||
|
|
||||||
|
cmd = srv.LookupCommand("somewhere")
|
||||||
|
assert.NotNil(t, cmd)
|
||||||
|
|
||||||
|
errChan = make(chan error)
|
||||||
|
err = cmd.Invoke(ctx, []string{"someone"}, errChan)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
assert.NoError(t, waitForErr(t, errChan))
|
||||||
|
|
||||||
|
mockedUIService.AssertExpectations(t)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func testScriptFile(t *testing.T, filename, code string) fs.FS {
|
||||||
|
t.Helper()
|
||||||
|
|
||||||
|
testFs := fstest.MapFS{
|
||||||
|
filename: &fstest.MapFile{
|
||||||
|
Data: []byte(code),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
return testFs
|
||||||
|
}
|
||||||
|
|
||||||
|
func waitForErr(t *testing.T, errChan chan error) error {
|
||||||
|
t.Helper()
|
||||||
|
|
||||||
|
select {
|
||||||
|
case err := <-errChan:
|
||||||
|
return err
|
||||||
|
case <-time.After(5 * time.Second):
|
||||||
|
t.Fatalf("timed-out waiting for an error")
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
11
internal/dynamo-browse/services/scriptmanager/serviceopts.go
Normal file
11
internal/dynamo-browse/services/scriptmanager/serviceopts.go
Normal file
|
@ -0,0 +1,11 @@
|
||||||
|
package scriptmanager
|
||||||
|
|
||||||
|
import "io/fs"
|
||||||
|
|
||||||
|
type ServiceOption func(srv *Service)
|
||||||
|
|
||||||
|
func WithFS(fs ...fs.FS) ServiceOption {
|
||||||
|
return func(srv *Service) {
|
||||||
|
srv.lookupPaths = fs
|
||||||
|
}
|
||||||
|
}
|
26
internal/dynamo-browse/services/scriptmanager/types.go
Normal file
26
internal/dynamo-browse/services/scriptmanager/types.go
Normal file
|
@ -0,0 +1,26 @@
|
||||||
|
package scriptmanager
|
||||||
|
|
||||||
|
import "context"
|
||||||
|
|
||||||
|
type ScriptPlugin struct {
|
||||||
|
scriptService *Service
|
||||||
|
name string
|
||||||
|
definedCommands map[string]*Command
|
||||||
|
}
|
||||||
|
|
||||||
|
func (sp *ScriptPlugin) Name() string {
|
||||||
|
return sp.name
|
||||||
|
}
|
||||||
|
|
||||||
|
type Command struct {
|
||||||
|
plugin *ScriptPlugin
|
||||||
|
cmdFn func(ctx context.Context, args []string) error
|
||||||
|
}
|
||||||
|
|
||||||
|
// Invoke will schedule the command for invocation. If the script scheduler is free, it will be started immediately.
|
||||||
|
// Otherwise an error will be returned.
|
||||||
|
func (c *Command) Invoke(ctx context.Context, args []string, errChan chan error) error {
|
||||||
|
return c.plugin.scriptService.sched.runNow(ctx, func(ctx context.Context) {
|
||||||
|
errChan <- c.cmdFn(ctx, args)
|
||||||
|
})
|
||||||
|
}
|
|
@ -27,7 +27,7 @@ func (s *ViewSnapshotService) PushSnapshot(details serialisable.ViewSnapshotDeta
|
||||||
return errors.Wrap(err, "cannot get snapshot head")
|
return errors.Wrap(err, "cannot get snapshot head")
|
||||||
}
|
}
|
||||||
|
|
||||||
if oldHead != nil && oldHead.Details == details {
|
if oldHead != nil && oldHead.Details.Equals(details, false) {
|
||||||
// Attempting to push a duplicate
|
// Attempting to push a duplicate
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,6 +1,9 @@
|
||||||
package viewsnapshot_test
|
package viewsnapshot_test
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"bytes"
|
||||||
|
"github.com/aws/aws-sdk-go-v2/service/dynamodb/types"
|
||||||
|
"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/models/serialisable"
|
||||||
"github.com/lmika/audax/internal/dynamo-browse/providers/workspacestore"
|
"github.com/lmika/audax/internal/dynamo-browse/providers/workspacestore"
|
||||||
"github.com/lmika/audax/internal/dynamo-browse/services/viewsnapshot"
|
"github.com/lmika/audax/internal/dynamo-browse/services/viewsnapshot"
|
||||||
|
@ -14,11 +17,14 @@ func TestViewSnapshotService_PushSnapshot(t *testing.T) {
|
||||||
ws := testworkspace.New(t)
|
ws := testworkspace.New(t)
|
||||||
|
|
||||||
service := viewsnapshot.NewService(workspacestore.NewResultSetSnapshotStore(ws))
|
service := viewsnapshot.NewService(workspacestore.NewResultSetSnapshotStore(ws))
|
||||||
|
q, _ := queryexpr.Parse("pk = \"abc\"")
|
||||||
|
qbs, _ := q.SerializeToBytes()
|
||||||
|
|
||||||
// Push some snapshots
|
// Push some snapshots
|
||||||
err := service.PushSnapshot(serialisable.ViewSnapshotDetails{
|
err := service.PushSnapshot(serialisable.ViewSnapshotDetails{
|
||||||
TableName: "normal-table",
|
TableName: "normal-table",
|
||||||
Query: "pk = 'abc'",
|
Query: qbs,
|
||||||
|
QueryHash: q.HashCode(),
|
||||||
Filter: "",
|
Filter: "",
|
||||||
})
|
})
|
||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
|
@ -27,9 +33,13 @@ func TestViewSnapshotService_PushSnapshot(t *testing.T) {
|
||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
assert.Equal(t, 1, cnt)
|
assert.Equal(t, 1, cnt)
|
||||||
|
|
||||||
|
q2, _ := queryexpr.Parse("another = \"test\"")
|
||||||
|
qbs2, _ := q.SerializeToBytes()
|
||||||
|
|
||||||
err = service.PushSnapshot(serialisable.ViewSnapshotDetails{
|
err = service.PushSnapshot(serialisable.ViewSnapshotDetails{
|
||||||
TableName: "abnormal-table",
|
TableName: "abnormal-table",
|
||||||
Query: "pk = 'abc'",
|
Query: qbs2,
|
||||||
|
QueryHash: q2.HashCode(),
|
||||||
Filter: "fla",
|
Filter: "fla",
|
||||||
})
|
})
|
||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
|
@ -41,7 +51,8 @@ func TestViewSnapshotService_PushSnapshot(t *testing.T) {
|
||||||
// Push a duplicate
|
// Push a duplicate
|
||||||
err = service.PushSnapshot(serialisable.ViewSnapshotDetails{
|
err = service.PushSnapshot(serialisable.ViewSnapshotDetails{
|
||||||
TableName: "abnormal-table",
|
TableName: "abnormal-table",
|
||||||
Query: "pk = 'abc'",
|
Query: qbs2,
|
||||||
|
QueryHash: q2.HashCode(),
|
||||||
Filter: "fla",
|
Filter: "fla",
|
||||||
})
|
})
|
||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
|
@ -50,4 +61,34 @@ func TestViewSnapshotService_PushSnapshot(t *testing.T) {
|
||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
assert.Equal(t, 2, cnt)
|
assert.Equal(t, 2, cnt)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
t.Run("should push expression with placeholder", func(t *testing.T) {
|
||||||
|
ws := testworkspace.New(t)
|
||||||
|
service := viewsnapshot.NewService(workspacestore.NewResultSetSnapshotStore(ws))
|
||||||
|
|
||||||
|
q, _ := queryexpr.Parse("another = $one")
|
||||||
|
q = q.WithValueParams(map[string]types.AttributeValue{
|
||||||
|
"one": &types.AttributeValueMemberS{Value: "bla-di-bla"},
|
||||||
|
})
|
||||||
|
qbs, _ := q.SerializeToBytes()
|
||||||
|
|
||||||
|
err := service.PushSnapshot(serialisable.ViewSnapshotDetails{
|
||||||
|
TableName: "abnormal-table",
|
||||||
|
Query: qbs,
|
||||||
|
QueryHash: q.HashCode(),
|
||||||
|
Filter: "fla",
|
||||||
|
})
|
||||||
|
assert.NoError(t, err)
|
||||||
|
|
||||||
|
vs, err := service.ViewRestore()
|
||||||
|
assert.NoError(t, err)
|
||||||
|
assert.Equal(t, "abnormal-table", vs.Details.TableName)
|
||||||
|
assert.Equal(t, "fla", vs.Details.Filter)
|
||||||
|
|
||||||
|
rq, err := queryexpr.DeserializeFrom(bytes.NewReader(vs.Details.Query))
|
||||||
|
assert.NoError(t, err)
|
||||||
|
assert.Equal(t, "bla-di-bla", rq.ValueParamOrNil("one").(*types.AttributeValueMemberS).Value)
|
||||||
|
assert.True(t, q.Equal(rq))
|
||||||
|
assert.Equal(t, q.HashCode(), rq.HashCode())
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
|
@ -19,6 +19,7 @@ import (
|
||||||
"github.com/lmika/audax/internal/dynamo-browse/ui/teamodels/styles"
|
"github.com/lmika/audax/internal/dynamo-browse/ui/teamodels/styles"
|
||||||
"github.com/lmika/audax/internal/dynamo-browse/ui/teamodels/tableselect"
|
"github.com/lmika/audax/internal/dynamo-browse/ui/teamodels/tableselect"
|
||||||
"github.com/lmika/audax/internal/dynamo-browse/ui/teamodels/utils"
|
"github.com/lmika/audax/internal/dynamo-browse/ui/teamodels/utils"
|
||||||
|
bus "github.com/lmika/events"
|
||||||
"github.com/pkg/errors"
|
"github.com/pkg/errors"
|
||||||
"log"
|
"log"
|
||||||
"os"
|
"os"
|
||||||
|
@ -43,11 +44,13 @@ type Model struct {
|
||||||
settingsController *controllers.SettingsController
|
settingsController *controllers.SettingsController
|
||||||
exportController *controllers.ExportController
|
exportController *controllers.ExportController
|
||||||
commandController *commandctrl.CommandController
|
commandController *commandctrl.CommandController
|
||||||
|
scriptController *controllers.ScriptController
|
||||||
jobController *controllers.JobsController
|
jobController *controllers.JobsController
|
||||||
colSelector *colselector.Model
|
colSelector *colselector.Model
|
||||||
itemEdit *dynamoitemedit.Model
|
itemEdit *dynamoitemedit.Model
|
||||||
statusAndPrompt *statusandprompt.StatusAndPrompt
|
statusAndPrompt *statusandprompt.StatusAndPrompt
|
||||||
tableSelect *tableselect.Model
|
tableSelect *tableselect.Model
|
||||||
|
eventBus *bus.Bus
|
||||||
|
|
||||||
mainViewIndex int
|
mainViewIndex int
|
||||||
|
|
||||||
|
@ -67,12 +70,14 @@ func NewModel(
|
||||||
jobController *controllers.JobsController,
|
jobController *controllers.JobsController,
|
||||||
itemRendererService *itemrenderer.Service,
|
itemRendererService *itemrenderer.Service,
|
||||||
cc *commandctrl.CommandController,
|
cc *commandctrl.CommandController,
|
||||||
|
scriptController *controllers.ScriptController,
|
||||||
|
eventBus *bus.Bus,
|
||||||
keyBindingController *controllers.KeyBindingController,
|
keyBindingController *controllers.KeyBindingController,
|
||||||
defaultKeyMap *keybindings.KeyBindings,
|
defaultKeyMap *keybindings.KeyBindings,
|
||||||
) Model {
|
) Model {
|
||||||
uiStyles := styles.DefaultStyles
|
uiStyles := styles.DefaultStyles
|
||||||
|
|
||||||
dtv := dynamotableview.New(defaultKeyMap.TableView, columnsController, settingsController, uiStyles)
|
dtv := dynamotableview.New(defaultKeyMap.TableView, columnsController, settingsController, eventBus, uiStyles)
|
||||||
div := dynamoitemview.New(itemRendererService, uiStyles)
|
div := dynamoitemview.New(itemRendererService, uiStyles)
|
||||||
mainView := layout.NewVBox(layout.LastChildFixedAt(14), dtv, div)
|
mainView := layout.NewVBox(layout.LastChildFixedAt(14), dtv, div)
|
||||||
|
|
||||||
|
@ -183,6 +188,19 @@ func NewModel(
|
||||||
return keyBindingController.Rebind(args[0], args[1], ctx.FromFile)
|
return keyBindingController.Rebind(args[0], args[1], ctx.FromFile)
|
||||||
},
|
},
|
||||||
|
|
||||||
|
"run-script": func(ctx commandctrl.ExecContext, args []string) tea.Msg {
|
||||||
|
if len(args) != 1 {
|
||||||
|
return events.Error(errors.New("expected: script name"))
|
||||||
|
}
|
||||||
|
return scriptController.RunScript(args[0])
|
||||||
|
},
|
||||||
|
"load-script": func(ctx commandctrl.ExecContext, args []string) tea.Msg {
|
||||||
|
if len(args) != 1 {
|
||||||
|
return events.Error(errors.New("expected: script name"))
|
||||||
|
}
|
||||||
|
return scriptController.LoadScript(args[0])
|
||||||
|
},
|
||||||
|
|
||||||
// Aliases
|
// Aliases
|
||||||
"unmark": cc.Alias("mark", []string{"none"}),
|
"unmark": cc.Alias("mark", []string{"none"}),
|
||||||
"sa": cc.Alias("set-attr", nil),
|
"sa": cc.Alias("set-attr", nil),
|
||||||
|
@ -198,6 +216,7 @@ func NewModel(
|
||||||
tableReadController: rc,
|
tableReadController: rc,
|
||||||
tableWriteController: wc,
|
tableWriteController: wc,
|
||||||
commandController: cc,
|
commandController: cc,
|
||||||
|
scriptController: scriptController,
|
||||||
jobController: jobController,
|
jobController: jobController,
|
||||||
itemEdit: itemEdit,
|
itemEdit: itemEdit,
|
||||||
colSelector: colSelector,
|
colSelector: colSelector,
|
||||||
|
|
|
@ -12,6 +12,7 @@ import (
|
||||||
"github.com/lmika/audax/internal/dynamo-browse/ui/teamodels/frame"
|
"github.com/lmika/audax/internal/dynamo-browse/ui/teamodels/frame"
|
||||||
"github.com/lmika/audax/internal/dynamo-browse/ui/teamodels/layout"
|
"github.com/lmika/audax/internal/dynamo-browse/ui/teamodels/layout"
|
||||||
"github.com/lmika/audax/internal/dynamo-browse/ui/teamodels/styles"
|
"github.com/lmika/audax/internal/dynamo-browse/ui/teamodels/styles"
|
||||||
|
bus "github.com/lmika/events"
|
||||||
table "github.com/lmika/go-bubble-table"
|
table "github.com/lmika/go-bubble-table"
|
||||||
"strings"
|
"strings"
|
||||||
)
|
)
|
||||||
|
@ -38,6 +39,7 @@ type Model struct {
|
||||||
keyBinding *keybindings.TableKeyBinding
|
keyBinding *keybindings.TableKeyBinding
|
||||||
setting Setting
|
setting Setting
|
||||||
columnsProvider ColumnsProvider
|
columnsProvider ColumnsProvider
|
||||||
|
bus *bus.Bus
|
||||||
|
|
||||||
// model state
|
// model state
|
||||||
isReadOnly bool
|
isReadOnly bool
|
||||||
|
@ -47,7 +49,7 @@ type Model struct {
|
||||||
resultSet *models.ResultSet
|
resultSet *models.ResultSet
|
||||||
}
|
}
|
||||||
|
|
||||||
func New(keyBinding *keybindings.TableKeyBinding, columnsProvider ColumnsProvider, setting Setting, uiStyles styles.Styles) *Model {
|
func New(keyBinding *keybindings.TableKeyBinding, columnsProvider ColumnsProvider, setting Setting, bus *bus.Bus, uiStyles styles.Styles) *Model {
|
||||||
frameTitle := frame.NewFrameTitle("No table", true, uiStyles.Frames)
|
frameTitle := frame.NewFrameTitle("No table", true, uiStyles.Frames)
|
||||||
isReadOnly := setting.IsReadOnly()
|
isReadOnly := setting.IsReadOnly()
|
||||||
|
|
||||||
|
@ -57,6 +59,7 @@ func New(keyBinding *keybindings.TableKeyBinding, columnsProvider ColumnsProvide
|
||||||
keyBinding: keyBinding,
|
keyBinding: keyBinding,
|
||||||
setting: setting,
|
setting: setting,
|
||||||
columnsProvider: columnsProvider,
|
columnsProvider: columnsProvider,
|
||||||
|
bus: bus,
|
||||||
}
|
}
|
||||||
|
|
||||||
model.table = table.New(columnModel{model}, 100, 100)
|
model.table = table.New(columnModel{model}, 100, 100)
|
||||||
|
@ -226,9 +229,11 @@ func (m *Model) selectedItem() (itemTableRow, bool) {
|
||||||
func (m *Model) postSelectedItemChanged() tea.Msg {
|
func (m *Model) postSelectedItemChanged() tea.Msg {
|
||||||
item, ok := m.selectedItem()
|
item, ok := m.selectedItem()
|
||||||
if !ok {
|
if !ok {
|
||||||
|
m.bus.Fire("ui.new-item-selected", item.resultSet, -1)
|
||||||
return dynamoitemview.NewItemSelected{ResultSet: item.resultSet, Item: nil}
|
return dynamoitemview.NewItemSelected{ResultSet: item.resultSet, Item: nil}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
m.bus.Fire("ui.new-item-selected", item.resultSet, item.itemIndex)
|
||||||
return dynamoitemview.NewItemSelected{ResultSet: item.resultSet, Item: item.item}
|
return dynamoitemview.NewItemSelected{ResultSet: item.resultSet, Item: item.item}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
3
internal/dynamo-browse/ui/teamodels/layout/events.go
Normal file
3
internal/dynamo-browse/ui/teamodels/layout/events.go
Normal file
|
@ -0,0 +1,3 @@
|
||||||
|
package layout
|
||||||
|
|
||||||
|
type RequestLayout struct{}
|
|
@ -21,8 +21,12 @@ func (f fullScreenModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
||||||
switch msg := msg.(type) {
|
switch msg := msg.(type) {
|
||||||
case tea.WindowSizeMsg:
|
case tea.WindowSizeMsg:
|
||||||
f.ready = true
|
f.ready = true
|
||||||
|
f.w, f.h = msg.Width, msg.Height
|
||||||
f.submodel = f.submodel.Resize(msg.Width, msg.Height)
|
f.submodel = f.submodel.Resize(msg.Width, msg.Height)
|
||||||
return f, nil
|
return f, nil
|
||||||
|
case RequestLayout:
|
||||||
|
f.submodel = f.submodel.Resize(f.w, f.h)
|
||||||
|
return f, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
newSubModel, cmd := f.submodel.Update(msg)
|
newSubModel, cmd := f.submodel.Update(msg)
|
||||||
|
|
|
@ -9,7 +9,6 @@ import (
|
||||||
"github.com/lmika/audax/internal/common/ui/events"
|
"github.com/lmika/audax/internal/common/ui/events"
|
||||||
"github.com/lmika/audax/internal/dynamo-browse/ui/teamodels/layout"
|
"github.com/lmika/audax/internal/dynamo-browse/ui/teamodels/layout"
|
||||||
"github.com/lmika/audax/internal/dynamo-browse/ui/teamodels/utils"
|
"github.com/lmika/audax/internal/dynamo-browse/ui/teamodels/utils"
|
||||||
"log"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
// StatusAndPrompt is a resizing model which displays a submodel and a status bar. When the start prompt
|
// StatusAndPrompt is a resizing model which displays a submodel and a status bar. When the start prompt
|
||||||
|
@ -23,7 +22,8 @@ type StatusAndPrompt struct {
|
||||||
spinnerVisible bool
|
spinnerVisible bool
|
||||||
pendingInput *events.PromptForInputMsg
|
pendingInput *events.PromptForInputMsg
|
||||||
textInput textinput.Model
|
textInput textinput.Model
|
||||||
width int
|
width, height int
|
||||||
|
lastModeLineHeight int
|
||||||
}
|
}
|
||||||
|
|
||||||
type Style struct {
|
type Style struct {
|
||||||
|
@ -84,11 +84,17 @@ func (s *StatusAndPrompt) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
||||||
s.textInput.Focus()
|
s.textInput.Focus()
|
||||||
s.textInput.SetValue("")
|
s.textInput.SetValue("")
|
||||||
s.pendingInput = &msg
|
s.pendingInput = &msg
|
||||||
return s, nil
|
|
||||||
case tea.KeyMsg:
|
case tea.KeyMsg:
|
||||||
if s.pendingInput != nil {
|
if s.pendingInput != nil {
|
||||||
switch msg.Type {
|
switch msg.Type {
|
||||||
case tea.KeyCtrlC, tea.KeyEsc:
|
case tea.KeyCtrlC, tea.KeyEsc:
|
||||||
|
if s.pendingInput.OnCancel != nil {
|
||||||
|
pendingInput := s.pendingInput
|
||||||
|
cc.Add(func() tea.Msg {
|
||||||
|
m := pendingInput.OnCancel()
|
||||||
|
return m
|
||||||
|
})
|
||||||
|
}
|
||||||
s.pendingInput = nil
|
s.pendingInput = nil
|
||||||
case tea.KeyEnter:
|
case tea.KeyEnter:
|
||||||
pendingInput := s.pendingInput
|
pendingInput := s.pendingInput
|
||||||
|
@ -96,7 +102,6 @@ func (s *StatusAndPrompt) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
||||||
|
|
||||||
return s, func() tea.Msg {
|
return s, func() tea.Msg {
|
||||||
m := pendingInput.OnDone(s.textInput.Value())
|
m := pendingInput.OnDone(s.textInput.Value())
|
||||||
log.Printf("return msg type = %T", m)
|
|
||||||
return m
|
return m
|
||||||
}
|
}
|
||||||
default:
|
default:
|
||||||
|
@ -116,6 +121,11 @@ func (s *StatusAndPrompt) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
||||||
s.spinner = cc.Collect(s.spinner.Update(msg)).(spinner.Model)
|
s.spinner = cc.Collect(s.spinner.Update(msg)).(spinner.Model)
|
||||||
}
|
}
|
||||||
s.model = cc.Collect(s.model.Update(msg)).(layout.ResizingModel)
|
s.model = cc.Collect(s.model.Update(msg)).(layout.ResizingModel)
|
||||||
|
|
||||||
|
// If the height of the modeline has changed, request a relayout
|
||||||
|
if s.lastModeLineHeight != lipgloss.Height(s.viewStatus()) {
|
||||||
|
cc.Add(events.SetTeaMessage(layout.RequestLayout{}))
|
||||||
|
}
|
||||||
return s, cc.Cmd()
|
return s, cc.Cmd()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -129,7 +139,9 @@ func (s *StatusAndPrompt) View() string {
|
||||||
|
|
||||||
func (s *StatusAndPrompt) Resize(w, h int) layout.ResizingModel {
|
func (s *StatusAndPrompt) Resize(w, h int) layout.ResizingModel {
|
||||||
s.width = w
|
s.width = w
|
||||||
submodelHeight := h - lipgloss.Height(s.viewStatus())
|
s.height = h
|
||||||
|
s.lastModeLineHeight = lipgloss.Height(s.viewStatus())
|
||||||
|
submodelHeight := h - s.lastModeLineHeight
|
||||||
s.model = s.model.Resize(w, submodelHeight)
|
s.model = s.model.Resize(w, submodelHeight)
|
||||||
return s
|
return s
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in a new issue