From 58e553d3492836e3cb3ac461c80bc3964792561c Mon Sep 17 00:00:00 2001 From: Christian Banse Date: Fri, 27 Dec 2024 15:13:55 +0100 Subject: [PATCH 01/35] Added GraphQL server --- cli/commands/securities_test.go | 5 +- go.mod | 12 +- go.sum | 34 +- gqlgen.yml | 121 + graph/generated.go | 4401 +++++++++++++++++++++++++++++++ graph/models_gen.go | 20 + graph/resolver.go | 34 + graph/schema.graphqls | 33 + graph/schema.resolvers.go | 133 + internal/persistence.go | 2 +- persistence/persistence.go | 19 +- persistence/persistence_test.go | 2 +- server/commands/init.go | 6 +- server/server.go | 28 +- 14 files changed, 4820 insertions(+), 30 deletions(-) create mode 100644 gqlgen.yml create mode 100644 graph/generated.go create mode 100644 graph/models_gen.go create mode 100644 graph/resolver.go create mode 100644 graph/schema.graphqls create mode 100644 graph/schema.resolvers.go diff --git a/cli/commands/securities_test.go b/cli/commands/securities_test.go index 6128e513..346641e0 100644 --- a/cli/commands/securities_test.go +++ b/cli/commands/securities_test.go @@ -110,14 +110,13 @@ func TestUpdateAllQuotes(t *testing.T) { func TestListSecurities(t *testing.T) { srv := servertest.NewServer(internal.NewTestDB(t, func(db *persistence.DB) { - q := persistence.New(db) - _, err := q.CreateSecurity(context.Background(), persistence.CreateSecurityParams{ + _, err := db.CreateSecurity(context.Background(), persistence.CreateSecurityParams{ ID: "1234", DisplayName: "One Two Three Four", }) assert.NoError(t, err) - _, err = q.UpsertListedSecurity(context.Background(), persistence.UpsertListedSecurityParams{ + _, err = db.UpsertListedSecurity(context.Background(), persistence.UpsertListedSecurityParams{ SecurityID: "1234", Ticker: "ONE", Currency: "USD", diff --git a/go.mod b/go.mod index 3050a9fa..84022aab 100644 --- a/go.mod +++ b/go.mod @@ -1,10 +1,13 @@ module github.com/oxisto/money-gopher -go 1.22.1 +go 1.22.5 + +toolchain go1.23.4 require ( connectrpc.com/connect v1.16.2 connectrpc.com/vanguard v0.3.0 + github.com/99designs/gqlgen v0.17.61 github.com/MicahParks/keyfunc/v3 v3.3.5 github.com/fatih/color v1.18.0 github.com/golang-jwt/jwt/v5 v5.2.1 @@ -16,6 +19,7 @@ require ( github.com/oxisto/oauth2go v0.14.0 github.com/pressly/goose/v3 v3.24.0 github.com/urfave/cli/v3 v3.0.0-beta1 + github.com/vektah/gqlparser/v2 v2.5.20 golang.org/x/net v0.33.0 golang.org/x/text v0.21.0 google.golang.org/protobuf v1.36.1 @@ -25,8 +29,12 @@ require github.com/google/go-cmp v0.6.0 // indirect require ( github.com/MicahParks/jwkset v0.5.19 // indirect + github.com/agnivade/levenshtein v1.2.0 // indirect + github.com/go-viper/mapstructure/v2 v2.2.1 // indirect + github.com/google/uuid v1.6.0 // indirect github.com/mfridman/interpolate v0.0.2 // indirect github.com/sethvargo/go-retry v0.3.0 // indirect + github.com/sosodev/duration v1.3.1 // indirect go.uber.org/multierr v1.11.0 // indirect golang.org/x/crypto v0.31.0 // indirect golang.org/x/oauth2 v0.20.0 // indirect @@ -36,3 +44,5 @@ require ( google.golang.org/genproto/googleapis/api v0.0.0-20241230172942-26aa7a208def google.golang.org/genproto/googleapis/rpc v0.0.0-20241223144023-3abc09e42ca8 // indirect ) + +replace github.com/99designs/gqlgen v0.17.61 => github.com/oxisto/gqlgen v0.17.62-0.20241227140449-4bf1c5c27bad diff --git a/go.sum b/go.sum index 6d46dbb0..623db40e 100644 --- a/go.sum +++ b/go.sum @@ -6,16 +6,28 @@ github.com/MicahParks/jwkset v0.5.19 h1:XZCsgJv05DBCvxEHYEHlSafqiuVn5ESG0VRB331F github.com/MicahParks/jwkset v0.5.19/go.mod h1:q8ptTGn/Z9c4MwbcfeCDssADeVQb3Pk7PnVxrvi+2QY= github.com/MicahParks/keyfunc/v3 v3.3.5 h1:7ceAJLUAldnoueHDNzF8Bx06oVcQ5CfJnYwNt1U3YYo= github.com/MicahParks/keyfunc/v3 v3.3.5/go.mod h1:SdCCyMJn/bYqWDvARspC6nCT8Sk74MjuAY22C7dCST8= +github.com/PuerkitoBio/goquery v1.9.3 h1:mpJr/ikUA9/GNJB/DBZcGeFDXUtosHRyRrwh7KGdTG0= +github.com/PuerkitoBio/goquery v1.9.3/go.mod h1:1ndLHPdTz+DyQPICCWYlYQMPl0oXZj0G6D4LCYA6u4U= +github.com/agnivade/levenshtein v1.2.0 h1:U9L4IOT0Y3i0TIlUIDJ7rVUziKi/zPbrJGaFrtYH3SY= +github.com/agnivade/levenshtein v1.2.0/go.mod h1:QVVI16kDrtSuwcpd0p1+xMC6Z/VfhtCyDIjcwga4/DU= +github.com/andreyvit/diff v0.0.0-20170406064948-c7f18ee00883 h1:bvNMNQO63//z+xNgfBlViaCIJKLlCJ6/fmUseuG0wVQ= +github.com/andreyvit/diff v0.0.0-20170406064948-c7f18ee00883/go.mod h1:rCTlJbsFo29Kk6CurOXKm700vrz8f0KW0JNfpkRJY/8= +github.com/andybalholm/cascadia v1.3.2 h1:3Xi6Dw5lHF15JtdcmAHD3i1+T8plmv7BQ/nsViSLyss= +github.com/andybalholm/cascadia v1.3.2/go.mod h1:7gtRlve5FxPPgIgX36uWBX58OdBsSS6lUvCFb+h7KvU= +github.com/arbovm/levenshtein v0.0.0-20160628152529-48b4e1c0c4d0 h1:jfIu9sQUG6Ig+0+Ap1h4unLjW6YQJpKZVmUzxsD4E/Q= +github.com/arbovm/levenshtein v0.0.0-20160628152529-48b4e1c0c4d0/go.mod h1:t2tdKJDJF9BV14lnkjHmOQgcvEKgtqs5a1N3LNdJhGE= 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/dgryski/trifles v0.0.0-20230903005119-f50d829f2e54 h1:SG7nF6SRlWhcT7cNTs5R6Hk4V2lcmLz2NsG2VnInyNo= +github.com/dgryski/trifles v0.0.0-20230903005119-f50d829f2e54/go.mod h1:if7Fbed8SFyPtHLHbg49SI7NAdJiC5WIA09pe59rfAA= github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY= github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto= github.com/fatih/color v1.18.0 h1:S8gINlzdQ840/4pfAwic/ZE0djQEH3wM94VfqLTZcOM= github.com/fatih/color v1.18.0/go.mod h1:4FelSpRwEGDpQ12mAdzqdOukCy4u8WUtOY6lkT/6HfU= +github.com/go-viper/mapstructure/v2 v2.2.1 h1:ZAaOCxANMuZx5RCeg0mBdEZk7DZasvvZIxtHqx8aGss= +github.com/go-viper/mapstructure/v2 v2.2.1/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM= github.com/golang-jwt/jwt/v5 v5.2.1 h1:OuVbFODueb089Lh128TAcimifWaLhJwVflnrgM17wHk= github.com/golang-jwt/jwt/v5 v5.2.1/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk= -github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38= -github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= @@ -35,12 +47,10 @@ github.com/mfridman/interpolate v0.0.2 h1:pnuTK7MQIxxFz1Gr+rjSIx9u7qVjf5VOoM/u6B github.com/mfridman/interpolate v0.0.2/go.mod h1:p+7uk6oE07mpE/Ik1b8EckO0O4ZXiGAfshKBWLUM9Xg= github.com/ncruces/go-strftime v0.1.9 h1:bY0MQC28UADQmHmaF5dgpLmImcShSi2kHU9XLdhx/f4= github.com/ncruces/go-strftime v0.1.9/go.mod h1:Fwc5htZGVVkseilnfgOVb9mKy6w1naJmn9CehxcKcls= -github.com/oxisto/assert v0.0.6 h1:Z/wRt0qndURRof+eOGr7GbcJ6BHZT2nyZd9diuZHS8o= -github.com/oxisto/assert v0.0.6/go.mod h1:07ANKfyBm6j+pZk1qArFueno6fCoEGKvPbPeJSQkH3s= -github.com/oxisto/assert v0.1.1 h1:y9A0ymf7i3RdAYF4CBYc+jRU+wBaVmsp1RJ4Yd73csw= -github.com/oxisto/assert v0.1.1/go.mod h1:3vg52jeU6iN+pplw4n2C+zHNit9+04Wr9qqty4EU9Mc= github.com/oxisto/assert v0.1.2 h1:atb9lmltuakIcA/K7QvXbXKBSWsXKaVFFBZL8u1icHk= github.com/oxisto/assert v0.1.2/go.mod h1:3vg52jeU6iN+pplw4n2C+zHNit9+04Wr9qqty4EU9Mc= +github.com/oxisto/gqlgen v0.17.62-0.20241227140449-4bf1c5c27bad h1:DHqTk1/Am9hh5JBfLSZir5ve8NNQe0/LrwlEAf3vx/E= +github.com/oxisto/gqlgen v0.17.62-0.20241227140449-4bf1c5c27bad/go.mod h1:vBeGFxYsZAp+OZ7DkbyiMwai2BnRsdIvIDhd6YN7olM= github.com/oxisto/oauth2go v0.14.0 h1:VjMJCBC3TxnXPEANWWsZudKlGYh06YgBl49fb5JhavM= github.com/oxisto/oauth2go v0.14.0/go.mod h1:8mUk9Gsrh4xgzrVLsliSGi/X3+ZQkXztfK+dpdAkRZM= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= @@ -49,12 +59,18 @@ github.com/pressly/goose/v3 v3.24.0 h1:sFbNms7Bd++2VMq6HSgDHDLWa7kHz1qXzPb3ZIU72 github.com/pressly/goose/v3 v3.24.0/go.mod h1:rEWreU9uVtt0DHCyLzF9gRcWiiTF/V+528DV+4DORug= github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec h1:W09IVJc94icq4NjY3clb7Lk8O1qJ8BdBEF8z0ibU0rE= github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo= +github.com/sergi/go-diff v1.3.1 h1:xkr+Oxo4BOQKmkn/B9eMK0g5Kg/983T9DqqPHwYqD+8= +github.com/sergi/go-diff v1.3.1/go.mod h1:aMJSSKb2lpPvRNec0+w3fl7LP9IOFzdc9Pa4NFbPK1I= github.com/sethvargo/go-retry v0.3.0 h1:EEt31A35QhrcRZtrYFDTBg91cqZVnFL2navjDrah2SE= github.com/sethvargo/go-retry v0.3.0/go.mod h1:mNX17F0C/HguQMyMyJxcnU471gOZGxCLyYaFyAZraas= +github.com/sosodev/duration v1.3.1 h1:qtHBDMQ6lvMQsL15g4aopM4HEfOaYuhWBw3NPTtlqq4= +github.com/sosodev/duration v1.3.1/go.mod h1:RQIBBX0+fMLc/D9+Jb/fwvVmo0eZvDDEERAikUR6SDg= github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= github.com/urfave/cli/v3 v3.0.0-beta1 h1:6DTaaUarcM0wX7qj5Hcvs+5Dm3dyUTBbEwIWAjcw9Zg= github.com/urfave/cli/v3 v3.0.0-beta1/go.mod h1:FnIeEMYu+ko8zP1F9Ypr3xkZMIDqW3DR92yUtY39q1Y= +github.com/vektah/gqlparser/v2 v2.5.20 h1:kPaWbhBntxoZPaNdBaIPT1Kh0i1b/onb5kXgEdP5JCo= +github.com/vektah/gqlparser/v2 v2.5.20/go.mod h1:xMl+ta8a5M1Yo1A1Iwt/k7gSpscwSnHZdw7tfhEGfTM= go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0= go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y= golang.org/x/crypto v0.31.0 h1:ihbySMvVjLAeSH1IbfcRTkD/iNscyz8rGzjF/E5hV6U= @@ -73,18 +89,12 @@ golang.org/x/text v0.21.0 h1:zyQAAkrwaneQ066sspRyJaG9VNi/YJ1NfzcGB3hZ/qo= golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ= golang.org/x/time v0.5.0 h1:o7cqy6amK/52YcAKIPlM3a+Fpj35zvRj2TP+e1xFSfk= golang.org/x/time v0.5.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM= -google.golang.org/genproto/googleapis/api v0.0.0-20241223144023-3abc09e42ca8 h1:st3LcW/BPi75W4q1jJTEor/QWwbNlPlDG0JTn6XhZu0= -google.golang.org/genproto/googleapis/api v0.0.0-20241223144023-3abc09e42ca8/go.mod h1:klhJGKFyG8Tn50enBn7gizg4nXGXJ+jqEREdCWaPcV4= google.golang.org/genproto/googleapis/api v0.0.0-20241230172942-26aa7a208def h1:0Km0hi+g2KXbXL0+riZzSCKz23f4MmwicuEb00JeonI= google.golang.org/genproto/googleapis/api v0.0.0-20241230172942-26aa7a208def/go.mod h1:u2DoMSpCXjrzqLdobRccQMc9wrnMAJ1DLng0a2yqM2Q= -google.golang.org/genproto/googleapis/rpc v0.0.0-20241219192143-6b3ec007d9bb h1:3oy2tynMOP1QbTC0MsNNAV+Se8M2Bd0A5+x1QHyw+pI= -google.golang.org/genproto/googleapis/rpc v0.0.0-20241219192143-6b3ec007d9bb/go.mod h1:lcTa1sDdWEIHMWlITnIczmw5w60CF9ffkb8Z+DVmmjA= google.golang.org/genproto/googleapis/rpc v0.0.0-20241223144023-3abc09e42ca8 h1:TqExAhdPaB60Ux47Cn0oLV07rGnxZzIsaRhQaqS666A= google.golang.org/genproto/googleapis/rpc v0.0.0-20241223144023-3abc09e42ca8/go.mod h1:lcTa1sDdWEIHMWlITnIczmw5w60CF9ffkb8Z+DVmmjA= google.golang.org/grpc v1.67.1 h1:zWnc1Vrcno+lHZCOofnIMvycFcc0QRGIzm9dhnDX68E= google.golang.org/grpc v1.67.1/go.mod h1:1gLDyUQU7CTLJI90u3nXZ9ekeghjeM7pTDZlqFNg2AA= -google.golang.org/protobuf v1.36.0 h1:mjIs9gYtt56AzC4ZaffQuh88TZurBGhIJMBZGSxNerQ= -google.golang.org/protobuf v1.36.0/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE= google.golang.org/protobuf v1.36.1 h1:yBPeRvTftaleIgM3PZ/WBIZ7XM/eEYAaEyCwvyjq/gk= google.golang.org/protobuf v1.36.1/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= diff --git a/gqlgen.yml b/gqlgen.yml new file mode 100644 index 00000000..6063bda4 --- /dev/null +++ b/gqlgen.yml @@ -0,0 +1,121 @@ +# Where are all the schema files located? globs are supported eg src/**/*.graphqls +schema: + - graph/*.graphqls + +# Where should the generated server code go? +exec: + package: graph + layout: single-file # Only other option is "follow-schema," ie multi-file. + + # Only for single-file layout: + filename: graph/generated.go + + # Only for follow-schema layout: + # dir: graph + # filename_template: "{name}.generated.go" + + # Optional: Maximum number of goroutines in concurrency to use per child resolvers(default: unlimited) + # worker_limit: 1000 + +# Uncomment to enable federation +# federation: +# filename: graph/federation.go +# package: graph +# version: 2 +# options: +# computed_requires: true + +# Where should any generated models go? +model: + filename: graph/models_gen.go + package: graph + + # Optional: Pass in a path to a new gotpl template to use for generating the models + # model_template: [your/path/model.gotpl] + +# Where should the resolver implementations go? +resolver: + package: graph + layout: follow-schema # Only other option is "single-file." + + # Only for single-file layout: + # filename: graph/resolver.go + + # Only for follow-schema layout: + dir: graph + filename_template: "{name}.resolvers.go" + + # Optional: turn on to not generate template comments above resolvers + omit_template_comment: false + + # Optional: Pass in a path to a new gotpl template to use for generating resolvers + # resolver_template: [your/path/resolver.gotpl] + # Optional: turn on to avoid rewriting existing resolver(s) when generating + # preserve_resolver: false + + # Optional: turn on use ` + "`" + `gqlgen:"fieldName"` + "`" + ` tags in your models + # struct_tag: json + +# Optional: turn on to use []Thing instead of []*Thing +# omit_slice_element_pointers: true + +# Optional: turn on to omit Is() methods to interface and unions +# omit_interface_checks: true + +# Optional: turn on to skip generation of ComplexityRoot struct content and Complexity function +# omit_complexity: false + +# Optional: turn on to not generate any file notice comments in generated files +# omit_gqlgen_file_notice: false + +# Optional: turn on to exclude the gqlgen version in the generated file notice. No effect if `omit_gqlgen_file_notice` is true. +# omit_gqlgen_version_in_file_notice: false + +# Optional: turn on to exclude root models such as Query and Mutation from the generated models file. +# omit_root_models: false + +# Optional: turn on to exclude resolver fields from the generated models file. +# omit_resolver_fields: false + +# Optional: turn off to make struct-type struct fields not use pointers +# e.g. type Thing struct { FieldA OtherThing } instead of { FieldA *OtherThing } +# struct_fields_always_pointers: true + +# Optional: turn off to make resolvers return values instead of pointers for structs +# resolvers_always_return_pointers: true + +# Optional: turn on to return pointers instead of values in unmarshalInput +# return_pointers_in_unmarshalinput: false + +# Optional: wrap nullable input fields with Omittable +# nullable_input_omittable: true + +# Optional: set to speed up generation time by not performing a final validation pass. +# skip_validation: true + +# Optional: set to skip running `go mod tidy` when generating server code +# skip_mod_tidy: true + +# Optional: if this is set to true, argument directives that +# decorate a field with a null value will still be called. +# +# This enables argumment directives to not just mutate +# argument values but to set them even if they're null. +call_argument_directives_with_null: true + +# Optional: set build tags that will be used to load packages +# go_build_tags: +# - private +# - enterprise + +# Optional: set to modify the initialisms regarded for Go names +# go_initialisms: +# replace_defaults: false # if true, the default initialisms will get dropped in favor of the new ones instead of being added +# initialisms: # List of initialisms to for Go names +# - 'CC' +# - 'BCC' + +# gqlgen will search for any type names in the schema in these go packages +# if they match it will use them, otherwise it will generate them. +autobind: + - "github.com/oxisto/money-gopher/persistence" diff --git a/graph/generated.go b/graph/generated.go new file mode 100644 index 00000000..93aaffe2 --- /dev/null +++ b/graph/generated.go @@ -0,0 +1,4401 @@ +// Code generated by github.com/99designs/gqlgen, DO NOT EDIT. + +package graph + +import ( + "bytes" + "context" + "embed" + "errors" + "fmt" + "strconv" + "sync" + "sync/atomic" + + "github.com/99designs/gqlgen/graphql" + "github.com/99designs/gqlgen/graphql/introspection" + "github.com/oxisto/money-gopher/persistence" + gqlparser "github.com/vektah/gqlparser/v2" + "github.com/vektah/gqlparser/v2/ast" +) + +// region ************************** generated!.gotpl ************************** + +// NewExecutableSchema creates an ExecutableSchema from the ResolverRoot interface. +func NewExecutableSchema(cfg Config) graphql.ExecutableSchema { + return &executableSchema{ + schema: cfg.Schema, + resolvers: cfg.Resolvers, + directives: cfg.Directives, + complexity: cfg.Complexity, + } +} + +type Config struct { + Schema *ast.Schema + Resolvers ResolverRoot + Directives DirectiveRoot + Complexity ComplexityRoot +} + +type ResolverRoot interface { + ListedSecurity() ListedSecurityResolver + Mutation() MutationResolver + Query() QueryResolver + Security() SecurityResolver +} + +type DirectiveRoot struct { +} + +type ComplexityRoot struct { + ListedSecurity struct { + Currency func(childComplexity int) int + Security func(childComplexity int) int + Ticker func(childComplexity int) int + } + + Mutation struct { + CreateSecurity func(childComplexity int, input SecurityInput) int + UpdateSecurity func(childComplexity int, id string, input SecurityInput) int + } + + Query struct { + Securities func(childComplexity int) int + Security func(childComplexity int, id string) int + } + + Security struct { + DisplayName func(childComplexity int) int + ID func(childComplexity int) int + ListedAs func(childComplexity int) int + QuoteProvider func(childComplexity int) int + } +} + +type ListedSecurityResolver interface { + Security(ctx context.Context, obj *persistence.ListedSecurity) (*persistence.Security, error) +} +type MutationResolver interface { + CreateSecurity(ctx context.Context, input SecurityInput) (*persistence.Security, error) + UpdateSecurity(ctx context.Context, id string, input SecurityInput) (*persistence.Security, error) +} +type QueryResolver interface { + Security(ctx context.Context, id string) (*persistence.Security, error) + Securities(ctx context.Context) ([]*persistence.Security, error) +} +type SecurityResolver interface { + QuoteProvider(ctx context.Context, obj *persistence.Security) (*string, error) + ListedAs(ctx context.Context, obj *persistence.Security) ([]*persistence.ListedSecurity, error) +} + +type executableSchema struct { + schema *ast.Schema + resolvers ResolverRoot + directives DirectiveRoot + complexity ComplexityRoot +} + +func (e *executableSchema) Schema() *ast.Schema { + if e.schema != nil { + return e.schema + } + return parsedSchema +} + +func (e *executableSchema) Complexity(typeName, field string, childComplexity int, rawArgs map[string]any) (int, bool) { + ec := executionContext{nil, e, 0, 0, nil} + _ = ec + switch typeName + "." + field { + + case "ListedSecurity.currency": + if e.complexity.ListedSecurity.Currency == nil { + break + } + + return e.complexity.ListedSecurity.Currency(childComplexity), true + + case "ListedSecurity.security": + if e.complexity.ListedSecurity.Security == nil { + break + } + + return e.complexity.ListedSecurity.Security(childComplexity), true + + case "ListedSecurity.ticker": + if e.complexity.ListedSecurity.Ticker == nil { + break + } + + return e.complexity.ListedSecurity.Ticker(childComplexity), true + + case "Mutation.createSecurity": + if e.complexity.Mutation.CreateSecurity == nil { + break + } + + args, err := ec.field_Mutation_createSecurity_args(context.TODO(), rawArgs) + if err != nil { + return 0, false + } + + return e.complexity.Mutation.CreateSecurity(childComplexity, args["input"].(SecurityInput)), true + + case "Mutation.updateSecurity": + if e.complexity.Mutation.UpdateSecurity == nil { + break + } + + args, err := ec.field_Mutation_updateSecurity_args(context.TODO(), rawArgs) + if err != nil { + return 0, false + } + + return e.complexity.Mutation.UpdateSecurity(childComplexity, args["id"].(string), args["input"].(SecurityInput)), true + + case "Query.securities": + if e.complexity.Query.Securities == nil { + break + } + + return e.complexity.Query.Securities(childComplexity), true + + case "Query.security": + if e.complexity.Query.Security == nil { + break + } + + args, err := ec.field_Query_security_args(context.TODO(), rawArgs) + if err != nil { + return 0, false + } + + return e.complexity.Query.Security(childComplexity, args["id"].(string)), true + + case "Security.displayName": + if e.complexity.Security.DisplayName == nil { + break + } + + return e.complexity.Security.DisplayName(childComplexity), true + + case "Security.id": + if e.complexity.Security.ID == nil { + break + } + + return e.complexity.Security.ID(childComplexity), true + + case "Security.listedAs": + if e.complexity.Security.ListedAs == nil { + break + } + + return e.complexity.Security.ListedAs(childComplexity), true + + case "Security.quoteProvider": + if e.complexity.Security.QuoteProvider == nil { + break + } + + return e.complexity.Security.QuoteProvider(childComplexity), true + + } + return 0, false +} + +func (e *executableSchema) Exec(ctx context.Context) graphql.ResponseHandler { + opCtx := graphql.GetOperationContext(ctx) + ec := executionContext{opCtx, e, 0, 0, make(chan graphql.DeferredResult)} + inputUnmarshalMap := graphql.BuildUnmarshalerMap( + ec.unmarshalInputListedSecurityInput, + ec.unmarshalInputSecurityInput, + ) + first := true + + switch opCtx.Operation.Operation { + case ast.Query: + return func(ctx context.Context) *graphql.Response { + var response graphql.Response + var data graphql.Marshaler + if first { + first = false + ctx = graphql.WithUnmarshalerMap(ctx, inputUnmarshalMap) + data = ec._Query(ctx, opCtx.Operation.SelectionSet) + } else { + if atomic.LoadInt32(&ec.pendingDeferred) > 0 { + result := <-ec.deferredResults + atomic.AddInt32(&ec.pendingDeferred, -1) + data = result.Result + response.Path = result.Path + response.Label = result.Label + response.Errors = result.Errors + } else { + return nil + } + } + var buf bytes.Buffer + data.MarshalGQL(&buf) + response.Data = buf.Bytes() + if atomic.LoadInt32(&ec.deferred) > 0 { + hasNext := atomic.LoadInt32(&ec.pendingDeferred) > 0 + response.HasNext = &hasNext + } + + return &response + } + case ast.Mutation: + return func(ctx context.Context) *graphql.Response { + if !first { + return nil + } + first = false + ctx = graphql.WithUnmarshalerMap(ctx, inputUnmarshalMap) + data := ec._Mutation(ctx, opCtx.Operation.SelectionSet) + var buf bytes.Buffer + data.MarshalGQL(&buf) + + return &graphql.Response{ + Data: buf.Bytes(), + } + } + + default: + return graphql.OneShot(graphql.ErrorResponse(ctx, "unsupported GraphQL operation")) + } +} + +type executionContext struct { + *graphql.OperationContext + *executableSchema + deferred int32 + pendingDeferred int32 + deferredResults chan graphql.DeferredResult +} + +func (ec *executionContext) processDeferredGroup(dg graphql.DeferredGroup) { + atomic.AddInt32(&ec.pendingDeferred, 1) + go func() { + ctx := graphql.WithFreshResponseContext(dg.Context) + dg.FieldSet.Dispatch(ctx) + ds := graphql.DeferredResult{ + Path: dg.Path, + Label: dg.Label, + Result: dg.FieldSet, + Errors: graphql.GetErrors(ctx), + } + // null fields should bubble up + if dg.FieldSet.Invalids > 0 { + ds.Result = graphql.Null + } + ec.deferredResults <- ds + }() +} + +func (ec *executionContext) introspectSchema() (*introspection.Schema, error) { + if ec.DisableIntrospection { + return nil, errors.New("introspection disabled") + } + return introspection.WrapSchema(ec.Schema()), nil +} + +func (ec *executionContext) introspectType(name string) (*introspection.Type, error) { + if ec.DisableIntrospection { + return nil, errors.New("introspection disabled") + } + return introspection.WrapTypeFromDef(ec.Schema(), ec.Schema().Types[name]), nil +} + +//go:embed "schema.graphqls" +var sourcesFS embed.FS + +func sourceData(filename string) string { + data, err := sourcesFS.ReadFile(filename) + if err != nil { + panic(fmt.Sprintf("codegen problem: %s not available", filename)) + } + return string(data) +} + +var sources = []*ast.Source{ + {Name: "schema.graphqls", Input: sourceData("schema.graphqls"), BuiltIn: false}, +} +var parsedSchema = gqlparser.MustLoadSchema(sources...) + +// endregion ************************** generated!.gotpl ************************** + +// region ***************************** args.gotpl ***************************** + +func (ec *executionContext) field_Mutation_createSecurity_args(ctx context.Context, rawArgs map[string]any) (map[string]any, error) { + var err error + args := map[string]any{} + arg0, err := ec.field_Mutation_createSecurity_argsInput(ctx, rawArgs) + if err != nil { + return nil, err + } + args["input"] = arg0 + return args, nil +} +func (ec *executionContext) field_Mutation_createSecurity_argsInput( + ctx context.Context, + rawArgs map[string]any, +) (SecurityInput, error) { + ctx = graphql.WithPathContext(ctx, graphql.NewPathWithField("input")) + if tmp, ok := rawArgs["input"]; ok { + return ec.unmarshalNSecurityInput2githubᚗcomᚋoxistoᚋmoneyᚑgopherᚋgraphᚐSecurityInput(ctx, tmp) + } + + var zeroVal SecurityInput + return zeroVal, nil +} + +func (ec *executionContext) field_Mutation_updateSecurity_args(ctx context.Context, rawArgs map[string]any) (map[string]any, error) { + var err error + args := map[string]any{} + arg0, err := ec.field_Mutation_updateSecurity_argsID(ctx, rawArgs) + if err != nil { + return nil, err + } + args["id"] = arg0 + arg1, err := ec.field_Mutation_updateSecurity_argsInput(ctx, rawArgs) + if err != nil { + return nil, err + } + args["input"] = arg1 + return args, nil +} +func (ec *executionContext) field_Mutation_updateSecurity_argsID( + ctx context.Context, + rawArgs map[string]any, +) (string, error) { + ctx = graphql.WithPathContext(ctx, graphql.NewPathWithField("id")) + if tmp, ok := rawArgs["id"]; ok { + return ec.unmarshalNID2string(ctx, tmp) + } + + var zeroVal string + return zeroVal, nil +} + +func (ec *executionContext) field_Mutation_updateSecurity_argsInput( + ctx context.Context, + rawArgs map[string]any, +) (SecurityInput, error) { + ctx = graphql.WithPathContext(ctx, graphql.NewPathWithField("input")) + if tmp, ok := rawArgs["input"]; ok { + return ec.unmarshalNSecurityInput2githubᚗcomᚋoxistoᚋmoneyᚑgopherᚋgraphᚐSecurityInput(ctx, tmp) + } + + var zeroVal SecurityInput + return zeroVal, nil +} + +func (ec *executionContext) field_Query___type_args(ctx context.Context, rawArgs map[string]any) (map[string]any, error) { + var err error + args := map[string]any{} + arg0, err := ec.field_Query___type_argsName(ctx, rawArgs) + if err != nil { + return nil, err + } + args["name"] = arg0 + return args, nil +} +func (ec *executionContext) field_Query___type_argsName( + ctx context.Context, + rawArgs map[string]any, +) (string, error) { + ctx = graphql.WithPathContext(ctx, graphql.NewPathWithField("name")) + if tmp, ok := rawArgs["name"]; ok { + return ec.unmarshalNString2string(ctx, tmp) + } + + var zeroVal string + return zeroVal, nil +} + +func (ec *executionContext) field_Query_security_args(ctx context.Context, rawArgs map[string]any) (map[string]any, error) { + var err error + args := map[string]any{} + arg0, err := ec.field_Query_security_argsID(ctx, rawArgs) + if err != nil { + return nil, err + } + args["id"] = arg0 + return args, nil +} +func (ec *executionContext) field_Query_security_argsID( + ctx context.Context, + rawArgs map[string]any, +) (string, error) { + ctx = graphql.WithPathContext(ctx, graphql.NewPathWithField("id")) + if tmp, ok := rawArgs["id"]; ok { + return ec.unmarshalNString2string(ctx, tmp) + } + + var zeroVal string + return zeroVal, nil +} + +func (ec *executionContext) field___Type_enumValues_args(ctx context.Context, rawArgs map[string]any) (map[string]any, error) { + var err error + args := map[string]any{} + arg0, err := ec.field___Type_enumValues_argsIncludeDeprecated(ctx, rawArgs) + if err != nil { + return nil, err + } + args["includeDeprecated"] = arg0 + return args, nil +} +func (ec *executionContext) field___Type_enumValues_argsIncludeDeprecated( + ctx context.Context, + rawArgs map[string]any, +) (bool, error) { + ctx = graphql.WithPathContext(ctx, graphql.NewPathWithField("includeDeprecated")) + if tmp, ok := rawArgs["includeDeprecated"]; ok { + return ec.unmarshalOBoolean2bool(ctx, tmp) + } + + var zeroVal bool + return zeroVal, nil +} + +func (ec *executionContext) field___Type_fields_args(ctx context.Context, rawArgs map[string]any) (map[string]any, error) { + var err error + args := map[string]any{} + arg0, err := ec.field___Type_fields_argsIncludeDeprecated(ctx, rawArgs) + if err != nil { + return nil, err + } + args["includeDeprecated"] = arg0 + return args, nil +} +func (ec *executionContext) field___Type_fields_argsIncludeDeprecated( + ctx context.Context, + rawArgs map[string]any, +) (bool, error) { + ctx = graphql.WithPathContext(ctx, graphql.NewPathWithField("includeDeprecated")) + if tmp, ok := rawArgs["includeDeprecated"]; ok { + return ec.unmarshalOBoolean2bool(ctx, tmp) + } + + var zeroVal bool + return zeroVal, nil +} + +// endregion ***************************** args.gotpl ***************************** + +// region ************************** directives.gotpl ************************** + +// endregion ************************** directives.gotpl ************************** + +// region **************************** field.gotpl ***************************** + +func (ec *executionContext) _ListedSecurity_ticker(ctx context.Context, field graphql.CollectedField, obj *persistence.ListedSecurity) (ret graphql.Marshaler) { + fc, err := ec.fieldContext_ListedSecurity_ticker(ctx, field) + if err != nil { + return graphql.Null + } + ctx = graphql.WithFieldContext(ctx, fc) + defer func() { + if r := recover(); r != nil { + ec.Error(ctx, ec.Recover(ctx, r)) + ret = graphql.Null + } + }() + resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (any, error) { + ctx = rctx // use context from middleware stack in children + return obj.Ticker, nil + }) + if err != nil { + ec.Error(ctx, err) + return graphql.Null + } + if resTmp == nil { + if !graphql.HasFieldError(ctx, fc) { + ec.Errorf(ctx, "must not be null") + } + return graphql.Null + } + res := resTmp.(string) + fc.Result = res + return ec.marshalNString2string(ctx, field.Selections, res) +} + +func (ec *executionContext) fieldContext_ListedSecurity_ticker(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { + fc = &graphql.FieldContext{ + Object: "ListedSecurity", + Field: field, + IsMethod: false, + IsResolver: false, + Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + return nil, errors.New("field of type String does not have child fields") + }, + } + return fc, nil +} + +func (ec *executionContext) _ListedSecurity_currency(ctx context.Context, field graphql.CollectedField, obj *persistence.ListedSecurity) (ret graphql.Marshaler) { + fc, err := ec.fieldContext_ListedSecurity_currency(ctx, field) + if err != nil { + return graphql.Null + } + ctx = graphql.WithFieldContext(ctx, fc) + defer func() { + if r := recover(); r != nil { + ec.Error(ctx, ec.Recover(ctx, r)) + ret = graphql.Null + } + }() + resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (any, error) { + ctx = rctx // use context from middleware stack in children + return obj.Currency, nil + }) + if err != nil { + ec.Error(ctx, err) + return graphql.Null + } + if resTmp == nil { + if !graphql.HasFieldError(ctx, fc) { + ec.Errorf(ctx, "must not be null") + } + return graphql.Null + } + res := resTmp.(string) + fc.Result = res + return ec.marshalNString2string(ctx, field.Selections, res) +} + +func (ec *executionContext) fieldContext_ListedSecurity_currency(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { + fc = &graphql.FieldContext{ + Object: "ListedSecurity", + Field: field, + IsMethod: false, + IsResolver: false, + Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + return nil, errors.New("field of type String does not have child fields") + }, + } + return fc, nil +} + +func (ec *executionContext) _ListedSecurity_security(ctx context.Context, field graphql.CollectedField, obj *persistence.ListedSecurity) (ret graphql.Marshaler) { + fc, err := ec.fieldContext_ListedSecurity_security(ctx, field) + if err != nil { + return graphql.Null + } + ctx = graphql.WithFieldContext(ctx, fc) + defer func() { + if r := recover(); r != nil { + ec.Error(ctx, ec.Recover(ctx, r)) + ret = graphql.Null + } + }() + resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (any, error) { + ctx = rctx // use context from middleware stack in children + return ec.resolvers.ListedSecurity().Security(rctx, obj) + }) + if err != nil { + ec.Error(ctx, err) + return graphql.Null + } + if resTmp == nil { + if !graphql.HasFieldError(ctx, fc) { + ec.Errorf(ctx, "must not be null") + } + return graphql.Null + } + res := resTmp.(*persistence.Security) + fc.Result = res + return ec.marshalNSecurity2ᚖgithubᚗcomᚋoxistoᚋmoneyᚑgopherᚋpersistenceᚐSecurity(ctx, field.Selections, res) +} + +func (ec *executionContext) fieldContext_ListedSecurity_security(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { + fc = &graphql.FieldContext{ + Object: "ListedSecurity", + Field: field, + IsMethod: true, + IsResolver: true, + Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + switch field.Name { + case "id": + return ec.fieldContext_Security_id(ctx, field) + case "displayName": + return ec.fieldContext_Security_displayName(ctx, field) + case "quoteProvider": + return ec.fieldContext_Security_quoteProvider(ctx, field) + case "listedAs": + return ec.fieldContext_Security_listedAs(ctx, field) + } + return nil, fmt.Errorf("no field named %q was found under type Security", field.Name) + }, + } + return fc, nil +} + +func (ec *executionContext) _Mutation_createSecurity(ctx context.Context, field graphql.CollectedField) (ret graphql.Marshaler) { + fc, err := ec.fieldContext_Mutation_createSecurity(ctx, field) + if err != nil { + return graphql.Null + } + ctx = graphql.WithFieldContext(ctx, fc) + defer func() { + if r := recover(); r != nil { + ec.Error(ctx, ec.Recover(ctx, r)) + ret = graphql.Null + } + }() + resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (any, error) { + ctx = rctx // use context from middleware stack in children + return ec.resolvers.Mutation().CreateSecurity(rctx, fc.Args["input"].(SecurityInput)) + }) + if err != nil { + ec.Error(ctx, err) + return graphql.Null + } + if resTmp == nil { + if !graphql.HasFieldError(ctx, fc) { + ec.Errorf(ctx, "must not be null") + } + return graphql.Null + } + res := resTmp.(*persistence.Security) + fc.Result = res + return ec.marshalNSecurity2ᚖgithubᚗcomᚋoxistoᚋmoneyᚑgopherᚋpersistenceᚐSecurity(ctx, field.Selections, res) +} + +func (ec *executionContext) fieldContext_Mutation_createSecurity(ctx context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { + fc = &graphql.FieldContext{ + Object: "Mutation", + Field: field, + IsMethod: true, + IsResolver: true, + Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + switch field.Name { + case "id": + return ec.fieldContext_Security_id(ctx, field) + case "displayName": + return ec.fieldContext_Security_displayName(ctx, field) + case "quoteProvider": + return ec.fieldContext_Security_quoteProvider(ctx, field) + case "listedAs": + return ec.fieldContext_Security_listedAs(ctx, field) + } + return nil, fmt.Errorf("no field named %q was found under type Security", field.Name) + }, + } + defer func() { + if r := recover(); r != nil { + err = ec.Recover(ctx, r) + ec.Error(ctx, err) + } + }() + ctx = graphql.WithFieldContext(ctx, fc) + if fc.Args, err = ec.field_Mutation_createSecurity_args(ctx, field.ArgumentMap(ec.Variables)); err != nil { + ec.Error(ctx, err) + return fc, err + } + return fc, nil +} + +func (ec *executionContext) _Mutation_updateSecurity(ctx context.Context, field graphql.CollectedField) (ret graphql.Marshaler) { + fc, err := ec.fieldContext_Mutation_updateSecurity(ctx, field) + if err != nil { + return graphql.Null + } + ctx = graphql.WithFieldContext(ctx, fc) + defer func() { + if r := recover(); r != nil { + ec.Error(ctx, ec.Recover(ctx, r)) + ret = graphql.Null + } + }() + resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (any, error) { + ctx = rctx // use context from middleware stack in children + return ec.resolvers.Mutation().UpdateSecurity(rctx, fc.Args["id"].(string), fc.Args["input"].(SecurityInput)) + }) + if err != nil { + ec.Error(ctx, err) + return graphql.Null + } + if resTmp == nil { + if !graphql.HasFieldError(ctx, fc) { + ec.Errorf(ctx, "must not be null") + } + return graphql.Null + } + res := resTmp.(*persistence.Security) + fc.Result = res + return ec.marshalNSecurity2ᚖgithubᚗcomᚋoxistoᚋmoneyᚑgopherᚋpersistenceᚐSecurity(ctx, field.Selections, res) +} + +func (ec *executionContext) fieldContext_Mutation_updateSecurity(ctx context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { + fc = &graphql.FieldContext{ + Object: "Mutation", + Field: field, + IsMethod: true, + IsResolver: true, + Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + switch field.Name { + case "id": + return ec.fieldContext_Security_id(ctx, field) + case "displayName": + return ec.fieldContext_Security_displayName(ctx, field) + case "quoteProvider": + return ec.fieldContext_Security_quoteProvider(ctx, field) + case "listedAs": + return ec.fieldContext_Security_listedAs(ctx, field) + } + return nil, fmt.Errorf("no field named %q was found under type Security", field.Name) + }, + } + defer func() { + if r := recover(); r != nil { + err = ec.Recover(ctx, r) + ec.Error(ctx, err) + } + }() + ctx = graphql.WithFieldContext(ctx, fc) + if fc.Args, err = ec.field_Mutation_updateSecurity_args(ctx, field.ArgumentMap(ec.Variables)); err != nil { + ec.Error(ctx, err) + return fc, err + } + return fc, nil +} + +func (ec *executionContext) _Query_security(ctx context.Context, field graphql.CollectedField) (ret graphql.Marshaler) { + fc, err := ec.fieldContext_Query_security(ctx, field) + if err != nil { + return graphql.Null + } + ctx = graphql.WithFieldContext(ctx, fc) + defer func() { + if r := recover(); r != nil { + ec.Error(ctx, ec.Recover(ctx, r)) + ret = graphql.Null + } + }() + resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (any, error) { + ctx = rctx // use context from middleware stack in children + return ec.resolvers.Query().Security(rctx, fc.Args["id"].(string)) + }) + if err != nil { + ec.Error(ctx, err) + return graphql.Null + } + if resTmp == nil { + return graphql.Null + } + res := resTmp.(*persistence.Security) + fc.Result = res + return ec.marshalOSecurity2ᚖgithubᚗcomᚋoxistoᚋmoneyᚑgopherᚋpersistenceᚐSecurity(ctx, field.Selections, res) +} + +func (ec *executionContext) fieldContext_Query_security(ctx context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { + fc = &graphql.FieldContext{ + Object: "Query", + Field: field, + IsMethod: true, + IsResolver: true, + Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + switch field.Name { + case "id": + return ec.fieldContext_Security_id(ctx, field) + case "displayName": + return ec.fieldContext_Security_displayName(ctx, field) + case "quoteProvider": + return ec.fieldContext_Security_quoteProvider(ctx, field) + case "listedAs": + return ec.fieldContext_Security_listedAs(ctx, field) + } + return nil, fmt.Errorf("no field named %q was found under type Security", field.Name) + }, + } + defer func() { + if r := recover(); r != nil { + err = ec.Recover(ctx, r) + ec.Error(ctx, err) + } + }() + ctx = graphql.WithFieldContext(ctx, fc) + if fc.Args, err = ec.field_Query_security_args(ctx, field.ArgumentMap(ec.Variables)); err != nil { + ec.Error(ctx, err) + return fc, err + } + return fc, nil +} + +func (ec *executionContext) _Query_securities(ctx context.Context, field graphql.CollectedField) (ret graphql.Marshaler) { + fc, err := ec.fieldContext_Query_securities(ctx, field) + if err != nil { + return graphql.Null + } + ctx = graphql.WithFieldContext(ctx, fc) + defer func() { + if r := recover(); r != nil { + ec.Error(ctx, ec.Recover(ctx, r)) + ret = graphql.Null + } + }() + resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (any, error) { + ctx = rctx // use context from middleware stack in children + return ec.resolvers.Query().Securities(rctx) + }) + if err != nil { + ec.Error(ctx, err) + return graphql.Null + } + if resTmp == nil { + if !graphql.HasFieldError(ctx, fc) { + ec.Errorf(ctx, "must not be null") + } + return graphql.Null + } + res := resTmp.([]*persistence.Security) + fc.Result = res + return ec.marshalNSecurity2ᚕᚖgithubᚗcomᚋoxistoᚋmoneyᚑgopherᚋpersistenceᚐSecurityᚄ(ctx, field.Selections, res) +} + +func (ec *executionContext) fieldContext_Query_securities(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { + fc = &graphql.FieldContext{ + Object: "Query", + Field: field, + IsMethod: true, + IsResolver: true, + Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + switch field.Name { + case "id": + return ec.fieldContext_Security_id(ctx, field) + case "displayName": + return ec.fieldContext_Security_displayName(ctx, field) + case "quoteProvider": + return ec.fieldContext_Security_quoteProvider(ctx, field) + case "listedAs": + return ec.fieldContext_Security_listedAs(ctx, field) + } + return nil, fmt.Errorf("no field named %q was found under type Security", field.Name) + }, + } + return fc, nil +} + +func (ec *executionContext) _Query___type(ctx context.Context, field graphql.CollectedField) (ret graphql.Marshaler) { + fc, err := ec.fieldContext_Query___type(ctx, field) + if err != nil { + return graphql.Null + } + ctx = graphql.WithFieldContext(ctx, fc) + defer func() { + if r := recover(); r != nil { + ec.Error(ctx, ec.Recover(ctx, r)) + ret = graphql.Null + } + }() + resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (any, error) { + ctx = rctx // use context from middleware stack in children + return ec.introspectType(fc.Args["name"].(string)) + }) + if err != nil { + ec.Error(ctx, err) + return graphql.Null + } + if resTmp == nil { + return graphql.Null + } + res := resTmp.(*introspection.Type) + fc.Result = res + return ec.marshalO__Type2ᚖgithubᚗcomᚋ99designsᚋgqlgenᚋgraphqlᚋintrospectionᚐType(ctx, field.Selections, res) +} + +func (ec *executionContext) fieldContext_Query___type(ctx context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { + fc = &graphql.FieldContext{ + Object: "Query", + Field: field, + IsMethod: true, + IsResolver: false, + Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + switch field.Name { + case "kind": + return ec.fieldContext___Type_kind(ctx, field) + case "name": + return ec.fieldContext___Type_name(ctx, field) + case "description": + return ec.fieldContext___Type_description(ctx, field) + case "fields": + return ec.fieldContext___Type_fields(ctx, field) + case "interfaces": + return ec.fieldContext___Type_interfaces(ctx, field) + case "possibleTypes": + return ec.fieldContext___Type_possibleTypes(ctx, field) + case "enumValues": + return ec.fieldContext___Type_enumValues(ctx, field) + case "inputFields": + return ec.fieldContext___Type_inputFields(ctx, field) + case "ofType": + return ec.fieldContext___Type_ofType(ctx, field) + case "specifiedByURL": + return ec.fieldContext___Type_specifiedByURL(ctx, field) + } + return nil, fmt.Errorf("no field named %q was found under type __Type", field.Name) + }, + } + defer func() { + if r := recover(); r != nil { + err = ec.Recover(ctx, r) + ec.Error(ctx, err) + } + }() + ctx = graphql.WithFieldContext(ctx, fc) + if fc.Args, err = ec.field_Query___type_args(ctx, field.ArgumentMap(ec.Variables)); err != nil { + ec.Error(ctx, err) + return fc, err + } + return fc, nil +} + +func (ec *executionContext) _Query___schema(ctx context.Context, field graphql.CollectedField) (ret graphql.Marshaler) { + fc, err := ec.fieldContext_Query___schema(ctx, field) + if err != nil { + return graphql.Null + } + ctx = graphql.WithFieldContext(ctx, fc) + defer func() { + if r := recover(); r != nil { + ec.Error(ctx, ec.Recover(ctx, r)) + ret = graphql.Null + } + }() + resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (any, error) { + ctx = rctx // use context from middleware stack in children + return ec.introspectSchema() + }) + if err != nil { + ec.Error(ctx, err) + return graphql.Null + } + if resTmp == nil { + return graphql.Null + } + res := resTmp.(*introspection.Schema) + fc.Result = res + return ec.marshalO__Schema2ᚖgithubᚗcomᚋ99designsᚋgqlgenᚋgraphqlᚋintrospectionᚐSchema(ctx, field.Selections, res) +} + +func (ec *executionContext) fieldContext_Query___schema(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { + fc = &graphql.FieldContext{ + Object: "Query", + Field: field, + IsMethod: true, + IsResolver: false, + Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + switch field.Name { + case "description": + return ec.fieldContext___Schema_description(ctx, field) + case "types": + return ec.fieldContext___Schema_types(ctx, field) + case "queryType": + return ec.fieldContext___Schema_queryType(ctx, field) + case "mutationType": + return ec.fieldContext___Schema_mutationType(ctx, field) + case "subscriptionType": + return ec.fieldContext___Schema_subscriptionType(ctx, field) + case "directives": + return ec.fieldContext___Schema_directives(ctx, field) + } + return nil, fmt.Errorf("no field named %q was found under type __Schema", field.Name) + }, + } + return fc, nil +} + +func (ec *executionContext) _Security_id(ctx context.Context, field graphql.CollectedField, obj *persistence.Security) (ret graphql.Marshaler) { + fc, err := ec.fieldContext_Security_id(ctx, field) + if err != nil { + return graphql.Null + } + ctx = graphql.WithFieldContext(ctx, fc) + defer func() { + if r := recover(); r != nil { + ec.Error(ctx, ec.Recover(ctx, r)) + ret = graphql.Null + } + }() + resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (any, error) { + ctx = rctx // use context from middleware stack in children + return obj.ID, nil + }) + if err != nil { + ec.Error(ctx, err) + return graphql.Null + } + if resTmp == nil { + if !graphql.HasFieldError(ctx, fc) { + ec.Errorf(ctx, "must not be null") + } + return graphql.Null + } + res := resTmp.(string) + fc.Result = res + return ec.marshalNString2string(ctx, field.Selections, res) +} + +func (ec *executionContext) fieldContext_Security_id(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { + fc = &graphql.FieldContext{ + Object: "Security", + Field: field, + IsMethod: false, + IsResolver: false, + Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + return nil, errors.New("field of type String does not have child fields") + }, + } + return fc, nil +} + +func (ec *executionContext) _Security_displayName(ctx context.Context, field graphql.CollectedField, obj *persistence.Security) (ret graphql.Marshaler) { + fc, err := ec.fieldContext_Security_displayName(ctx, field) + if err != nil { + return graphql.Null + } + ctx = graphql.WithFieldContext(ctx, fc) + defer func() { + if r := recover(); r != nil { + ec.Error(ctx, ec.Recover(ctx, r)) + ret = graphql.Null + } + }() + resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (any, error) { + ctx = rctx // use context from middleware stack in children + return obj.DisplayName, nil + }) + if err != nil { + ec.Error(ctx, err) + return graphql.Null + } + if resTmp == nil { + if !graphql.HasFieldError(ctx, fc) { + ec.Errorf(ctx, "must not be null") + } + return graphql.Null + } + res := resTmp.(string) + fc.Result = res + return ec.marshalNString2string(ctx, field.Selections, res) +} + +func (ec *executionContext) fieldContext_Security_displayName(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { + fc = &graphql.FieldContext{ + Object: "Security", + Field: field, + IsMethod: false, + IsResolver: false, + Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + return nil, errors.New("field of type String does not have child fields") + }, + } + return fc, nil +} + +func (ec *executionContext) _Security_quoteProvider(ctx context.Context, field graphql.CollectedField, obj *persistence.Security) (ret graphql.Marshaler) { + fc, err := ec.fieldContext_Security_quoteProvider(ctx, field) + if err != nil { + return graphql.Null + } + ctx = graphql.WithFieldContext(ctx, fc) + defer func() { + if r := recover(); r != nil { + ec.Error(ctx, ec.Recover(ctx, r)) + ret = graphql.Null + } + }() + resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (any, error) { + ctx = rctx // use context from middleware stack in children + return ec.resolvers.Security().QuoteProvider(rctx, obj) + }) + if err != nil { + ec.Error(ctx, err) + return graphql.Null + } + if resTmp == nil { + return graphql.Null + } + res := resTmp.(*string) + fc.Result = res + return ec.marshalOString2ᚖstring(ctx, field.Selections, res) +} + +func (ec *executionContext) fieldContext_Security_quoteProvider(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { + fc = &graphql.FieldContext{ + Object: "Security", + Field: field, + IsMethod: true, + IsResolver: true, + Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + return nil, errors.New("field of type String does not have child fields") + }, + } + return fc, nil +} + +func (ec *executionContext) _Security_listedAs(ctx context.Context, field graphql.CollectedField, obj *persistence.Security) (ret graphql.Marshaler) { + fc, err := ec.fieldContext_Security_listedAs(ctx, field) + if err != nil { + return graphql.Null + } + ctx = graphql.WithFieldContext(ctx, fc) + defer func() { + if r := recover(); r != nil { + ec.Error(ctx, ec.Recover(ctx, r)) + ret = graphql.Null + } + }() + resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (any, error) { + ctx = rctx // use context from middleware stack in children + return ec.resolvers.Security().ListedAs(rctx, obj) + }) + if err != nil { + ec.Error(ctx, err) + return graphql.Null + } + if resTmp == nil { + return graphql.Null + } + res := resTmp.([]*persistence.ListedSecurity) + fc.Result = res + return ec.marshalOListedSecurity2ᚕᚖgithubᚗcomᚋoxistoᚋmoneyᚑgopherᚋpersistenceᚐListedSecurityᚄ(ctx, field.Selections, res) +} + +func (ec *executionContext) fieldContext_Security_listedAs(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { + fc = &graphql.FieldContext{ + Object: "Security", + Field: field, + IsMethod: true, + IsResolver: true, + Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + switch field.Name { + case "ticker": + return ec.fieldContext_ListedSecurity_ticker(ctx, field) + case "currency": + return ec.fieldContext_ListedSecurity_currency(ctx, field) + case "security": + return ec.fieldContext_ListedSecurity_security(ctx, field) + } + return nil, fmt.Errorf("no field named %q was found under type ListedSecurity", field.Name) + }, + } + return fc, nil +} + +func (ec *executionContext) ___Directive_name(ctx context.Context, field graphql.CollectedField, obj *introspection.Directive) (ret graphql.Marshaler) { + fc, err := ec.fieldContext___Directive_name(ctx, field) + if err != nil { + return graphql.Null + } + ctx = graphql.WithFieldContext(ctx, fc) + defer func() { + if r := recover(); r != nil { + ec.Error(ctx, ec.Recover(ctx, r)) + ret = graphql.Null + } + }() + resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (any, error) { + ctx = rctx // use context from middleware stack in children + return obj.Name, nil + }) + if err != nil { + ec.Error(ctx, err) + return graphql.Null + } + if resTmp == nil { + if !graphql.HasFieldError(ctx, fc) { + ec.Errorf(ctx, "must not be null") + } + return graphql.Null + } + res := resTmp.(string) + fc.Result = res + return ec.marshalNString2string(ctx, field.Selections, res) +} + +func (ec *executionContext) fieldContext___Directive_name(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { + fc = &graphql.FieldContext{ + Object: "__Directive", + Field: field, + IsMethod: false, + IsResolver: false, + Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + return nil, errors.New("field of type String does not have child fields") + }, + } + return fc, nil +} + +func (ec *executionContext) ___Directive_description(ctx context.Context, field graphql.CollectedField, obj *introspection.Directive) (ret graphql.Marshaler) { + fc, err := ec.fieldContext___Directive_description(ctx, field) + if err != nil { + return graphql.Null + } + ctx = graphql.WithFieldContext(ctx, fc) + defer func() { + if r := recover(); r != nil { + ec.Error(ctx, ec.Recover(ctx, r)) + ret = graphql.Null + } + }() + resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (any, error) { + ctx = rctx // use context from middleware stack in children + return obj.Description(), nil + }) + if err != nil { + ec.Error(ctx, err) + return graphql.Null + } + if resTmp == nil { + return graphql.Null + } + res := resTmp.(*string) + fc.Result = res + return ec.marshalOString2ᚖstring(ctx, field.Selections, res) +} + +func (ec *executionContext) fieldContext___Directive_description(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { + fc = &graphql.FieldContext{ + Object: "__Directive", + Field: field, + IsMethod: true, + IsResolver: false, + Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + return nil, errors.New("field of type String does not have child fields") + }, + } + return fc, nil +} + +func (ec *executionContext) ___Directive_locations(ctx context.Context, field graphql.CollectedField, obj *introspection.Directive) (ret graphql.Marshaler) { + fc, err := ec.fieldContext___Directive_locations(ctx, field) + if err != nil { + return graphql.Null + } + ctx = graphql.WithFieldContext(ctx, fc) + defer func() { + if r := recover(); r != nil { + ec.Error(ctx, ec.Recover(ctx, r)) + ret = graphql.Null + } + }() + resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (any, error) { + ctx = rctx // use context from middleware stack in children + return obj.Locations, nil + }) + if err != nil { + ec.Error(ctx, err) + return graphql.Null + } + if resTmp == nil { + if !graphql.HasFieldError(ctx, fc) { + ec.Errorf(ctx, "must not be null") + } + return graphql.Null + } + res := resTmp.([]string) + fc.Result = res + return ec.marshalN__DirectiveLocation2ᚕstringᚄ(ctx, field.Selections, res) +} + +func (ec *executionContext) fieldContext___Directive_locations(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { + fc = &graphql.FieldContext{ + Object: "__Directive", + Field: field, + IsMethod: false, + IsResolver: false, + Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + return nil, errors.New("field of type __DirectiveLocation does not have child fields") + }, + } + return fc, nil +} + +func (ec *executionContext) ___Directive_args(ctx context.Context, field graphql.CollectedField, obj *introspection.Directive) (ret graphql.Marshaler) { + fc, err := ec.fieldContext___Directive_args(ctx, field) + if err != nil { + return graphql.Null + } + ctx = graphql.WithFieldContext(ctx, fc) + defer func() { + if r := recover(); r != nil { + ec.Error(ctx, ec.Recover(ctx, r)) + ret = graphql.Null + } + }() + resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (any, error) { + ctx = rctx // use context from middleware stack in children + return obj.Args, nil + }) + if err != nil { + ec.Error(ctx, err) + return graphql.Null + } + if resTmp == nil { + if !graphql.HasFieldError(ctx, fc) { + ec.Errorf(ctx, "must not be null") + } + return graphql.Null + } + res := resTmp.([]introspection.InputValue) + fc.Result = res + return ec.marshalN__InputValue2ᚕgithubᚗcomᚋ99designsᚋgqlgenᚋgraphqlᚋintrospectionᚐInputValueᚄ(ctx, field.Selections, res) +} + +func (ec *executionContext) fieldContext___Directive_args(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { + fc = &graphql.FieldContext{ + Object: "__Directive", + Field: field, + IsMethod: false, + IsResolver: false, + Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + switch field.Name { + case "name": + return ec.fieldContext___InputValue_name(ctx, field) + case "description": + return ec.fieldContext___InputValue_description(ctx, field) + case "type": + return ec.fieldContext___InputValue_type(ctx, field) + case "defaultValue": + return ec.fieldContext___InputValue_defaultValue(ctx, field) + } + return nil, fmt.Errorf("no field named %q was found under type __InputValue", field.Name) + }, + } + return fc, nil +} + +func (ec *executionContext) ___Directive_isRepeatable(ctx context.Context, field graphql.CollectedField, obj *introspection.Directive) (ret graphql.Marshaler) { + fc, err := ec.fieldContext___Directive_isRepeatable(ctx, field) + if err != nil { + return graphql.Null + } + ctx = graphql.WithFieldContext(ctx, fc) + defer func() { + if r := recover(); r != nil { + ec.Error(ctx, ec.Recover(ctx, r)) + ret = graphql.Null + } + }() + resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (any, error) { + ctx = rctx // use context from middleware stack in children + return obj.IsRepeatable, nil + }) + if err != nil { + ec.Error(ctx, err) + return graphql.Null + } + if resTmp == nil { + if !graphql.HasFieldError(ctx, fc) { + ec.Errorf(ctx, "must not be null") + } + return graphql.Null + } + res := resTmp.(bool) + fc.Result = res + return ec.marshalNBoolean2bool(ctx, field.Selections, res) +} + +func (ec *executionContext) fieldContext___Directive_isRepeatable(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { + fc = &graphql.FieldContext{ + Object: "__Directive", + Field: field, + IsMethod: false, + IsResolver: false, + Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + return nil, errors.New("field of type Boolean does not have child fields") + }, + } + return fc, nil +} + +func (ec *executionContext) ___EnumValue_name(ctx context.Context, field graphql.CollectedField, obj *introspection.EnumValue) (ret graphql.Marshaler) { + fc, err := ec.fieldContext___EnumValue_name(ctx, field) + if err != nil { + return graphql.Null + } + ctx = graphql.WithFieldContext(ctx, fc) + defer func() { + if r := recover(); r != nil { + ec.Error(ctx, ec.Recover(ctx, r)) + ret = graphql.Null + } + }() + resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (any, error) { + ctx = rctx // use context from middleware stack in children + return obj.Name, nil + }) + if err != nil { + ec.Error(ctx, err) + return graphql.Null + } + if resTmp == nil { + if !graphql.HasFieldError(ctx, fc) { + ec.Errorf(ctx, "must not be null") + } + return graphql.Null + } + res := resTmp.(string) + fc.Result = res + return ec.marshalNString2string(ctx, field.Selections, res) +} + +func (ec *executionContext) fieldContext___EnumValue_name(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { + fc = &graphql.FieldContext{ + Object: "__EnumValue", + Field: field, + IsMethod: false, + IsResolver: false, + Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + return nil, errors.New("field of type String does not have child fields") + }, + } + return fc, nil +} + +func (ec *executionContext) ___EnumValue_description(ctx context.Context, field graphql.CollectedField, obj *introspection.EnumValue) (ret graphql.Marshaler) { + fc, err := ec.fieldContext___EnumValue_description(ctx, field) + if err != nil { + return graphql.Null + } + ctx = graphql.WithFieldContext(ctx, fc) + defer func() { + if r := recover(); r != nil { + ec.Error(ctx, ec.Recover(ctx, r)) + ret = graphql.Null + } + }() + resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (any, error) { + ctx = rctx // use context from middleware stack in children + return obj.Description(), nil + }) + if err != nil { + ec.Error(ctx, err) + return graphql.Null + } + if resTmp == nil { + return graphql.Null + } + res := resTmp.(*string) + fc.Result = res + return ec.marshalOString2ᚖstring(ctx, field.Selections, res) +} + +func (ec *executionContext) fieldContext___EnumValue_description(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { + fc = &graphql.FieldContext{ + Object: "__EnumValue", + Field: field, + IsMethod: true, + IsResolver: false, + Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + return nil, errors.New("field of type String does not have child fields") + }, + } + return fc, nil +} + +func (ec *executionContext) ___EnumValue_isDeprecated(ctx context.Context, field graphql.CollectedField, obj *introspection.EnumValue) (ret graphql.Marshaler) { + fc, err := ec.fieldContext___EnumValue_isDeprecated(ctx, field) + if err != nil { + return graphql.Null + } + ctx = graphql.WithFieldContext(ctx, fc) + defer func() { + if r := recover(); r != nil { + ec.Error(ctx, ec.Recover(ctx, r)) + ret = graphql.Null + } + }() + resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (any, error) { + ctx = rctx // use context from middleware stack in children + return obj.IsDeprecated(), nil + }) + if err != nil { + ec.Error(ctx, err) + return graphql.Null + } + if resTmp == nil { + if !graphql.HasFieldError(ctx, fc) { + ec.Errorf(ctx, "must not be null") + } + return graphql.Null + } + res := resTmp.(bool) + fc.Result = res + return ec.marshalNBoolean2bool(ctx, field.Selections, res) +} + +func (ec *executionContext) fieldContext___EnumValue_isDeprecated(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { + fc = &graphql.FieldContext{ + Object: "__EnumValue", + Field: field, + IsMethod: true, + IsResolver: false, + Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + return nil, errors.New("field of type Boolean does not have child fields") + }, + } + return fc, nil +} + +func (ec *executionContext) ___EnumValue_deprecationReason(ctx context.Context, field graphql.CollectedField, obj *introspection.EnumValue) (ret graphql.Marshaler) { + fc, err := ec.fieldContext___EnumValue_deprecationReason(ctx, field) + if err != nil { + return graphql.Null + } + ctx = graphql.WithFieldContext(ctx, fc) + defer func() { + if r := recover(); r != nil { + ec.Error(ctx, ec.Recover(ctx, r)) + ret = graphql.Null + } + }() + resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (any, error) { + ctx = rctx // use context from middleware stack in children + return obj.DeprecationReason(), nil + }) + if err != nil { + ec.Error(ctx, err) + return graphql.Null + } + if resTmp == nil { + return graphql.Null + } + res := resTmp.(*string) + fc.Result = res + return ec.marshalOString2ᚖstring(ctx, field.Selections, res) +} + +func (ec *executionContext) fieldContext___EnumValue_deprecationReason(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { + fc = &graphql.FieldContext{ + Object: "__EnumValue", + Field: field, + IsMethod: true, + IsResolver: false, + Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + return nil, errors.New("field of type String does not have child fields") + }, + } + return fc, nil +} + +func (ec *executionContext) ___Field_name(ctx context.Context, field graphql.CollectedField, obj *introspection.Field) (ret graphql.Marshaler) { + fc, err := ec.fieldContext___Field_name(ctx, field) + if err != nil { + return graphql.Null + } + ctx = graphql.WithFieldContext(ctx, fc) + defer func() { + if r := recover(); r != nil { + ec.Error(ctx, ec.Recover(ctx, r)) + ret = graphql.Null + } + }() + resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (any, error) { + ctx = rctx // use context from middleware stack in children + return obj.Name, nil + }) + if err != nil { + ec.Error(ctx, err) + return graphql.Null + } + if resTmp == nil { + if !graphql.HasFieldError(ctx, fc) { + ec.Errorf(ctx, "must not be null") + } + return graphql.Null + } + res := resTmp.(string) + fc.Result = res + return ec.marshalNString2string(ctx, field.Selections, res) +} + +func (ec *executionContext) fieldContext___Field_name(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { + fc = &graphql.FieldContext{ + Object: "__Field", + Field: field, + IsMethod: false, + IsResolver: false, + Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + return nil, errors.New("field of type String does not have child fields") + }, + } + return fc, nil +} + +func (ec *executionContext) ___Field_description(ctx context.Context, field graphql.CollectedField, obj *introspection.Field) (ret graphql.Marshaler) { + fc, err := ec.fieldContext___Field_description(ctx, field) + if err != nil { + return graphql.Null + } + ctx = graphql.WithFieldContext(ctx, fc) + defer func() { + if r := recover(); r != nil { + ec.Error(ctx, ec.Recover(ctx, r)) + ret = graphql.Null + } + }() + resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (any, error) { + ctx = rctx // use context from middleware stack in children + return obj.Description(), nil + }) + if err != nil { + ec.Error(ctx, err) + return graphql.Null + } + if resTmp == nil { + return graphql.Null + } + res := resTmp.(*string) + fc.Result = res + return ec.marshalOString2ᚖstring(ctx, field.Selections, res) +} + +func (ec *executionContext) fieldContext___Field_description(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { + fc = &graphql.FieldContext{ + Object: "__Field", + Field: field, + IsMethod: true, + IsResolver: false, + Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + return nil, errors.New("field of type String does not have child fields") + }, + } + return fc, nil +} + +func (ec *executionContext) ___Field_args(ctx context.Context, field graphql.CollectedField, obj *introspection.Field) (ret graphql.Marshaler) { + fc, err := ec.fieldContext___Field_args(ctx, field) + if err != nil { + return graphql.Null + } + ctx = graphql.WithFieldContext(ctx, fc) + defer func() { + if r := recover(); r != nil { + ec.Error(ctx, ec.Recover(ctx, r)) + ret = graphql.Null + } + }() + resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (any, error) { + ctx = rctx // use context from middleware stack in children + return obj.Args, nil + }) + if err != nil { + ec.Error(ctx, err) + return graphql.Null + } + if resTmp == nil { + if !graphql.HasFieldError(ctx, fc) { + ec.Errorf(ctx, "must not be null") + } + return graphql.Null + } + res := resTmp.([]introspection.InputValue) + fc.Result = res + return ec.marshalN__InputValue2ᚕgithubᚗcomᚋ99designsᚋgqlgenᚋgraphqlᚋintrospectionᚐInputValueᚄ(ctx, field.Selections, res) +} + +func (ec *executionContext) fieldContext___Field_args(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { + fc = &graphql.FieldContext{ + Object: "__Field", + Field: field, + IsMethod: false, + IsResolver: false, + Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + switch field.Name { + case "name": + return ec.fieldContext___InputValue_name(ctx, field) + case "description": + return ec.fieldContext___InputValue_description(ctx, field) + case "type": + return ec.fieldContext___InputValue_type(ctx, field) + case "defaultValue": + return ec.fieldContext___InputValue_defaultValue(ctx, field) + } + return nil, fmt.Errorf("no field named %q was found under type __InputValue", field.Name) + }, + } + return fc, nil +} + +func (ec *executionContext) ___Field_type(ctx context.Context, field graphql.CollectedField, obj *introspection.Field) (ret graphql.Marshaler) { + fc, err := ec.fieldContext___Field_type(ctx, field) + if err != nil { + return graphql.Null + } + ctx = graphql.WithFieldContext(ctx, fc) + defer func() { + if r := recover(); r != nil { + ec.Error(ctx, ec.Recover(ctx, r)) + ret = graphql.Null + } + }() + resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (any, error) { + ctx = rctx // use context from middleware stack in children + return obj.Type, nil + }) + if err != nil { + ec.Error(ctx, err) + return graphql.Null + } + if resTmp == nil { + if !graphql.HasFieldError(ctx, fc) { + ec.Errorf(ctx, "must not be null") + } + return graphql.Null + } + res := resTmp.(*introspection.Type) + fc.Result = res + return ec.marshalN__Type2ᚖgithubᚗcomᚋ99designsᚋgqlgenᚋgraphqlᚋintrospectionᚐType(ctx, field.Selections, res) +} + +func (ec *executionContext) fieldContext___Field_type(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { + fc = &graphql.FieldContext{ + Object: "__Field", + Field: field, + IsMethod: false, + IsResolver: false, + Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + switch field.Name { + case "kind": + return ec.fieldContext___Type_kind(ctx, field) + case "name": + return ec.fieldContext___Type_name(ctx, field) + case "description": + return ec.fieldContext___Type_description(ctx, field) + case "fields": + return ec.fieldContext___Type_fields(ctx, field) + case "interfaces": + return ec.fieldContext___Type_interfaces(ctx, field) + case "possibleTypes": + return ec.fieldContext___Type_possibleTypes(ctx, field) + case "enumValues": + return ec.fieldContext___Type_enumValues(ctx, field) + case "inputFields": + return ec.fieldContext___Type_inputFields(ctx, field) + case "ofType": + return ec.fieldContext___Type_ofType(ctx, field) + case "specifiedByURL": + return ec.fieldContext___Type_specifiedByURL(ctx, field) + } + return nil, fmt.Errorf("no field named %q was found under type __Type", field.Name) + }, + } + return fc, nil +} + +func (ec *executionContext) ___Field_isDeprecated(ctx context.Context, field graphql.CollectedField, obj *introspection.Field) (ret graphql.Marshaler) { + fc, err := ec.fieldContext___Field_isDeprecated(ctx, field) + if err != nil { + return graphql.Null + } + ctx = graphql.WithFieldContext(ctx, fc) + defer func() { + if r := recover(); r != nil { + ec.Error(ctx, ec.Recover(ctx, r)) + ret = graphql.Null + } + }() + resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (any, error) { + ctx = rctx // use context from middleware stack in children + return obj.IsDeprecated(), nil + }) + if err != nil { + ec.Error(ctx, err) + return graphql.Null + } + if resTmp == nil { + if !graphql.HasFieldError(ctx, fc) { + ec.Errorf(ctx, "must not be null") + } + return graphql.Null + } + res := resTmp.(bool) + fc.Result = res + return ec.marshalNBoolean2bool(ctx, field.Selections, res) +} + +func (ec *executionContext) fieldContext___Field_isDeprecated(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { + fc = &graphql.FieldContext{ + Object: "__Field", + Field: field, + IsMethod: true, + IsResolver: false, + Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + return nil, errors.New("field of type Boolean does not have child fields") + }, + } + return fc, nil +} + +func (ec *executionContext) ___Field_deprecationReason(ctx context.Context, field graphql.CollectedField, obj *introspection.Field) (ret graphql.Marshaler) { + fc, err := ec.fieldContext___Field_deprecationReason(ctx, field) + if err != nil { + return graphql.Null + } + ctx = graphql.WithFieldContext(ctx, fc) + defer func() { + if r := recover(); r != nil { + ec.Error(ctx, ec.Recover(ctx, r)) + ret = graphql.Null + } + }() + resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (any, error) { + ctx = rctx // use context from middleware stack in children + return obj.DeprecationReason(), nil + }) + if err != nil { + ec.Error(ctx, err) + return graphql.Null + } + if resTmp == nil { + return graphql.Null + } + res := resTmp.(*string) + fc.Result = res + return ec.marshalOString2ᚖstring(ctx, field.Selections, res) +} + +func (ec *executionContext) fieldContext___Field_deprecationReason(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { + fc = &graphql.FieldContext{ + Object: "__Field", + Field: field, + IsMethod: true, + IsResolver: false, + Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + return nil, errors.New("field of type String does not have child fields") + }, + } + return fc, nil +} + +func (ec *executionContext) ___InputValue_name(ctx context.Context, field graphql.CollectedField, obj *introspection.InputValue) (ret graphql.Marshaler) { + fc, err := ec.fieldContext___InputValue_name(ctx, field) + if err != nil { + return graphql.Null + } + ctx = graphql.WithFieldContext(ctx, fc) + defer func() { + if r := recover(); r != nil { + ec.Error(ctx, ec.Recover(ctx, r)) + ret = graphql.Null + } + }() + resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (any, error) { + ctx = rctx // use context from middleware stack in children + return obj.Name, nil + }) + if err != nil { + ec.Error(ctx, err) + return graphql.Null + } + if resTmp == nil { + if !graphql.HasFieldError(ctx, fc) { + ec.Errorf(ctx, "must not be null") + } + return graphql.Null + } + res := resTmp.(string) + fc.Result = res + return ec.marshalNString2string(ctx, field.Selections, res) +} + +func (ec *executionContext) fieldContext___InputValue_name(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { + fc = &graphql.FieldContext{ + Object: "__InputValue", + Field: field, + IsMethod: false, + IsResolver: false, + Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + return nil, errors.New("field of type String does not have child fields") + }, + } + return fc, nil +} + +func (ec *executionContext) ___InputValue_description(ctx context.Context, field graphql.CollectedField, obj *introspection.InputValue) (ret graphql.Marshaler) { + fc, err := ec.fieldContext___InputValue_description(ctx, field) + if err != nil { + return graphql.Null + } + ctx = graphql.WithFieldContext(ctx, fc) + defer func() { + if r := recover(); r != nil { + ec.Error(ctx, ec.Recover(ctx, r)) + ret = graphql.Null + } + }() + resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (any, error) { + ctx = rctx // use context from middleware stack in children + return obj.Description(), nil + }) + if err != nil { + ec.Error(ctx, err) + return graphql.Null + } + if resTmp == nil { + return graphql.Null + } + res := resTmp.(*string) + fc.Result = res + return ec.marshalOString2ᚖstring(ctx, field.Selections, res) +} + +func (ec *executionContext) fieldContext___InputValue_description(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { + fc = &graphql.FieldContext{ + Object: "__InputValue", + Field: field, + IsMethod: true, + IsResolver: false, + Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + return nil, errors.New("field of type String does not have child fields") + }, + } + return fc, nil +} + +func (ec *executionContext) ___InputValue_type(ctx context.Context, field graphql.CollectedField, obj *introspection.InputValue) (ret graphql.Marshaler) { + fc, err := ec.fieldContext___InputValue_type(ctx, field) + if err != nil { + return graphql.Null + } + ctx = graphql.WithFieldContext(ctx, fc) + defer func() { + if r := recover(); r != nil { + ec.Error(ctx, ec.Recover(ctx, r)) + ret = graphql.Null + } + }() + resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (any, error) { + ctx = rctx // use context from middleware stack in children + return obj.Type, nil + }) + if err != nil { + ec.Error(ctx, err) + return graphql.Null + } + if resTmp == nil { + if !graphql.HasFieldError(ctx, fc) { + ec.Errorf(ctx, "must not be null") + } + return graphql.Null + } + res := resTmp.(*introspection.Type) + fc.Result = res + return ec.marshalN__Type2ᚖgithubᚗcomᚋ99designsᚋgqlgenᚋgraphqlᚋintrospectionᚐType(ctx, field.Selections, res) +} + +func (ec *executionContext) fieldContext___InputValue_type(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { + fc = &graphql.FieldContext{ + Object: "__InputValue", + Field: field, + IsMethod: false, + IsResolver: false, + Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + switch field.Name { + case "kind": + return ec.fieldContext___Type_kind(ctx, field) + case "name": + return ec.fieldContext___Type_name(ctx, field) + case "description": + return ec.fieldContext___Type_description(ctx, field) + case "fields": + return ec.fieldContext___Type_fields(ctx, field) + case "interfaces": + return ec.fieldContext___Type_interfaces(ctx, field) + case "possibleTypes": + return ec.fieldContext___Type_possibleTypes(ctx, field) + case "enumValues": + return ec.fieldContext___Type_enumValues(ctx, field) + case "inputFields": + return ec.fieldContext___Type_inputFields(ctx, field) + case "ofType": + return ec.fieldContext___Type_ofType(ctx, field) + case "specifiedByURL": + return ec.fieldContext___Type_specifiedByURL(ctx, field) + } + return nil, fmt.Errorf("no field named %q was found under type __Type", field.Name) + }, + } + return fc, nil +} + +func (ec *executionContext) ___InputValue_defaultValue(ctx context.Context, field graphql.CollectedField, obj *introspection.InputValue) (ret graphql.Marshaler) { + fc, err := ec.fieldContext___InputValue_defaultValue(ctx, field) + if err != nil { + return graphql.Null + } + ctx = graphql.WithFieldContext(ctx, fc) + defer func() { + if r := recover(); r != nil { + ec.Error(ctx, ec.Recover(ctx, r)) + ret = graphql.Null + } + }() + resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (any, error) { + ctx = rctx // use context from middleware stack in children + return obj.DefaultValue, nil + }) + if err != nil { + ec.Error(ctx, err) + return graphql.Null + } + if resTmp == nil { + return graphql.Null + } + res := resTmp.(*string) + fc.Result = res + return ec.marshalOString2ᚖstring(ctx, field.Selections, res) +} + +func (ec *executionContext) fieldContext___InputValue_defaultValue(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { + fc = &graphql.FieldContext{ + Object: "__InputValue", + Field: field, + IsMethod: false, + IsResolver: false, + Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + return nil, errors.New("field of type String does not have child fields") + }, + } + return fc, nil +} + +func (ec *executionContext) ___Schema_description(ctx context.Context, field graphql.CollectedField, obj *introspection.Schema) (ret graphql.Marshaler) { + fc, err := ec.fieldContext___Schema_description(ctx, field) + if err != nil { + return graphql.Null + } + ctx = graphql.WithFieldContext(ctx, fc) + defer func() { + if r := recover(); r != nil { + ec.Error(ctx, ec.Recover(ctx, r)) + ret = graphql.Null + } + }() + resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (any, error) { + ctx = rctx // use context from middleware stack in children + return obj.Description(), nil + }) + if err != nil { + ec.Error(ctx, err) + return graphql.Null + } + if resTmp == nil { + return graphql.Null + } + res := resTmp.(*string) + fc.Result = res + return ec.marshalOString2ᚖstring(ctx, field.Selections, res) +} + +func (ec *executionContext) fieldContext___Schema_description(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { + fc = &graphql.FieldContext{ + Object: "__Schema", + Field: field, + IsMethod: true, + IsResolver: false, + Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + return nil, errors.New("field of type String does not have child fields") + }, + } + return fc, nil +} + +func (ec *executionContext) ___Schema_types(ctx context.Context, field graphql.CollectedField, obj *introspection.Schema) (ret graphql.Marshaler) { + fc, err := ec.fieldContext___Schema_types(ctx, field) + if err != nil { + return graphql.Null + } + ctx = graphql.WithFieldContext(ctx, fc) + defer func() { + if r := recover(); r != nil { + ec.Error(ctx, ec.Recover(ctx, r)) + ret = graphql.Null + } + }() + resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (any, error) { + ctx = rctx // use context from middleware stack in children + return obj.Types(), nil + }) + if err != nil { + ec.Error(ctx, err) + return graphql.Null + } + if resTmp == nil { + if !graphql.HasFieldError(ctx, fc) { + ec.Errorf(ctx, "must not be null") + } + return graphql.Null + } + res := resTmp.([]introspection.Type) + fc.Result = res + return ec.marshalN__Type2ᚕgithubᚗcomᚋ99designsᚋgqlgenᚋgraphqlᚋintrospectionᚐTypeᚄ(ctx, field.Selections, res) +} + +func (ec *executionContext) fieldContext___Schema_types(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { + fc = &graphql.FieldContext{ + Object: "__Schema", + Field: field, + IsMethod: true, + IsResolver: false, + Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + switch field.Name { + case "kind": + return ec.fieldContext___Type_kind(ctx, field) + case "name": + return ec.fieldContext___Type_name(ctx, field) + case "description": + return ec.fieldContext___Type_description(ctx, field) + case "fields": + return ec.fieldContext___Type_fields(ctx, field) + case "interfaces": + return ec.fieldContext___Type_interfaces(ctx, field) + case "possibleTypes": + return ec.fieldContext___Type_possibleTypes(ctx, field) + case "enumValues": + return ec.fieldContext___Type_enumValues(ctx, field) + case "inputFields": + return ec.fieldContext___Type_inputFields(ctx, field) + case "ofType": + return ec.fieldContext___Type_ofType(ctx, field) + case "specifiedByURL": + return ec.fieldContext___Type_specifiedByURL(ctx, field) + } + return nil, fmt.Errorf("no field named %q was found under type __Type", field.Name) + }, + } + return fc, nil +} + +func (ec *executionContext) ___Schema_queryType(ctx context.Context, field graphql.CollectedField, obj *introspection.Schema) (ret graphql.Marshaler) { + fc, err := ec.fieldContext___Schema_queryType(ctx, field) + if err != nil { + return graphql.Null + } + ctx = graphql.WithFieldContext(ctx, fc) + defer func() { + if r := recover(); r != nil { + ec.Error(ctx, ec.Recover(ctx, r)) + ret = graphql.Null + } + }() + resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (any, error) { + ctx = rctx // use context from middleware stack in children + return obj.QueryType(), nil + }) + if err != nil { + ec.Error(ctx, err) + return graphql.Null + } + if resTmp == nil { + if !graphql.HasFieldError(ctx, fc) { + ec.Errorf(ctx, "must not be null") + } + return graphql.Null + } + res := resTmp.(*introspection.Type) + fc.Result = res + return ec.marshalN__Type2ᚖgithubᚗcomᚋ99designsᚋgqlgenᚋgraphqlᚋintrospectionᚐType(ctx, field.Selections, res) +} + +func (ec *executionContext) fieldContext___Schema_queryType(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { + fc = &graphql.FieldContext{ + Object: "__Schema", + Field: field, + IsMethod: true, + IsResolver: false, + Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + switch field.Name { + case "kind": + return ec.fieldContext___Type_kind(ctx, field) + case "name": + return ec.fieldContext___Type_name(ctx, field) + case "description": + return ec.fieldContext___Type_description(ctx, field) + case "fields": + return ec.fieldContext___Type_fields(ctx, field) + case "interfaces": + return ec.fieldContext___Type_interfaces(ctx, field) + case "possibleTypes": + return ec.fieldContext___Type_possibleTypes(ctx, field) + case "enumValues": + return ec.fieldContext___Type_enumValues(ctx, field) + case "inputFields": + return ec.fieldContext___Type_inputFields(ctx, field) + case "ofType": + return ec.fieldContext___Type_ofType(ctx, field) + case "specifiedByURL": + return ec.fieldContext___Type_specifiedByURL(ctx, field) + } + return nil, fmt.Errorf("no field named %q was found under type __Type", field.Name) + }, + } + return fc, nil +} + +func (ec *executionContext) ___Schema_mutationType(ctx context.Context, field graphql.CollectedField, obj *introspection.Schema) (ret graphql.Marshaler) { + fc, err := ec.fieldContext___Schema_mutationType(ctx, field) + if err != nil { + return graphql.Null + } + ctx = graphql.WithFieldContext(ctx, fc) + defer func() { + if r := recover(); r != nil { + ec.Error(ctx, ec.Recover(ctx, r)) + ret = graphql.Null + } + }() + resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (any, error) { + ctx = rctx // use context from middleware stack in children + return obj.MutationType(), nil + }) + if err != nil { + ec.Error(ctx, err) + return graphql.Null + } + if resTmp == nil { + return graphql.Null + } + res := resTmp.(*introspection.Type) + fc.Result = res + return ec.marshalO__Type2ᚖgithubᚗcomᚋ99designsᚋgqlgenᚋgraphqlᚋintrospectionᚐType(ctx, field.Selections, res) +} + +func (ec *executionContext) fieldContext___Schema_mutationType(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { + fc = &graphql.FieldContext{ + Object: "__Schema", + Field: field, + IsMethod: true, + IsResolver: false, + Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + switch field.Name { + case "kind": + return ec.fieldContext___Type_kind(ctx, field) + case "name": + return ec.fieldContext___Type_name(ctx, field) + case "description": + return ec.fieldContext___Type_description(ctx, field) + case "fields": + return ec.fieldContext___Type_fields(ctx, field) + case "interfaces": + return ec.fieldContext___Type_interfaces(ctx, field) + case "possibleTypes": + return ec.fieldContext___Type_possibleTypes(ctx, field) + case "enumValues": + return ec.fieldContext___Type_enumValues(ctx, field) + case "inputFields": + return ec.fieldContext___Type_inputFields(ctx, field) + case "ofType": + return ec.fieldContext___Type_ofType(ctx, field) + case "specifiedByURL": + return ec.fieldContext___Type_specifiedByURL(ctx, field) + } + return nil, fmt.Errorf("no field named %q was found under type __Type", field.Name) + }, + } + return fc, nil +} + +func (ec *executionContext) ___Schema_subscriptionType(ctx context.Context, field graphql.CollectedField, obj *introspection.Schema) (ret graphql.Marshaler) { + fc, err := ec.fieldContext___Schema_subscriptionType(ctx, field) + if err != nil { + return graphql.Null + } + ctx = graphql.WithFieldContext(ctx, fc) + defer func() { + if r := recover(); r != nil { + ec.Error(ctx, ec.Recover(ctx, r)) + ret = graphql.Null + } + }() + resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (any, error) { + ctx = rctx // use context from middleware stack in children + return obj.SubscriptionType(), nil + }) + if err != nil { + ec.Error(ctx, err) + return graphql.Null + } + if resTmp == nil { + return graphql.Null + } + res := resTmp.(*introspection.Type) + fc.Result = res + return ec.marshalO__Type2ᚖgithubᚗcomᚋ99designsᚋgqlgenᚋgraphqlᚋintrospectionᚐType(ctx, field.Selections, res) +} + +func (ec *executionContext) fieldContext___Schema_subscriptionType(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { + fc = &graphql.FieldContext{ + Object: "__Schema", + Field: field, + IsMethod: true, + IsResolver: false, + Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + switch field.Name { + case "kind": + return ec.fieldContext___Type_kind(ctx, field) + case "name": + return ec.fieldContext___Type_name(ctx, field) + case "description": + return ec.fieldContext___Type_description(ctx, field) + case "fields": + return ec.fieldContext___Type_fields(ctx, field) + case "interfaces": + return ec.fieldContext___Type_interfaces(ctx, field) + case "possibleTypes": + return ec.fieldContext___Type_possibleTypes(ctx, field) + case "enumValues": + return ec.fieldContext___Type_enumValues(ctx, field) + case "inputFields": + return ec.fieldContext___Type_inputFields(ctx, field) + case "ofType": + return ec.fieldContext___Type_ofType(ctx, field) + case "specifiedByURL": + return ec.fieldContext___Type_specifiedByURL(ctx, field) + } + return nil, fmt.Errorf("no field named %q was found under type __Type", field.Name) + }, + } + return fc, nil +} + +func (ec *executionContext) ___Schema_directives(ctx context.Context, field graphql.CollectedField, obj *introspection.Schema) (ret graphql.Marshaler) { + fc, err := ec.fieldContext___Schema_directives(ctx, field) + if err != nil { + return graphql.Null + } + ctx = graphql.WithFieldContext(ctx, fc) + defer func() { + if r := recover(); r != nil { + ec.Error(ctx, ec.Recover(ctx, r)) + ret = graphql.Null + } + }() + resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (any, error) { + ctx = rctx // use context from middleware stack in children + return obj.Directives(), nil + }) + if err != nil { + ec.Error(ctx, err) + return graphql.Null + } + if resTmp == nil { + if !graphql.HasFieldError(ctx, fc) { + ec.Errorf(ctx, "must not be null") + } + return graphql.Null + } + res := resTmp.([]introspection.Directive) + fc.Result = res + return ec.marshalN__Directive2ᚕgithubᚗcomᚋ99designsᚋgqlgenᚋgraphqlᚋintrospectionᚐDirectiveᚄ(ctx, field.Selections, res) +} + +func (ec *executionContext) fieldContext___Schema_directives(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { + fc = &graphql.FieldContext{ + Object: "__Schema", + Field: field, + IsMethod: true, + IsResolver: false, + Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + switch field.Name { + case "name": + return ec.fieldContext___Directive_name(ctx, field) + case "description": + return ec.fieldContext___Directive_description(ctx, field) + case "locations": + return ec.fieldContext___Directive_locations(ctx, field) + case "args": + return ec.fieldContext___Directive_args(ctx, field) + case "isRepeatable": + return ec.fieldContext___Directive_isRepeatable(ctx, field) + } + return nil, fmt.Errorf("no field named %q was found under type __Directive", field.Name) + }, + } + return fc, nil +} + +func (ec *executionContext) ___Type_kind(ctx context.Context, field graphql.CollectedField, obj *introspection.Type) (ret graphql.Marshaler) { + fc, err := ec.fieldContext___Type_kind(ctx, field) + if err != nil { + return graphql.Null + } + ctx = graphql.WithFieldContext(ctx, fc) + defer func() { + if r := recover(); r != nil { + ec.Error(ctx, ec.Recover(ctx, r)) + ret = graphql.Null + } + }() + resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (any, error) { + ctx = rctx // use context from middleware stack in children + return obj.Kind(), nil + }) + if err != nil { + ec.Error(ctx, err) + return graphql.Null + } + if resTmp == nil { + if !graphql.HasFieldError(ctx, fc) { + ec.Errorf(ctx, "must not be null") + } + return graphql.Null + } + res := resTmp.(string) + fc.Result = res + return ec.marshalN__TypeKind2string(ctx, field.Selections, res) +} + +func (ec *executionContext) fieldContext___Type_kind(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { + fc = &graphql.FieldContext{ + Object: "__Type", + Field: field, + IsMethod: true, + IsResolver: false, + Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + return nil, errors.New("field of type __TypeKind does not have child fields") + }, + } + return fc, nil +} + +func (ec *executionContext) ___Type_name(ctx context.Context, field graphql.CollectedField, obj *introspection.Type) (ret graphql.Marshaler) { + fc, err := ec.fieldContext___Type_name(ctx, field) + if err != nil { + return graphql.Null + } + ctx = graphql.WithFieldContext(ctx, fc) + defer func() { + if r := recover(); r != nil { + ec.Error(ctx, ec.Recover(ctx, r)) + ret = graphql.Null + } + }() + resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (any, error) { + ctx = rctx // use context from middleware stack in children + return obj.Name(), nil + }) + if err != nil { + ec.Error(ctx, err) + return graphql.Null + } + if resTmp == nil { + return graphql.Null + } + res := resTmp.(*string) + fc.Result = res + return ec.marshalOString2ᚖstring(ctx, field.Selections, res) +} + +func (ec *executionContext) fieldContext___Type_name(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { + fc = &graphql.FieldContext{ + Object: "__Type", + Field: field, + IsMethod: true, + IsResolver: false, + Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + return nil, errors.New("field of type String does not have child fields") + }, + } + return fc, nil +} + +func (ec *executionContext) ___Type_description(ctx context.Context, field graphql.CollectedField, obj *introspection.Type) (ret graphql.Marshaler) { + fc, err := ec.fieldContext___Type_description(ctx, field) + if err != nil { + return graphql.Null + } + ctx = graphql.WithFieldContext(ctx, fc) + defer func() { + if r := recover(); r != nil { + ec.Error(ctx, ec.Recover(ctx, r)) + ret = graphql.Null + } + }() + resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (any, error) { + ctx = rctx // use context from middleware stack in children + return obj.Description(), nil + }) + if err != nil { + ec.Error(ctx, err) + return graphql.Null + } + if resTmp == nil { + return graphql.Null + } + res := resTmp.(*string) + fc.Result = res + return ec.marshalOString2ᚖstring(ctx, field.Selections, res) +} + +func (ec *executionContext) fieldContext___Type_description(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { + fc = &graphql.FieldContext{ + Object: "__Type", + Field: field, + IsMethod: true, + IsResolver: false, + Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + return nil, errors.New("field of type String does not have child fields") + }, + } + return fc, nil +} + +func (ec *executionContext) ___Type_fields(ctx context.Context, field graphql.CollectedField, obj *introspection.Type) (ret graphql.Marshaler) { + fc, err := ec.fieldContext___Type_fields(ctx, field) + if err != nil { + return graphql.Null + } + ctx = graphql.WithFieldContext(ctx, fc) + defer func() { + if r := recover(); r != nil { + ec.Error(ctx, ec.Recover(ctx, r)) + ret = graphql.Null + } + }() + resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (any, error) { + ctx = rctx // use context from middleware stack in children + return obj.Fields(fc.Args["includeDeprecated"].(bool)), nil + }) + if err != nil { + ec.Error(ctx, err) + return graphql.Null + } + if resTmp == nil { + return graphql.Null + } + res := resTmp.([]introspection.Field) + fc.Result = res + return ec.marshalO__Field2ᚕgithubᚗcomᚋ99designsᚋgqlgenᚋgraphqlᚋintrospectionᚐFieldᚄ(ctx, field.Selections, res) +} + +func (ec *executionContext) fieldContext___Type_fields(ctx context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { + fc = &graphql.FieldContext{ + Object: "__Type", + Field: field, + IsMethod: true, + IsResolver: false, + Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + switch field.Name { + case "name": + return ec.fieldContext___Field_name(ctx, field) + case "description": + return ec.fieldContext___Field_description(ctx, field) + case "args": + return ec.fieldContext___Field_args(ctx, field) + case "type": + return ec.fieldContext___Field_type(ctx, field) + case "isDeprecated": + return ec.fieldContext___Field_isDeprecated(ctx, field) + case "deprecationReason": + return ec.fieldContext___Field_deprecationReason(ctx, field) + } + return nil, fmt.Errorf("no field named %q was found under type __Field", field.Name) + }, + } + defer func() { + if r := recover(); r != nil { + err = ec.Recover(ctx, r) + ec.Error(ctx, err) + } + }() + ctx = graphql.WithFieldContext(ctx, fc) + if fc.Args, err = ec.field___Type_fields_args(ctx, field.ArgumentMap(ec.Variables)); err != nil { + ec.Error(ctx, err) + return fc, err + } + return fc, nil +} + +func (ec *executionContext) ___Type_interfaces(ctx context.Context, field graphql.CollectedField, obj *introspection.Type) (ret graphql.Marshaler) { + fc, err := ec.fieldContext___Type_interfaces(ctx, field) + if err != nil { + return graphql.Null + } + ctx = graphql.WithFieldContext(ctx, fc) + defer func() { + if r := recover(); r != nil { + ec.Error(ctx, ec.Recover(ctx, r)) + ret = graphql.Null + } + }() + resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (any, error) { + ctx = rctx // use context from middleware stack in children + return obj.Interfaces(), nil + }) + if err != nil { + ec.Error(ctx, err) + return graphql.Null + } + if resTmp == nil { + return graphql.Null + } + res := resTmp.([]introspection.Type) + fc.Result = res + return ec.marshalO__Type2ᚕgithubᚗcomᚋ99designsᚋgqlgenᚋgraphqlᚋintrospectionᚐTypeᚄ(ctx, field.Selections, res) +} + +func (ec *executionContext) fieldContext___Type_interfaces(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { + fc = &graphql.FieldContext{ + Object: "__Type", + Field: field, + IsMethod: true, + IsResolver: false, + Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + switch field.Name { + case "kind": + return ec.fieldContext___Type_kind(ctx, field) + case "name": + return ec.fieldContext___Type_name(ctx, field) + case "description": + return ec.fieldContext___Type_description(ctx, field) + case "fields": + return ec.fieldContext___Type_fields(ctx, field) + case "interfaces": + return ec.fieldContext___Type_interfaces(ctx, field) + case "possibleTypes": + return ec.fieldContext___Type_possibleTypes(ctx, field) + case "enumValues": + return ec.fieldContext___Type_enumValues(ctx, field) + case "inputFields": + return ec.fieldContext___Type_inputFields(ctx, field) + case "ofType": + return ec.fieldContext___Type_ofType(ctx, field) + case "specifiedByURL": + return ec.fieldContext___Type_specifiedByURL(ctx, field) + } + return nil, fmt.Errorf("no field named %q was found under type __Type", field.Name) + }, + } + return fc, nil +} + +func (ec *executionContext) ___Type_possibleTypes(ctx context.Context, field graphql.CollectedField, obj *introspection.Type) (ret graphql.Marshaler) { + fc, err := ec.fieldContext___Type_possibleTypes(ctx, field) + if err != nil { + return graphql.Null + } + ctx = graphql.WithFieldContext(ctx, fc) + defer func() { + if r := recover(); r != nil { + ec.Error(ctx, ec.Recover(ctx, r)) + ret = graphql.Null + } + }() + resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (any, error) { + ctx = rctx // use context from middleware stack in children + return obj.PossibleTypes(), nil + }) + if err != nil { + ec.Error(ctx, err) + return graphql.Null + } + if resTmp == nil { + return graphql.Null + } + res := resTmp.([]introspection.Type) + fc.Result = res + return ec.marshalO__Type2ᚕgithubᚗcomᚋ99designsᚋgqlgenᚋgraphqlᚋintrospectionᚐTypeᚄ(ctx, field.Selections, res) +} + +func (ec *executionContext) fieldContext___Type_possibleTypes(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { + fc = &graphql.FieldContext{ + Object: "__Type", + Field: field, + IsMethod: true, + IsResolver: false, + Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + switch field.Name { + case "kind": + return ec.fieldContext___Type_kind(ctx, field) + case "name": + return ec.fieldContext___Type_name(ctx, field) + case "description": + return ec.fieldContext___Type_description(ctx, field) + case "fields": + return ec.fieldContext___Type_fields(ctx, field) + case "interfaces": + return ec.fieldContext___Type_interfaces(ctx, field) + case "possibleTypes": + return ec.fieldContext___Type_possibleTypes(ctx, field) + case "enumValues": + return ec.fieldContext___Type_enumValues(ctx, field) + case "inputFields": + return ec.fieldContext___Type_inputFields(ctx, field) + case "ofType": + return ec.fieldContext___Type_ofType(ctx, field) + case "specifiedByURL": + return ec.fieldContext___Type_specifiedByURL(ctx, field) + } + return nil, fmt.Errorf("no field named %q was found under type __Type", field.Name) + }, + } + return fc, nil +} + +func (ec *executionContext) ___Type_enumValues(ctx context.Context, field graphql.CollectedField, obj *introspection.Type) (ret graphql.Marshaler) { + fc, err := ec.fieldContext___Type_enumValues(ctx, field) + if err != nil { + return graphql.Null + } + ctx = graphql.WithFieldContext(ctx, fc) + defer func() { + if r := recover(); r != nil { + ec.Error(ctx, ec.Recover(ctx, r)) + ret = graphql.Null + } + }() + resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (any, error) { + ctx = rctx // use context from middleware stack in children + return obj.EnumValues(fc.Args["includeDeprecated"].(bool)), nil + }) + if err != nil { + ec.Error(ctx, err) + return graphql.Null + } + if resTmp == nil { + return graphql.Null + } + res := resTmp.([]introspection.EnumValue) + fc.Result = res + return ec.marshalO__EnumValue2ᚕgithubᚗcomᚋ99designsᚋgqlgenᚋgraphqlᚋintrospectionᚐEnumValueᚄ(ctx, field.Selections, res) +} + +func (ec *executionContext) fieldContext___Type_enumValues(ctx context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { + fc = &graphql.FieldContext{ + Object: "__Type", + Field: field, + IsMethod: true, + IsResolver: false, + Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + switch field.Name { + case "name": + return ec.fieldContext___EnumValue_name(ctx, field) + case "description": + return ec.fieldContext___EnumValue_description(ctx, field) + case "isDeprecated": + return ec.fieldContext___EnumValue_isDeprecated(ctx, field) + case "deprecationReason": + return ec.fieldContext___EnumValue_deprecationReason(ctx, field) + } + return nil, fmt.Errorf("no field named %q was found under type __EnumValue", field.Name) + }, + } + defer func() { + if r := recover(); r != nil { + err = ec.Recover(ctx, r) + ec.Error(ctx, err) + } + }() + ctx = graphql.WithFieldContext(ctx, fc) + if fc.Args, err = ec.field___Type_enumValues_args(ctx, field.ArgumentMap(ec.Variables)); err != nil { + ec.Error(ctx, err) + return fc, err + } + return fc, nil +} + +func (ec *executionContext) ___Type_inputFields(ctx context.Context, field graphql.CollectedField, obj *introspection.Type) (ret graphql.Marshaler) { + fc, err := ec.fieldContext___Type_inputFields(ctx, field) + if err != nil { + return graphql.Null + } + ctx = graphql.WithFieldContext(ctx, fc) + defer func() { + if r := recover(); r != nil { + ec.Error(ctx, ec.Recover(ctx, r)) + ret = graphql.Null + } + }() + resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (any, error) { + ctx = rctx // use context from middleware stack in children + return obj.InputFields(), nil + }) + if err != nil { + ec.Error(ctx, err) + return graphql.Null + } + if resTmp == nil { + return graphql.Null + } + res := resTmp.([]introspection.InputValue) + fc.Result = res + return ec.marshalO__InputValue2ᚕgithubᚗcomᚋ99designsᚋgqlgenᚋgraphqlᚋintrospectionᚐInputValueᚄ(ctx, field.Selections, res) +} + +func (ec *executionContext) fieldContext___Type_inputFields(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { + fc = &graphql.FieldContext{ + Object: "__Type", + Field: field, + IsMethod: true, + IsResolver: false, + Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + switch field.Name { + case "name": + return ec.fieldContext___InputValue_name(ctx, field) + case "description": + return ec.fieldContext___InputValue_description(ctx, field) + case "type": + return ec.fieldContext___InputValue_type(ctx, field) + case "defaultValue": + return ec.fieldContext___InputValue_defaultValue(ctx, field) + } + return nil, fmt.Errorf("no field named %q was found under type __InputValue", field.Name) + }, + } + return fc, nil +} + +func (ec *executionContext) ___Type_ofType(ctx context.Context, field graphql.CollectedField, obj *introspection.Type) (ret graphql.Marshaler) { + fc, err := ec.fieldContext___Type_ofType(ctx, field) + if err != nil { + return graphql.Null + } + ctx = graphql.WithFieldContext(ctx, fc) + defer func() { + if r := recover(); r != nil { + ec.Error(ctx, ec.Recover(ctx, r)) + ret = graphql.Null + } + }() + resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (any, error) { + ctx = rctx // use context from middleware stack in children + return obj.OfType(), nil + }) + if err != nil { + ec.Error(ctx, err) + return graphql.Null + } + if resTmp == nil { + return graphql.Null + } + res := resTmp.(*introspection.Type) + fc.Result = res + return ec.marshalO__Type2ᚖgithubᚗcomᚋ99designsᚋgqlgenᚋgraphqlᚋintrospectionᚐType(ctx, field.Selections, res) +} + +func (ec *executionContext) fieldContext___Type_ofType(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { + fc = &graphql.FieldContext{ + Object: "__Type", + Field: field, + IsMethod: true, + IsResolver: false, + Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + switch field.Name { + case "kind": + return ec.fieldContext___Type_kind(ctx, field) + case "name": + return ec.fieldContext___Type_name(ctx, field) + case "description": + return ec.fieldContext___Type_description(ctx, field) + case "fields": + return ec.fieldContext___Type_fields(ctx, field) + case "interfaces": + return ec.fieldContext___Type_interfaces(ctx, field) + case "possibleTypes": + return ec.fieldContext___Type_possibleTypes(ctx, field) + case "enumValues": + return ec.fieldContext___Type_enumValues(ctx, field) + case "inputFields": + return ec.fieldContext___Type_inputFields(ctx, field) + case "ofType": + return ec.fieldContext___Type_ofType(ctx, field) + case "specifiedByURL": + return ec.fieldContext___Type_specifiedByURL(ctx, field) + } + return nil, fmt.Errorf("no field named %q was found under type __Type", field.Name) + }, + } + return fc, nil +} + +func (ec *executionContext) ___Type_specifiedByURL(ctx context.Context, field graphql.CollectedField, obj *introspection.Type) (ret graphql.Marshaler) { + fc, err := ec.fieldContext___Type_specifiedByURL(ctx, field) + if err != nil { + return graphql.Null + } + ctx = graphql.WithFieldContext(ctx, fc) + defer func() { + if r := recover(); r != nil { + ec.Error(ctx, ec.Recover(ctx, r)) + ret = graphql.Null + } + }() + resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (any, error) { + ctx = rctx // use context from middleware stack in children + return obj.SpecifiedByURL(), nil + }) + if err != nil { + ec.Error(ctx, err) + return graphql.Null + } + if resTmp == nil { + return graphql.Null + } + res := resTmp.(*string) + fc.Result = res + return ec.marshalOString2ᚖstring(ctx, field.Selections, res) +} + +func (ec *executionContext) fieldContext___Type_specifiedByURL(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { + fc = &graphql.FieldContext{ + Object: "__Type", + Field: field, + IsMethod: true, + IsResolver: false, + Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + return nil, errors.New("field of type String does not have child fields") + }, + } + return fc, nil +} + +// endregion **************************** field.gotpl ***************************** + +// region **************************** input.gotpl ***************************** + +func (ec *executionContext) unmarshalInputListedSecurityInput(ctx context.Context, obj any) (ListedSecurityInput, error) { + var it ListedSecurityInput + asMap := map[string]any{} + for k, v := range obj.(map[string]any) { + asMap[k] = v + } + + fieldsInOrder := [...]string{"ticker", "currency"} + for _, k := range fieldsInOrder { + v, ok := asMap[k] + if !ok { + continue + } + switch k { + case "ticker": + ctx := graphql.WithPathContext(ctx, graphql.NewPathWithField("ticker")) + data, err := ec.unmarshalNString2string(ctx, v) + if err != nil { + return it, err + } + it.Ticker = data + case "currency": + ctx := graphql.WithPathContext(ctx, graphql.NewPathWithField("currency")) + data, err := ec.unmarshalNString2string(ctx, v) + if err != nil { + return it, err + } + it.Currency = data + } + } + + return it, nil +} + +func (ec *executionContext) unmarshalInputSecurityInput(ctx context.Context, obj any) (SecurityInput, error) { + var it SecurityInput + asMap := map[string]any{} + for k, v := range obj.(map[string]any) { + asMap[k] = v + } + + fieldsInOrder := [...]string{"id", "displayName", "listedAs"} + for _, k := range fieldsInOrder { + v, ok := asMap[k] + if !ok { + continue + } + switch k { + case "id": + ctx := graphql.WithPathContext(ctx, graphql.NewPathWithField("id")) + data, err := ec.unmarshalNString2string(ctx, v) + if err != nil { + return it, err + } + it.ID = data + case "displayName": + ctx := graphql.WithPathContext(ctx, graphql.NewPathWithField("displayName")) + data, err := ec.unmarshalNString2string(ctx, v) + if err != nil { + return it, err + } + it.DisplayName = data + case "listedAs": + ctx := graphql.WithPathContext(ctx, graphql.NewPathWithField("listedAs")) + data, err := ec.unmarshalOListedSecurityInput2ᚕᚖgithubᚗcomᚋoxistoᚋmoneyᚑgopherᚋgraphᚐListedSecurityInputᚄ(ctx, v) + if err != nil { + return it, err + } + it.ListedAs = data + } + } + + return it, nil +} + +// endregion **************************** input.gotpl ***************************** + +// region ************************** interface.gotpl *************************** + +// endregion ************************** interface.gotpl *************************** + +// region **************************** object.gotpl **************************** + +var listedSecurityImplementors = []string{"ListedSecurity"} + +func (ec *executionContext) _ListedSecurity(ctx context.Context, sel ast.SelectionSet, obj *persistence.ListedSecurity) graphql.Marshaler { + fields := graphql.CollectFields(ec.OperationContext, sel, listedSecurityImplementors) + + out := graphql.NewFieldSet(fields) + deferred := make(map[string]*graphql.FieldSet) + for i, field := range fields { + switch field.Name { + case "__typename": + out.Values[i] = graphql.MarshalString("ListedSecurity") + case "ticker": + out.Values[i] = ec._ListedSecurity_ticker(ctx, field, obj) + if out.Values[i] == graphql.Null { + atomic.AddUint32(&out.Invalids, 1) + } + case "currency": + out.Values[i] = ec._ListedSecurity_currency(ctx, field, obj) + if out.Values[i] == graphql.Null { + atomic.AddUint32(&out.Invalids, 1) + } + case "security": + field := field + + innerFunc := func(ctx context.Context, fs *graphql.FieldSet) (res graphql.Marshaler) { + defer func() { + if r := recover(); r != nil { + ec.Error(ctx, ec.Recover(ctx, r)) + } + }() + res = ec._ListedSecurity_security(ctx, field, obj) + if res == graphql.Null { + atomic.AddUint32(&fs.Invalids, 1) + } + return res + } + + if field.Deferrable != nil { + dfs, ok := deferred[field.Deferrable.Label] + di := 0 + if ok { + dfs.AddField(field) + di = len(dfs.Values) - 1 + } else { + dfs = graphql.NewFieldSet([]graphql.CollectedField{field}) + deferred[field.Deferrable.Label] = dfs + } + dfs.Concurrently(di, func(ctx context.Context) graphql.Marshaler { + return innerFunc(ctx, dfs) + }) + + // don't run the out.Concurrently() call below + out.Values[i] = graphql.Null + continue + } + + out.Concurrently(i, func(ctx context.Context) graphql.Marshaler { return innerFunc(ctx, out) }) + default: + panic("unknown field " + strconv.Quote(field.Name)) + } + } + out.Dispatch(ctx) + if out.Invalids > 0 { + return graphql.Null + } + + atomic.AddInt32(&ec.deferred, int32(len(deferred))) + + for label, dfs := range deferred { + ec.processDeferredGroup(graphql.DeferredGroup{ + Label: label, + Path: graphql.GetPath(ctx), + FieldSet: dfs, + Context: ctx, + }) + } + + return out +} + +var mutationImplementors = []string{"Mutation"} + +func (ec *executionContext) _Mutation(ctx context.Context, sel ast.SelectionSet) graphql.Marshaler { + fields := graphql.CollectFields(ec.OperationContext, sel, mutationImplementors) + ctx = graphql.WithFieldContext(ctx, &graphql.FieldContext{ + Object: "Mutation", + }) + + out := graphql.NewFieldSet(fields) + deferred := make(map[string]*graphql.FieldSet) + for i, field := range fields { + innerCtx := graphql.WithRootFieldContext(ctx, &graphql.RootFieldContext{ + Object: field.Name, + Field: field, + }) + + switch field.Name { + case "__typename": + out.Values[i] = graphql.MarshalString("Mutation") + case "createSecurity": + out.Values[i] = ec.OperationContext.RootResolverMiddleware(innerCtx, func(ctx context.Context) (res graphql.Marshaler) { + return ec._Mutation_createSecurity(ctx, field) + }) + if out.Values[i] == graphql.Null { + out.Invalids++ + } + case "updateSecurity": + out.Values[i] = ec.OperationContext.RootResolverMiddleware(innerCtx, func(ctx context.Context) (res graphql.Marshaler) { + return ec._Mutation_updateSecurity(ctx, field) + }) + if out.Values[i] == graphql.Null { + out.Invalids++ + } + default: + panic("unknown field " + strconv.Quote(field.Name)) + } + } + out.Dispatch(ctx) + if out.Invalids > 0 { + return graphql.Null + } + + atomic.AddInt32(&ec.deferred, int32(len(deferred))) + + for label, dfs := range deferred { + ec.processDeferredGroup(graphql.DeferredGroup{ + Label: label, + Path: graphql.GetPath(ctx), + FieldSet: dfs, + Context: ctx, + }) + } + + return out +} + +var queryImplementors = []string{"Query"} + +func (ec *executionContext) _Query(ctx context.Context, sel ast.SelectionSet) graphql.Marshaler { + fields := graphql.CollectFields(ec.OperationContext, sel, queryImplementors) + ctx = graphql.WithFieldContext(ctx, &graphql.FieldContext{ + Object: "Query", + }) + + out := graphql.NewFieldSet(fields) + deferred := make(map[string]*graphql.FieldSet) + for i, field := range fields { + innerCtx := graphql.WithRootFieldContext(ctx, &graphql.RootFieldContext{ + Object: field.Name, + Field: field, + }) + + switch field.Name { + case "__typename": + out.Values[i] = graphql.MarshalString("Query") + case "security": + field := field + + innerFunc := func(ctx context.Context, _ *graphql.FieldSet) (res graphql.Marshaler) { + defer func() { + if r := recover(); r != nil { + ec.Error(ctx, ec.Recover(ctx, r)) + } + }() + res = ec._Query_security(ctx, field) + return res + } + + rrm := func(ctx context.Context) graphql.Marshaler { + return ec.OperationContext.RootResolverMiddleware(ctx, + func(ctx context.Context) graphql.Marshaler { return innerFunc(ctx, out) }) + } + + out.Concurrently(i, func(ctx context.Context) graphql.Marshaler { return rrm(innerCtx) }) + case "securities": + field := field + + innerFunc := func(ctx context.Context, fs *graphql.FieldSet) (res graphql.Marshaler) { + defer func() { + if r := recover(); r != nil { + ec.Error(ctx, ec.Recover(ctx, r)) + } + }() + res = ec._Query_securities(ctx, field) + if res == graphql.Null { + atomic.AddUint32(&fs.Invalids, 1) + } + return res + } + + rrm := func(ctx context.Context) graphql.Marshaler { + return ec.OperationContext.RootResolverMiddleware(ctx, + func(ctx context.Context) graphql.Marshaler { return innerFunc(ctx, out) }) + } + + out.Concurrently(i, func(ctx context.Context) graphql.Marshaler { return rrm(innerCtx) }) + case "__type": + out.Values[i] = ec.OperationContext.RootResolverMiddleware(innerCtx, func(ctx context.Context) (res graphql.Marshaler) { + return ec._Query___type(ctx, field) + }) + case "__schema": + out.Values[i] = ec.OperationContext.RootResolverMiddleware(innerCtx, func(ctx context.Context) (res graphql.Marshaler) { + return ec._Query___schema(ctx, field) + }) + default: + panic("unknown field " + strconv.Quote(field.Name)) + } + } + out.Dispatch(ctx) + if out.Invalids > 0 { + return graphql.Null + } + + atomic.AddInt32(&ec.deferred, int32(len(deferred))) + + for label, dfs := range deferred { + ec.processDeferredGroup(graphql.DeferredGroup{ + Label: label, + Path: graphql.GetPath(ctx), + FieldSet: dfs, + Context: ctx, + }) + } + + return out +} + +var securityImplementors = []string{"Security"} + +func (ec *executionContext) _Security(ctx context.Context, sel ast.SelectionSet, obj *persistence.Security) graphql.Marshaler { + fields := graphql.CollectFields(ec.OperationContext, sel, securityImplementors) + + out := graphql.NewFieldSet(fields) + deferred := make(map[string]*graphql.FieldSet) + for i, field := range fields { + switch field.Name { + case "__typename": + out.Values[i] = graphql.MarshalString("Security") + case "id": + out.Values[i] = ec._Security_id(ctx, field, obj) + if out.Values[i] == graphql.Null { + atomic.AddUint32(&out.Invalids, 1) + } + case "displayName": + out.Values[i] = ec._Security_displayName(ctx, field, obj) + if out.Values[i] == graphql.Null { + atomic.AddUint32(&out.Invalids, 1) + } + case "quoteProvider": + field := field + + innerFunc := func(ctx context.Context, _ *graphql.FieldSet) (res graphql.Marshaler) { + defer func() { + if r := recover(); r != nil { + ec.Error(ctx, ec.Recover(ctx, r)) + } + }() + res = ec._Security_quoteProvider(ctx, field, obj) + return res + } + + if field.Deferrable != nil { + dfs, ok := deferred[field.Deferrable.Label] + di := 0 + if ok { + dfs.AddField(field) + di = len(dfs.Values) - 1 + } else { + dfs = graphql.NewFieldSet([]graphql.CollectedField{field}) + deferred[field.Deferrable.Label] = dfs + } + dfs.Concurrently(di, func(ctx context.Context) graphql.Marshaler { + return innerFunc(ctx, dfs) + }) + + // don't run the out.Concurrently() call below + out.Values[i] = graphql.Null + continue + } + + out.Concurrently(i, func(ctx context.Context) graphql.Marshaler { return innerFunc(ctx, out) }) + case "listedAs": + field := field + + innerFunc := func(ctx context.Context, _ *graphql.FieldSet) (res graphql.Marshaler) { + defer func() { + if r := recover(); r != nil { + ec.Error(ctx, ec.Recover(ctx, r)) + } + }() + res = ec._Security_listedAs(ctx, field, obj) + return res + } + + if field.Deferrable != nil { + dfs, ok := deferred[field.Deferrable.Label] + di := 0 + if ok { + dfs.AddField(field) + di = len(dfs.Values) - 1 + } else { + dfs = graphql.NewFieldSet([]graphql.CollectedField{field}) + deferred[field.Deferrable.Label] = dfs + } + dfs.Concurrently(di, func(ctx context.Context) graphql.Marshaler { + return innerFunc(ctx, dfs) + }) + + // don't run the out.Concurrently() call below + out.Values[i] = graphql.Null + continue + } + + out.Concurrently(i, func(ctx context.Context) graphql.Marshaler { return innerFunc(ctx, out) }) + default: + panic("unknown field " + strconv.Quote(field.Name)) + } + } + out.Dispatch(ctx) + if out.Invalids > 0 { + return graphql.Null + } + + atomic.AddInt32(&ec.deferred, int32(len(deferred))) + + for label, dfs := range deferred { + ec.processDeferredGroup(graphql.DeferredGroup{ + Label: label, + Path: graphql.GetPath(ctx), + FieldSet: dfs, + Context: ctx, + }) + } + + return out +} + +var __DirectiveImplementors = []string{"__Directive"} + +func (ec *executionContext) ___Directive(ctx context.Context, sel ast.SelectionSet, obj *introspection.Directive) graphql.Marshaler { + fields := graphql.CollectFields(ec.OperationContext, sel, __DirectiveImplementors) + + out := graphql.NewFieldSet(fields) + deferred := make(map[string]*graphql.FieldSet) + for i, field := range fields { + switch field.Name { + case "__typename": + out.Values[i] = graphql.MarshalString("__Directive") + case "name": + out.Values[i] = ec.___Directive_name(ctx, field, obj) + if out.Values[i] == graphql.Null { + out.Invalids++ + } + case "description": + out.Values[i] = ec.___Directive_description(ctx, field, obj) + case "locations": + out.Values[i] = ec.___Directive_locations(ctx, field, obj) + if out.Values[i] == graphql.Null { + out.Invalids++ + } + case "args": + out.Values[i] = ec.___Directive_args(ctx, field, obj) + if out.Values[i] == graphql.Null { + out.Invalids++ + } + case "isRepeatable": + out.Values[i] = ec.___Directive_isRepeatable(ctx, field, obj) + if out.Values[i] == graphql.Null { + out.Invalids++ + } + default: + panic("unknown field " + strconv.Quote(field.Name)) + } + } + out.Dispatch(ctx) + if out.Invalids > 0 { + return graphql.Null + } + + atomic.AddInt32(&ec.deferred, int32(len(deferred))) + + for label, dfs := range deferred { + ec.processDeferredGroup(graphql.DeferredGroup{ + Label: label, + Path: graphql.GetPath(ctx), + FieldSet: dfs, + Context: ctx, + }) + } + + return out +} + +var __EnumValueImplementors = []string{"__EnumValue"} + +func (ec *executionContext) ___EnumValue(ctx context.Context, sel ast.SelectionSet, obj *introspection.EnumValue) graphql.Marshaler { + fields := graphql.CollectFields(ec.OperationContext, sel, __EnumValueImplementors) + + out := graphql.NewFieldSet(fields) + deferred := make(map[string]*graphql.FieldSet) + for i, field := range fields { + switch field.Name { + case "__typename": + out.Values[i] = graphql.MarshalString("__EnumValue") + case "name": + out.Values[i] = ec.___EnumValue_name(ctx, field, obj) + if out.Values[i] == graphql.Null { + out.Invalids++ + } + case "description": + out.Values[i] = ec.___EnumValue_description(ctx, field, obj) + case "isDeprecated": + out.Values[i] = ec.___EnumValue_isDeprecated(ctx, field, obj) + if out.Values[i] == graphql.Null { + out.Invalids++ + } + case "deprecationReason": + out.Values[i] = ec.___EnumValue_deprecationReason(ctx, field, obj) + default: + panic("unknown field " + strconv.Quote(field.Name)) + } + } + out.Dispatch(ctx) + if out.Invalids > 0 { + return graphql.Null + } + + atomic.AddInt32(&ec.deferred, int32(len(deferred))) + + for label, dfs := range deferred { + ec.processDeferredGroup(graphql.DeferredGroup{ + Label: label, + Path: graphql.GetPath(ctx), + FieldSet: dfs, + Context: ctx, + }) + } + + return out +} + +var __FieldImplementors = []string{"__Field"} + +func (ec *executionContext) ___Field(ctx context.Context, sel ast.SelectionSet, obj *introspection.Field) graphql.Marshaler { + fields := graphql.CollectFields(ec.OperationContext, sel, __FieldImplementors) + + out := graphql.NewFieldSet(fields) + deferred := make(map[string]*graphql.FieldSet) + for i, field := range fields { + switch field.Name { + case "__typename": + out.Values[i] = graphql.MarshalString("__Field") + case "name": + out.Values[i] = ec.___Field_name(ctx, field, obj) + if out.Values[i] == graphql.Null { + out.Invalids++ + } + case "description": + out.Values[i] = ec.___Field_description(ctx, field, obj) + case "args": + out.Values[i] = ec.___Field_args(ctx, field, obj) + if out.Values[i] == graphql.Null { + out.Invalids++ + } + case "type": + out.Values[i] = ec.___Field_type(ctx, field, obj) + if out.Values[i] == graphql.Null { + out.Invalids++ + } + case "isDeprecated": + out.Values[i] = ec.___Field_isDeprecated(ctx, field, obj) + if out.Values[i] == graphql.Null { + out.Invalids++ + } + case "deprecationReason": + out.Values[i] = ec.___Field_deprecationReason(ctx, field, obj) + default: + panic("unknown field " + strconv.Quote(field.Name)) + } + } + out.Dispatch(ctx) + if out.Invalids > 0 { + return graphql.Null + } + + atomic.AddInt32(&ec.deferred, int32(len(deferred))) + + for label, dfs := range deferred { + ec.processDeferredGroup(graphql.DeferredGroup{ + Label: label, + Path: graphql.GetPath(ctx), + FieldSet: dfs, + Context: ctx, + }) + } + + return out +} + +var __InputValueImplementors = []string{"__InputValue"} + +func (ec *executionContext) ___InputValue(ctx context.Context, sel ast.SelectionSet, obj *introspection.InputValue) graphql.Marshaler { + fields := graphql.CollectFields(ec.OperationContext, sel, __InputValueImplementors) + + out := graphql.NewFieldSet(fields) + deferred := make(map[string]*graphql.FieldSet) + for i, field := range fields { + switch field.Name { + case "__typename": + out.Values[i] = graphql.MarshalString("__InputValue") + case "name": + out.Values[i] = ec.___InputValue_name(ctx, field, obj) + if out.Values[i] == graphql.Null { + out.Invalids++ + } + case "description": + out.Values[i] = ec.___InputValue_description(ctx, field, obj) + case "type": + out.Values[i] = ec.___InputValue_type(ctx, field, obj) + if out.Values[i] == graphql.Null { + out.Invalids++ + } + case "defaultValue": + out.Values[i] = ec.___InputValue_defaultValue(ctx, field, obj) + default: + panic("unknown field " + strconv.Quote(field.Name)) + } + } + out.Dispatch(ctx) + if out.Invalids > 0 { + return graphql.Null + } + + atomic.AddInt32(&ec.deferred, int32(len(deferred))) + + for label, dfs := range deferred { + ec.processDeferredGroup(graphql.DeferredGroup{ + Label: label, + Path: graphql.GetPath(ctx), + FieldSet: dfs, + Context: ctx, + }) + } + + return out +} + +var __SchemaImplementors = []string{"__Schema"} + +func (ec *executionContext) ___Schema(ctx context.Context, sel ast.SelectionSet, obj *introspection.Schema) graphql.Marshaler { + fields := graphql.CollectFields(ec.OperationContext, sel, __SchemaImplementors) + + out := graphql.NewFieldSet(fields) + deferred := make(map[string]*graphql.FieldSet) + for i, field := range fields { + switch field.Name { + case "__typename": + out.Values[i] = graphql.MarshalString("__Schema") + case "description": + out.Values[i] = ec.___Schema_description(ctx, field, obj) + case "types": + out.Values[i] = ec.___Schema_types(ctx, field, obj) + if out.Values[i] == graphql.Null { + out.Invalids++ + } + case "queryType": + out.Values[i] = ec.___Schema_queryType(ctx, field, obj) + if out.Values[i] == graphql.Null { + out.Invalids++ + } + case "mutationType": + out.Values[i] = ec.___Schema_mutationType(ctx, field, obj) + case "subscriptionType": + out.Values[i] = ec.___Schema_subscriptionType(ctx, field, obj) + case "directives": + out.Values[i] = ec.___Schema_directives(ctx, field, obj) + if out.Values[i] == graphql.Null { + out.Invalids++ + } + default: + panic("unknown field " + strconv.Quote(field.Name)) + } + } + out.Dispatch(ctx) + if out.Invalids > 0 { + return graphql.Null + } + + atomic.AddInt32(&ec.deferred, int32(len(deferred))) + + for label, dfs := range deferred { + ec.processDeferredGroup(graphql.DeferredGroup{ + Label: label, + Path: graphql.GetPath(ctx), + FieldSet: dfs, + Context: ctx, + }) + } + + return out +} + +var __TypeImplementors = []string{"__Type"} + +func (ec *executionContext) ___Type(ctx context.Context, sel ast.SelectionSet, obj *introspection.Type) graphql.Marshaler { + fields := graphql.CollectFields(ec.OperationContext, sel, __TypeImplementors) + + out := graphql.NewFieldSet(fields) + deferred := make(map[string]*graphql.FieldSet) + for i, field := range fields { + switch field.Name { + case "__typename": + out.Values[i] = graphql.MarshalString("__Type") + case "kind": + out.Values[i] = ec.___Type_kind(ctx, field, obj) + if out.Values[i] == graphql.Null { + out.Invalids++ + } + case "name": + out.Values[i] = ec.___Type_name(ctx, field, obj) + case "description": + out.Values[i] = ec.___Type_description(ctx, field, obj) + case "fields": + out.Values[i] = ec.___Type_fields(ctx, field, obj) + case "interfaces": + out.Values[i] = ec.___Type_interfaces(ctx, field, obj) + case "possibleTypes": + out.Values[i] = ec.___Type_possibleTypes(ctx, field, obj) + case "enumValues": + out.Values[i] = ec.___Type_enumValues(ctx, field, obj) + case "inputFields": + out.Values[i] = ec.___Type_inputFields(ctx, field, obj) + case "ofType": + out.Values[i] = ec.___Type_ofType(ctx, field, obj) + case "specifiedByURL": + out.Values[i] = ec.___Type_specifiedByURL(ctx, field, obj) + default: + panic("unknown field " + strconv.Quote(field.Name)) + } + } + out.Dispatch(ctx) + if out.Invalids > 0 { + return graphql.Null + } + + atomic.AddInt32(&ec.deferred, int32(len(deferred))) + + for label, dfs := range deferred { + ec.processDeferredGroup(graphql.DeferredGroup{ + Label: label, + Path: graphql.GetPath(ctx), + FieldSet: dfs, + Context: ctx, + }) + } + + return out +} + +// endregion **************************** object.gotpl **************************** + +// region ***************************** type.gotpl ***************************** + +func (ec *executionContext) unmarshalNBoolean2bool(ctx context.Context, v any) (bool, error) { + res, err := graphql.UnmarshalBoolean(v) + return res, graphql.ErrorOnPath(ctx, err) +} + +func (ec *executionContext) marshalNBoolean2bool(ctx context.Context, sel ast.SelectionSet, v bool) graphql.Marshaler { + res := graphql.MarshalBoolean(v) + if res == graphql.Null { + if !graphql.HasFieldError(ctx, graphql.GetFieldContext(ctx)) { + ec.Errorf(ctx, "the requested element is null which the schema does not allow") + } + } + return res +} + +func (ec *executionContext) unmarshalNID2string(ctx context.Context, v any) (string, error) { + res, err := graphql.UnmarshalID(v) + return res, graphql.ErrorOnPath(ctx, err) +} + +func (ec *executionContext) marshalNID2string(ctx context.Context, sel ast.SelectionSet, v string) graphql.Marshaler { + res := graphql.MarshalID(v) + if res == graphql.Null { + if !graphql.HasFieldError(ctx, graphql.GetFieldContext(ctx)) { + ec.Errorf(ctx, "the requested element is null which the schema does not allow") + } + } + return res +} + +func (ec *executionContext) marshalNListedSecurity2ᚖgithubᚗcomᚋoxistoᚋmoneyᚑgopherᚋpersistenceᚐListedSecurity(ctx context.Context, sel ast.SelectionSet, v *persistence.ListedSecurity) graphql.Marshaler { + if v == nil { + if !graphql.HasFieldError(ctx, graphql.GetFieldContext(ctx)) { + ec.Errorf(ctx, "the requested element is null which the schema does not allow") + } + return graphql.Null + } + return ec._ListedSecurity(ctx, sel, v) +} + +func (ec *executionContext) unmarshalNListedSecurityInput2ᚖgithubᚗcomᚋoxistoᚋmoneyᚑgopherᚋgraphᚐListedSecurityInput(ctx context.Context, v any) (*ListedSecurityInput, error) { + res, err := ec.unmarshalInputListedSecurityInput(ctx, v) + return &res, graphql.ErrorOnPath(ctx, err) +} + +func (ec *executionContext) marshalNSecurity2githubᚗcomᚋoxistoᚋmoneyᚑgopherᚋpersistenceᚐSecurity(ctx context.Context, sel ast.SelectionSet, v persistence.Security) graphql.Marshaler { + return ec._Security(ctx, sel, &v) +} + +func (ec *executionContext) marshalNSecurity2ᚕᚖgithubᚗcomᚋoxistoᚋmoneyᚑgopherᚋpersistenceᚐSecurityᚄ(ctx context.Context, sel ast.SelectionSet, v []*persistence.Security) graphql.Marshaler { + ret := make(graphql.Array, len(v)) + var wg sync.WaitGroup + isLen1 := len(v) == 1 + if !isLen1 { + wg.Add(len(v)) + } + for i := range v { + i := i + fc := &graphql.FieldContext{ + Index: &i, + Result: &v[i], + } + ctx := graphql.WithFieldContext(ctx, fc) + f := func(i int) { + defer func() { + if r := recover(); r != nil { + ec.Error(ctx, ec.Recover(ctx, r)) + ret = nil + } + }() + if !isLen1 { + defer wg.Done() + } + ret[i] = ec.marshalNSecurity2ᚖgithubᚗcomᚋoxistoᚋmoneyᚑgopherᚋpersistenceᚐSecurity(ctx, sel, v[i]) + } + if isLen1 { + f(i) + } else { + go f(i) + } + + } + wg.Wait() + + for _, e := range ret { + if e == graphql.Null { + return graphql.Null + } + } + + return ret +} + +func (ec *executionContext) marshalNSecurity2ᚖgithubᚗcomᚋoxistoᚋmoneyᚑgopherᚋpersistenceᚐSecurity(ctx context.Context, sel ast.SelectionSet, v *persistence.Security) graphql.Marshaler { + if v == nil { + if !graphql.HasFieldError(ctx, graphql.GetFieldContext(ctx)) { + ec.Errorf(ctx, "the requested element is null which the schema does not allow") + } + return graphql.Null + } + return ec._Security(ctx, sel, v) +} + +func (ec *executionContext) unmarshalNSecurityInput2githubᚗcomᚋoxistoᚋmoneyᚑgopherᚋgraphᚐSecurityInput(ctx context.Context, v any) (SecurityInput, error) { + res, err := ec.unmarshalInputSecurityInput(ctx, v) + return res, graphql.ErrorOnPath(ctx, err) +} + +func (ec *executionContext) unmarshalNString2string(ctx context.Context, v any) (string, error) { + res, err := graphql.UnmarshalString(v) + return res, graphql.ErrorOnPath(ctx, err) +} + +func (ec *executionContext) marshalNString2string(ctx context.Context, sel ast.SelectionSet, v string) graphql.Marshaler { + res := graphql.MarshalString(v) + if res == graphql.Null { + if !graphql.HasFieldError(ctx, graphql.GetFieldContext(ctx)) { + ec.Errorf(ctx, "the requested element is null which the schema does not allow") + } + } + return res +} + +func (ec *executionContext) marshalN__Directive2githubᚗcomᚋ99designsᚋgqlgenᚋgraphqlᚋintrospectionᚐDirective(ctx context.Context, sel ast.SelectionSet, v introspection.Directive) graphql.Marshaler { + return ec.___Directive(ctx, sel, &v) +} + +func (ec *executionContext) marshalN__Directive2ᚕgithubᚗcomᚋ99designsᚋgqlgenᚋgraphqlᚋintrospectionᚐDirectiveᚄ(ctx context.Context, sel ast.SelectionSet, v []introspection.Directive) graphql.Marshaler { + ret := make(graphql.Array, len(v)) + var wg sync.WaitGroup + isLen1 := len(v) == 1 + if !isLen1 { + wg.Add(len(v)) + } + for i := range v { + i := i + fc := &graphql.FieldContext{ + Index: &i, + Result: &v[i], + } + ctx := graphql.WithFieldContext(ctx, fc) + f := func(i int) { + defer func() { + if r := recover(); r != nil { + ec.Error(ctx, ec.Recover(ctx, r)) + ret = nil + } + }() + if !isLen1 { + defer wg.Done() + } + ret[i] = ec.marshalN__Directive2githubᚗcomᚋ99designsᚋgqlgenᚋgraphqlᚋintrospectionᚐDirective(ctx, sel, v[i]) + } + if isLen1 { + f(i) + } else { + go f(i) + } + + } + wg.Wait() + + for _, e := range ret { + if e == graphql.Null { + return graphql.Null + } + } + + return ret +} + +func (ec *executionContext) unmarshalN__DirectiveLocation2string(ctx context.Context, v any) (string, error) { + res, err := graphql.UnmarshalString(v) + return res, graphql.ErrorOnPath(ctx, err) +} + +func (ec *executionContext) marshalN__DirectiveLocation2string(ctx context.Context, sel ast.SelectionSet, v string) graphql.Marshaler { + res := graphql.MarshalString(v) + if res == graphql.Null { + if !graphql.HasFieldError(ctx, graphql.GetFieldContext(ctx)) { + ec.Errorf(ctx, "the requested element is null which the schema does not allow") + } + } + return res +} + +func (ec *executionContext) unmarshalN__DirectiveLocation2ᚕstringᚄ(ctx context.Context, v any) ([]string, error) { + var vSlice []any + if v != nil { + vSlice = graphql.CoerceList(v) + } + var err error + res := make([]string, len(vSlice)) + for i := range vSlice { + ctx := graphql.WithPathContext(ctx, graphql.NewPathWithIndex(i)) + res[i], err = ec.unmarshalN__DirectiveLocation2string(ctx, vSlice[i]) + if err != nil { + return nil, err + } + } + return res, nil +} + +func (ec *executionContext) marshalN__DirectiveLocation2ᚕstringᚄ(ctx context.Context, sel ast.SelectionSet, v []string) graphql.Marshaler { + ret := make(graphql.Array, len(v)) + var wg sync.WaitGroup + isLen1 := len(v) == 1 + if !isLen1 { + wg.Add(len(v)) + } + for i := range v { + i := i + fc := &graphql.FieldContext{ + Index: &i, + Result: &v[i], + } + ctx := graphql.WithFieldContext(ctx, fc) + f := func(i int) { + defer func() { + if r := recover(); r != nil { + ec.Error(ctx, ec.Recover(ctx, r)) + ret = nil + } + }() + if !isLen1 { + defer wg.Done() + } + ret[i] = ec.marshalN__DirectiveLocation2string(ctx, sel, v[i]) + } + if isLen1 { + f(i) + } else { + go f(i) + } + + } + wg.Wait() + + for _, e := range ret { + if e == graphql.Null { + return graphql.Null + } + } + + return ret +} + +func (ec *executionContext) marshalN__EnumValue2githubᚗcomᚋ99designsᚋgqlgenᚋgraphqlᚋintrospectionᚐEnumValue(ctx context.Context, sel ast.SelectionSet, v introspection.EnumValue) graphql.Marshaler { + return ec.___EnumValue(ctx, sel, &v) +} + +func (ec *executionContext) marshalN__Field2githubᚗcomᚋ99designsᚋgqlgenᚋgraphqlᚋintrospectionᚐField(ctx context.Context, sel ast.SelectionSet, v introspection.Field) graphql.Marshaler { + return ec.___Field(ctx, sel, &v) +} + +func (ec *executionContext) marshalN__InputValue2githubᚗcomᚋ99designsᚋgqlgenᚋgraphqlᚋintrospectionᚐInputValue(ctx context.Context, sel ast.SelectionSet, v introspection.InputValue) graphql.Marshaler { + return ec.___InputValue(ctx, sel, &v) +} + +func (ec *executionContext) marshalN__InputValue2ᚕgithubᚗcomᚋ99designsᚋgqlgenᚋgraphqlᚋintrospectionᚐInputValueᚄ(ctx context.Context, sel ast.SelectionSet, v []introspection.InputValue) graphql.Marshaler { + ret := make(graphql.Array, len(v)) + var wg sync.WaitGroup + isLen1 := len(v) == 1 + if !isLen1 { + wg.Add(len(v)) + } + for i := range v { + i := i + fc := &graphql.FieldContext{ + Index: &i, + Result: &v[i], + } + ctx := graphql.WithFieldContext(ctx, fc) + f := func(i int) { + defer func() { + if r := recover(); r != nil { + ec.Error(ctx, ec.Recover(ctx, r)) + ret = nil + } + }() + if !isLen1 { + defer wg.Done() + } + ret[i] = ec.marshalN__InputValue2githubᚗcomᚋ99designsᚋgqlgenᚋgraphqlᚋintrospectionᚐInputValue(ctx, sel, v[i]) + } + if isLen1 { + f(i) + } else { + go f(i) + } + + } + wg.Wait() + + for _, e := range ret { + if e == graphql.Null { + return graphql.Null + } + } + + return ret +} + +func (ec *executionContext) marshalN__Type2githubᚗcomᚋ99designsᚋgqlgenᚋgraphqlᚋintrospectionᚐType(ctx context.Context, sel ast.SelectionSet, v introspection.Type) graphql.Marshaler { + return ec.___Type(ctx, sel, &v) +} + +func (ec *executionContext) marshalN__Type2ᚕgithubᚗcomᚋ99designsᚋgqlgenᚋgraphqlᚋintrospectionᚐTypeᚄ(ctx context.Context, sel ast.SelectionSet, v []introspection.Type) graphql.Marshaler { + ret := make(graphql.Array, len(v)) + var wg sync.WaitGroup + isLen1 := len(v) == 1 + if !isLen1 { + wg.Add(len(v)) + } + for i := range v { + i := i + fc := &graphql.FieldContext{ + Index: &i, + Result: &v[i], + } + ctx := graphql.WithFieldContext(ctx, fc) + f := func(i int) { + defer func() { + if r := recover(); r != nil { + ec.Error(ctx, ec.Recover(ctx, r)) + ret = nil + } + }() + if !isLen1 { + defer wg.Done() + } + ret[i] = ec.marshalN__Type2githubᚗcomᚋ99designsᚋgqlgenᚋgraphqlᚋintrospectionᚐType(ctx, sel, v[i]) + } + if isLen1 { + f(i) + } else { + go f(i) + } + + } + wg.Wait() + + for _, e := range ret { + if e == graphql.Null { + return graphql.Null + } + } + + return ret +} + +func (ec *executionContext) marshalN__Type2ᚖgithubᚗcomᚋ99designsᚋgqlgenᚋgraphqlᚋintrospectionᚐType(ctx context.Context, sel ast.SelectionSet, v *introspection.Type) graphql.Marshaler { + if v == nil { + if !graphql.HasFieldError(ctx, graphql.GetFieldContext(ctx)) { + ec.Errorf(ctx, "the requested element is null which the schema does not allow") + } + return graphql.Null + } + return ec.___Type(ctx, sel, v) +} + +func (ec *executionContext) unmarshalN__TypeKind2string(ctx context.Context, v any) (string, error) { + res, err := graphql.UnmarshalString(v) + return res, graphql.ErrorOnPath(ctx, err) +} + +func (ec *executionContext) marshalN__TypeKind2string(ctx context.Context, sel ast.SelectionSet, v string) graphql.Marshaler { + res := graphql.MarshalString(v) + if res == graphql.Null { + if !graphql.HasFieldError(ctx, graphql.GetFieldContext(ctx)) { + ec.Errorf(ctx, "the requested element is null which the schema does not allow") + } + } + return res +} + +func (ec *executionContext) unmarshalOBoolean2bool(ctx context.Context, v any) (bool, error) { + res, err := graphql.UnmarshalBoolean(v) + return res, graphql.ErrorOnPath(ctx, err) +} + +func (ec *executionContext) marshalOBoolean2bool(ctx context.Context, sel ast.SelectionSet, v bool) graphql.Marshaler { + res := graphql.MarshalBoolean(v) + return res +} + +func (ec *executionContext) unmarshalOBoolean2ᚖbool(ctx context.Context, v any) (*bool, error) { + if v == nil { + return nil, nil + } + res, err := graphql.UnmarshalBoolean(v) + return &res, graphql.ErrorOnPath(ctx, err) +} + +func (ec *executionContext) marshalOBoolean2ᚖbool(ctx context.Context, sel ast.SelectionSet, v *bool) graphql.Marshaler { + if v == nil { + return graphql.Null + } + res := graphql.MarshalBoolean(*v) + return res +} + +func (ec *executionContext) marshalOListedSecurity2ᚕᚖgithubᚗcomᚋoxistoᚋmoneyᚑgopherᚋpersistenceᚐListedSecurityᚄ(ctx context.Context, sel ast.SelectionSet, v []*persistence.ListedSecurity) graphql.Marshaler { + if v == nil { + return graphql.Null + } + ret := make(graphql.Array, len(v)) + var wg sync.WaitGroup + isLen1 := len(v) == 1 + if !isLen1 { + wg.Add(len(v)) + } + for i := range v { + i := i + fc := &graphql.FieldContext{ + Index: &i, + Result: &v[i], + } + ctx := graphql.WithFieldContext(ctx, fc) + f := func(i int) { + defer func() { + if r := recover(); r != nil { + ec.Error(ctx, ec.Recover(ctx, r)) + ret = nil + } + }() + if !isLen1 { + defer wg.Done() + } + ret[i] = ec.marshalNListedSecurity2ᚖgithubᚗcomᚋoxistoᚋmoneyᚑgopherᚋpersistenceᚐListedSecurity(ctx, sel, v[i]) + } + if isLen1 { + f(i) + } else { + go f(i) + } + + } + wg.Wait() + + for _, e := range ret { + if e == graphql.Null { + return graphql.Null + } + } + + return ret +} + +func (ec *executionContext) unmarshalOListedSecurityInput2ᚕᚖgithubᚗcomᚋoxistoᚋmoneyᚑgopherᚋgraphᚐListedSecurityInputᚄ(ctx context.Context, v any) ([]*ListedSecurityInput, error) { + if v == nil { + return nil, nil + } + var vSlice []any + if v != nil { + vSlice = graphql.CoerceList(v) + } + var err error + res := make([]*ListedSecurityInput, len(vSlice)) + for i := range vSlice { + ctx := graphql.WithPathContext(ctx, graphql.NewPathWithIndex(i)) + res[i], err = ec.unmarshalNListedSecurityInput2ᚖgithubᚗcomᚋoxistoᚋmoneyᚑgopherᚋgraphᚐListedSecurityInput(ctx, vSlice[i]) + if err != nil { + return nil, err + } + } + return res, nil +} + +func (ec *executionContext) marshalOSecurity2ᚖgithubᚗcomᚋoxistoᚋmoneyᚑgopherᚋpersistenceᚐSecurity(ctx context.Context, sel ast.SelectionSet, v *persistence.Security) graphql.Marshaler { + if v == nil { + return graphql.Null + } + return ec._Security(ctx, sel, v) +} + +func (ec *executionContext) unmarshalOString2ᚖstring(ctx context.Context, v any) (*string, error) { + if v == nil { + return nil, nil + } + res, err := graphql.UnmarshalString(v) + return &res, graphql.ErrorOnPath(ctx, err) +} + +func (ec *executionContext) marshalOString2ᚖstring(ctx context.Context, sel ast.SelectionSet, v *string) graphql.Marshaler { + if v == nil { + return graphql.Null + } + res := graphql.MarshalString(*v) + return res +} + +func (ec *executionContext) marshalO__EnumValue2ᚕgithubᚗcomᚋ99designsᚋgqlgenᚋgraphqlᚋintrospectionᚐEnumValueᚄ(ctx context.Context, sel ast.SelectionSet, v []introspection.EnumValue) graphql.Marshaler { + if v == nil { + return graphql.Null + } + ret := make(graphql.Array, len(v)) + var wg sync.WaitGroup + isLen1 := len(v) == 1 + if !isLen1 { + wg.Add(len(v)) + } + for i := range v { + i := i + fc := &graphql.FieldContext{ + Index: &i, + Result: &v[i], + } + ctx := graphql.WithFieldContext(ctx, fc) + f := func(i int) { + defer func() { + if r := recover(); r != nil { + ec.Error(ctx, ec.Recover(ctx, r)) + ret = nil + } + }() + if !isLen1 { + defer wg.Done() + } + ret[i] = ec.marshalN__EnumValue2githubᚗcomᚋ99designsᚋgqlgenᚋgraphqlᚋintrospectionᚐEnumValue(ctx, sel, v[i]) + } + if isLen1 { + f(i) + } else { + go f(i) + } + + } + wg.Wait() + + for _, e := range ret { + if e == graphql.Null { + return graphql.Null + } + } + + return ret +} + +func (ec *executionContext) marshalO__Field2ᚕgithubᚗcomᚋ99designsᚋgqlgenᚋgraphqlᚋintrospectionᚐFieldᚄ(ctx context.Context, sel ast.SelectionSet, v []introspection.Field) graphql.Marshaler { + if v == nil { + return graphql.Null + } + ret := make(graphql.Array, len(v)) + var wg sync.WaitGroup + isLen1 := len(v) == 1 + if !isLen1 { + wg.Add(len(v)) + } + for i := range v { + i := i + fc := &graphql.FieldContext{ + Index: &i, + Result: &v[i], + } + ctx := graphql.WithFieldContext(ctx, fc) + f := func(i int) { + defer func() { + if r := recover(); r != nil { + ec.Error(ctx, ec.Recover(ctx, r)) + ret = nil + } + }() + if !isLen1 { + defer wg.Done() + } + ret[i] = ec.marshalN__Field2githubᚗcomᚋ99designsᚋgqlgenᚋgraphqlᚋintrospectionᚐField(ctx, sel, v[i]) + } + if isLen1 { + f(i) + } else { + go f(i) + } + + } + wg.Wait() + + for _, e := range ret { + if e == graphql.Null { + return graphql.Null + } + } + + return ret +} + +func (ec *executionContext) marshalO__InputValue2ᚕgithubᚗcomᚋ99designsᚋgqlgenᚋgraphqlᚋintrospectionᚐInputValueᚄ(ctx context.Context, sel ast.SelectionSet, v []introspection.InputValue) graphql.Marshaler { + if v == nil { + return graphql.Null + } + ret := make(graphql.Array, len(v)) + var wg sync.WaitGroup + isLen1 := len(v) == 1 + if !isLen1 { + wg.Add(len(v)) + } + for i := range v { + i := i + fc := &graphql.FieldContext{ + Index: &i, + Result: &v[i], + } + ctx := graphql.WithFieldContext(ctx, fc) + f := func(i int) { + defer func() { + if r := recover(); r != nil { + ec.Error(ctx, ec.Recover(ctx, r)) + ret = nil + } + }() + if !isLen1 { + defer wg.Done() + } + ret[i] = ec.marshalN__InputValue2githubᚗcomᚋ99designsᚋgqlgenᚋgraphqlᚋintrospectionᚐInputValue(ctx, sel, v[i]) + } + if isLen1 { + f(i) + } else { + go f(i) + } + + } + wg.Wait() + + for _, e := range ret { + if e == graphql.Null { + return graphql.Null + } + } + + return ret +} + +func (ec *executionContext) marshalO__Schema2ᚖgithubᚗcomᚋ99designsᚋgqlgenᚋgraphqlᚋintrospectionᚐSchema(ctx context.Context, sel ast.SelectionSet, v *introspection.Schema) graphql.Marshaler { + if v == nil { + return graphql.Null + } + return ec.___Schema(ctx, sel, v) +} + +func (ec *executionContext) marshalO__Type2ᚕgithubᚗcomᚋ99designsᚋgqlgenᚋgraphqlᚋintrospectionᚐTypeᚄ(ctx context.Context, sel ast.SelectionSet, v []introspection.Type) graphql.Marshaler { + if v == nil { + return graphql.Null + } + ret := make(graphql.Array, len(v)) + var wg sync.WaitGroup + isLen1 := len(v) == 1 + if !isLen1 { + wg.Add(len(v)) + } + for i := range v { + i := i + fc := &graphql.FieldContext{ + Index: &i, + Result: &v[i], + } + ctx := graphql.WithFieldContext(ctx, fc) + f := func(i int) { + defer func() { + if r := recover(); r != nil { + ec.Error(ctx, ec.Recover(ctx, r)) + ret = nil + } + }() + if !isLen1 { + defer wg.Done() + } + ret[i] = ec.marshalN__Type2githubᚗcomᚋ99designsᚋgqlgenᚋgraphqlᚋintrospectionᚐType(ctx, sel, v[i]) + } + if isLen1 { + f(i) + } else { + go f(i) + } + + } + wg.Wait() + + for _, e := range ret { + if e == graphql.Null { + return graphql.Null + } + } + + return ret +} + +func (ec *executionContext) marshalO__Type2ᚖgithubᚗcomᚋ99designsᚋgqlgenᚋgraphqlᚋintrospectionᚐType(ctx context.Context, sel ast.SelectionSet, v *introspection.Type) graphql.Marshaler { + if v == nil { + return graphql.Null + } + return ec.___Type(ctx, sel, v) +} + +// endregion ***************************** type.gotpl ***************************** diff --git a/graph/models_gen.go b/graph/models_gen.go new file mode 100644 index 00000000..ffbaa5e3 --- /dev/null +++ b/graph/models_gen.go @@ -0,0 +1,20 @@ +// Code generated by github.com/99designs/gqlgen, DO NOT EDIT. + +package graph + +type ListedSecurityInput struct { + Ticker string `json:"ticker"` + Currency string `json:"currency"` +} + +type Mutation struct { +} + +type Query struct { +} + +type SecurityInput struct { + ID string `json:"id"` + DisplayName string `json:"displayName"` + ListedAs []*ListedSecurityInput `json:"listedAs,omitempty"` +} diff --git a/graph/resolver.go b/graph/resolver.go new file mode 100644 index 00000000..34db2539 --- /dev/null +++ b/graph/resolver.go @@ -0,0 +1,34 @@ +package graph + +import ( + "github.com/oxisto/money-gopher/persistence" +) + +// This file will not be regenerated automatically. +// +// It serves as dependency injection for your app, add any dependencies you require here. + +type Resolver struct { + DB *persistence.DB +} + +func withTx[T any](r *Resolver, f func(qtx *persistence.Queries) (*T, error)) (res *T, err error) { + tx, err := r.DB.Begin() + if err != nil { + return nil, err + } + defer tx.Rollback() + + qtx := r.DB.WithTx(tx) + res, err = f(qtx) + if err != nil { + return nil, err + } + + err = tx.Commit() + if err != nil { + return nil, err + } + + return res, nil +} diff --git a/graph/schema.graphqls b/graph/schema.graphqls new file mode 100644 index 00000000..d36fea52 --- /dev/null +++ b/graph/schema.graphqls @@ -0,0 +1,33 @@ +type Security { + id: String! + displayName: String! + quoteProvider: String + listedAs: [ListedSecurity!] +} + +type ListedSecurity { + ticker: String! + currency: String! + security: Security! +} + +input SecurityInput { + id: String! + displayName: String! + listedAs: [ListedSecurityInput!] +} + +input ListedSecurityInput { + ticker: String! + currency: String! +} + +type Mutation { + createSecurity(input: SecurityInput!): Security! + updateSecurity(id: ID!, input: SecurityInput!): Security! +} + +type Query { + security(id: String!): Security + securities: [Security!]! +} diff --git a/graph/schema.resolvers.go b/graph/schema.resolvers.go new file mode 100644 index 00000000..f6080aeb --- /dev/null +++ b/graph/schema.resolvers.go @@ -0,0 +1,133 @@ +package graph + +// This file will be automatically regenerated based on the schema, any resolver implementations +// will be copied through when generating and any unknown code will be moved to the end. +// Code generated by github.com/99designs/gqlgen version v0.17.61 + +import ( + "context" + "slices" + + "github.com/oxisto/money-gopher/persistence" +) + +// Security is the resolver for the security field. +func (r *listedSecurityResolver) Security(ctx context.Context, obj *persistence.ListedSecurity) (*persistence.Security, error) { + return r.DB.GetSecurity(ctx, obj.SecurityID) +} + +// CreateSecurity is the resolver for the createSecurity field. +func (r *mutationResolver) CreateSecurity(ctx context.Context, input SecurityInput) (*persistence.Security, error) { + return withTx(r.Resolver, func(qtx *persistence.Queries) (*persistence.Security, error) { + sec, err := qtx.CreateSecurity(ctx, persistence.CreateSecurityParams{ + ID: input.ID, + DisplayName: input.DisplayName, + }) + if err != nil { + return nil, err + } + + for _, listed := range input.ListedAs { + _, err = qtx.UpsertListedSecurity(ctx, persistence.UpsertListedSecurityParams{ + SecurityID: sec.ID, + Ticker: listed.Ticker, + Currency: listed.Currency, + }) + if err != nil { + return nil, err + } + } + + return sec, nil + }) +} + +// UpdateSecurity is the resolver for the updateSecurity field. +func (r *mutationResolver) UpdateSecurity(ctx context.Context, id string, input SecurityInput) (*persistence.Security, error) { + return withTx(r.Resolver, func(qtx *persistence.Queries) (*persistence.Security, error) { + sec, err := qtx.UpdateSecurity(ctx, persistence.UpdateSecurityParams{ + ID: id, + DisplayName: input.DisplayName, + }) + if err != nil { + return nil, err + } + + // Retrieve old listed securities + oldListed, err := qtx.ListListedSecuritiesBySecurityID(ctx, id) + if err != nil { + return nil, err + } + + // Upsert the new listed securities + for _, listed := range input.ListedAs { + _, err = qtx.UpsertListedSecurity(ctx, persistence.UpsertListedSecurityParams{ + SecurityID: sec.ID, + Ticker: listed.Ticker, + Currency: listed.Currency, + }) + if err != nil { + return nil, err + } + + // Remove the listed security from the old list + oldListed = slices.DeleteFunc(oldListed, func(ls *persistence.ListedSecurity) bool { + return ls.Ticker == listed.Ticker + }) + } + + // Remove the old listed securities + for _, old := range oldListed { + _, err = qtx.DeleteListedSecurity(ctx, persistence.DeleteListedSecurityParams{ + SecurityID: sec.ID, + Ticker: old.Ticker, + }) + if err != nil { + return nil, err + } + } + + return sec, nil + }) +} + +// Security is the resolver for the security field. +func (r *queryResolver) Security(ctx context.Context, id string) (*persistence.Security, error) { + return r.DB.GetSecurity(ctx, id) +} + +// Securities is the resolver for the securities field. +func (r *queryResolver) Securities(ctx context.Context) ([]*persistence.Security, error) { + return r.DB.ListSecurities(ctx) +} + +// QuoteProvider is the resolver for the quoteProvider field. +func (r *securityResolver) QuoteProvider(ctx context.Context, obj *persistence.Security) (*string, error) { + if obj.QuoteProvider.Valid { + return &obj.QuoteProvider.String, nil + } + + return nil, nil +} + +// ListedAs is the resolver for the listedAs field. +func (r *securityResolver) ListedAs(ctx context.Context, obj *persistence.Security) ([]*persistence.ListedSecurity, error) { + return r.DB.ListListedSecuritiesBySecurityID(ctx, obj.ID) +} + +// ListedSecurity returns ListedSecurityResolver implementation. +func (r *Resolver) ListedSecurity() ListedSecurityResolver { return &listedSecurityResolver{r} } + +// Mutation returns MutationResolver implementation. +func (r *Resolver) Mutation() MutationResolver { return &mutationResolver{r} } + +// Query returns QueryResolver implementation. +func (r *Resolver) Query() QueryResolver { return &queryResolver{r} } + +// Security returns SecurityResolver implementation. +func (r *Resolver) Security() SecurityResolver { return &securityResolver{r} } + +type listedSecurityResolver struct{ *Resolver } +type mutationResolver struct{ *Resolver } +type queryResolver struct{ *Resolver } +type securityResolver struct{ *Resolver } diff --git a/internal/persistence.go b/internal/persistence.go index 9ac60b1c..02507667 100644 --- a/internal/persistence.go +++ b/internal/persistence.go @@ -27,7 +27,7 @@ func NewTestDB(t *testing.T, inits ...func(db *persistence.DB)) (db *persistence err error ) - db, _, err = persistence.OpenDB(persistence.Options{UseInMemory: true}) + db, err = persistence.OpenDB(persistence.Options{UseInMemory: true}) if err != nil { t.Fatalf("Could not create test DB: %v", err) } diff --git a/persistence/persistence.go b/persistence/persistence.go index f3d91511..7f77ff36 100644 --- a/persistence/persistence.go +++ b/persistence/persistence.go @@ -49,7 +49,10 @@ func (o Options) LogValue() slog.Value { } // DB is a type alias around [sql.DB] to avoid importing the [database/sql] package. -type DB = sql.DB +type DB struct { + *Queries + *sql.DB +} type StorageObject interface { InitTables(db *DB) (err error) @@ -80,22 +83,23 @@ type ops[T StorageObject] struct { } // OpenDB opens a connection to our database. -func OpenDB(opts Options) (db *DB, q *Queries, err error) { +func OpenDB(opts Options) (db *DB, err error) { if opts.UseInMemory { opts.DSN = ":memory:?_pragma=foreign_keys(1)" } else if opts.DSN == "" { opts.DSN = "money.db" } - db, err = sql.Open("sqlite3", opts.DSN) + inner, err := sql.Open("sqlite3", opts.DSN) if err != nil { - return nil, nil, fmt.Errorf("could not open database: %w", err) + return nil, fmt.Errorf("could not open database: %w", err) } + db = &DB{Queries: New(inner), DB: inner} slog.Info("Successfully opened database connection", "opts", opts) // Prepare database migrations with goose - provider, err := goose.NewProvider(database.DialectSQLite3, db, migrations.Embed) + provider, err := goose.NewProvider(database.DialectSQLite3, db.DB, migrations.Embed) if err != nil { log.Fatal(err) } @@ -103,16 +107,13 @@ func OpenDB(opts Options) (db *DB, q *Queries, err error) { // Apply all migrations results, err := provider.Up(context.Background()) if err != nil { - return nil, nil, err + return nil, err } for _, result := range results { slog.Debug("Applied migration.", "migration", result) } - // Create a new query object - q = New(db) - return } diff --git a/persistence/persistence_test.go b/persistence/persistence_test.go index 2387ed38..76447829 100644 --- a/persistence/persistence_test.go +++ b/persistence/persistence_test.go @@ -54,7 +54,7 @@ func TestOpenDB(t *testing.T) { } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - gotDb, _, err := OpenDB(tt.args.opts) + gotDb, err := OpenDB(tt.args.opts) if (err != nil) != tt.wantErr { t.Errorf("OpenDB() error = %v, wantErr %v", err, tt.wantErr) return diff --git a/server/commands/init.go b/server/commands/init.go index cafcac43..4be7606b 100644 --- a/server/commands/init.go +++ b/server/commands/init.go @@ -83,11 +83,13 @@ func RunServer(ctx context.Context, cmd *cli.Command) error { slog.SetDefault(logger) slog.Info("Welcome to the Money Gopher", "money", "🤑") - pdb, q, err := persistence.OpenDB(persistence.Options{}) + db, err := persistence.OpenDB(persistence.Options{}) if err != nil { slog.Error("Error while opening database", tint.Err(err)) return err } - return server.StartServer(pdb, q, opts) + go server.StartGraphQLServer(db) + + return server.StartServer(db, opts) } diff --git a/server/server.go b/server/server.go index 9795d73a..dbbe99da 100644 --- a/server/server.go +++ b/server/server.go @@ -18,10 +18,16 @@ package server import ( "crypto/ecdsa" + "log" "log/slog" "net/http" + "github.com/99designs/gqlgen/graphql/handler" + "github.com/99designs/gqlgen/graphql/handler/extension" + "github.com/99designs/gqlgen/graphql/handler/transport" + "github.com/99designs/gqlgen/graphql/playground" "github.com/oxisto/money-gopher/gen/portfoliov1connect" + "github.com/oxisto/money-gopher/graph" "github.com/oxisto/money-gopher/persistence" "github.com/oxisto/money-gopher/service/portfolio" "github.com/oxisto/money-gopher/service/securities" @@ -47,7 +53,7 @@ type Options struct { } // StartServer starts the server. -func StartServer(pdb *persistence.DB, q *persistence.Queries, opts Options) (err error) { +func StartServer(pdb *persistence.DB, opts Options) (err error) { var ( authSrv *oauth2.AuthorizationServer transcoder *vanguard.Transcoder @@ -108,3 +114,23 @@ func StartServer(pdb *persistence.DB, q *persistence.Queries, opts Options) (err slog.Error("listen failed", tint.Err(err)) return err } + +func StartGraphQLServer(db *persistence.DB) (err error) { + port := "9090" + srv := handler.New(graph.NewExecutableSchema(graph.Config{Resolvers: &graph.Resolver{ + DB: db, + }})) + + srv.AddTransport(transport.Options{}) + srv.AddTransport(transport.GET{}) + srv.AddTransport(transport.POST{}) + + srv.Use(extension.Introspection{}) + + http.Handle("/", playground.Handler("GraphQL playground", "/query")) + http.Handle("/query", srv) + + log.Printf("connect to http://localhost:%s/ for GraphQL playground", port) + log.Fatal(http.ListenAndServe(":"+port, nil)) + return err +} From 72e3ba0c3c22e73aca7ba656d51ac952146548ec Mon Sep 17 00:00:00 2001 From: Christian Banse Date: Fri, 27 Dec 2024 17:32:11 +0100 Subject: [PATCH 02/35] Migrating first clients to graphQL --- cli/cli.go | 14 +++++ cli/commands/securities.go | 51 +++++++++++++++- cli/commands/securities_test.go | 72 +++++++++++++++++++++-- go.mod | 1 + go.sum | 2 + internal/testing/servertest/servertest.go | 2 + server/commands/init.go | 2 - server/server.go | 12 ++-- 8 files changed, 140 insertions(+), 16 deletions(-) diff --git a/cli/cli.go b/cli/cli.go index 768582d4..e9b58531 100644 --- a/cli/cli.go +++ b/cli/cli.go @@ -21,11 +21,13 @@ import ( "context" "encoding/json" "fmt" + "io" "log/slog" "net/http" "os" "github.com/oxisto/money-gopher/gen/portfoliov1connect" + "github.com/shurcooL/graphql" "connectrpc.com/connect" "github.com/lmittmann/tint" @@ -42,6 +44,7 @@ var SessionKey sessionKeyType type Session struct { PortfolioClient portfoliov1connect.PortfolioServiceClient `json:"-"` SecuritiesClient portfoliov1connect.SecuritiesServiceClient `json:"-"` + GraphQL *graphql.Client opts *SessionOptions } @@ -78,6 +81,9 @@ func (opts *SessionOptions) MergeWith(other *SessionOptions) *SessionOptions { // DefaultBaseURL is the default base URL for all services. const DefaultBaseURL = "http://localhost:8080" +// DefaultGraphURL is the default URL for the GraphQL service. +const DefaultGraphURL = "http://localhost:9090/graphql" + // NewSession creates a new session. func NewSession(opts *SessionOptions) (s *Session) { def := &SessionOptions{ @@ -170,6 +176,14 @@ func (s *Session) initClients() { connect.WithHTTPGet(), connect.WithInterceptors(connect.UnaryInterceptorFunc(interceptor)), ) + + s.GraphQL = graphql.NewClient(s.opts.BaseURL+"/graphql/query", s.opts.HttpClient) +} + +func (s *Session) WriteJSON(w io.Writer, v any) { + enc := json.NewEncoder(w) + enc.SetIndent("", " ") + enc.Encode(v) } // FromContext extracts the session from the context. diff --git a/cli/commands/securities.go b/cli/commands/securities.go index d189eeeb..d48b3ec3 100644 --- a/cli/commands/securities.go +++ b/cli/commands/securities.go @@ -23,6 +23,7 @@ import ( mcli "github.com/oxisto/money-gopher/cli" portfoliov1 "github.com/oxisto/money-gopher/gen" + "github.com/shurcooL/graphql" "connectrpc.com/connect" "github.com/urfave/cli/v3" @@ -39,6 +40,14 @@ var SecuritiesCmd = &cli.Command{ Usage: "Lists all securities", Action: ListSecurities, }, + { + Name: "show", + Usage: "Shows information about a security", + Action: ShowSecurity, + Flags: []cli.Flag{ + &cli.StringFlag{Name: "security-id", Usage: "The security ID", Required: true}, + }, + }, { Name: "update-quote", Usage: "Triggers an update of one or more securities' quotes", @@ -58,13 +67,49 @@ var SecuritiesCmd = &cli.Command{ } // ListSecurities lists all securities. -func ListSecurities(ctx context.Context, cmd *cli.Command) error { +func ListSecurities(ctx context.Context, cmd *cli.Command) (err error) { s := mcli.FromContext(ctx) - res, err := s.SecuritiesClient.ListSecurities(context.Background(), connect.NewRequest(&portfoliov1.ListSecuritiesRequest{})) + + var query struct { + Securities []struct { + ID string `json:"id"` + DisplayName string `json:"displayName"` + } `json:"securities"` + } + + err = s.GraphQL.Query(context.Background(), &query, nil) + if err != nil { + return err + } + + s.WriteJSON(cmd.Writer, query) + + return nil +} + +// ShowSecurity shows information about a security. +func ShowSecurity(ctx context.Context, cmd *cli.Command) (err error) { + s := mcli.FromContext(ctx) + + var query struct { + Security struct { + ID string `json:"id"` + DisplayName string `json:"displayName"` + ListedAs []struct { + Ticker string `json:"ticker"` + } `json:"listedAs"` + } `graphql:"security(id: $id)" json:"security"` + } + + err = s.GraphQL.Query(context.Background(), &query, map[string]interface{}{ + "id": graphql.String(cmd.String("security-id")), + }) if err != nil { return err } - fmt.Fprintln(cmd.Writer, res.Msg.Securities) + + s.WriteJSON(cmd.Writer, query) + return nil } diff --git a/cli/commands/securities_test.go b/cli/commands/securities_test.go index 346641e0..366356a0 100644 --- a/cli/commands/securities_test.go +++ b/cli/commands/securities_test.go @@ -19,17 +19,15 @@ package commands import ( "context" - "strings" "testing" + "github.com/oxisto/assert" moneygopher "github.com/oxisto/money-gopher" portfoliov1 "github.com/oxisto/money-gopher/gen" "github.com/oxisto/money-gopher/internal" "github.com/oxisto/money-gopher/internal/testing/clitest" "github.com/oxisto/money-gopher/internal/testing/servertest" "github.com/oxisto/money-gopher/persistence" - - "github.com/oxisto/assert" "github.com/urfave/cli/v3" ) @@ -142,7 +140,19 @@ func TestListSecurities(t *testing.T) { cmd: clitest.MockCommand(t, SecuritiesCmd.Command("list").Flags), }, wantRec: func(t *testing.T, rec *clitest.CommandRecorder) bool { - return assert.Equals(t, true, strings.Contains(rec.String(), "1234")) + return assert.Equals(t, `{ + "securities": [ + { + "id": "1234", + "displayName": "One Two Three Four" + }, + { + "id": "US0378331005", + "displayName": "Apple Inc." + } + ] +} +`, rec.String()) }, }, } @@ -162,6 +172,60 @@ func TestListSecurities(t *testing.T) { } } +func TestShowSecurity(t *testing.T) { + srv := servertest.NewServer(internal.NewTestDB(t)) + defer srv.Close() + + type args struct { + ctx context.Context + cmd *cli.Command + } + tests := []struct { + name string + args args + wantErr bool + wantRec assert.Want[*clitest.CommandRecorder] + }{ + { + name: "happy path", + args: args{ + ctx: clitest.NewSessionContext(t, srv), + cmd: clitest.MockCommand(t, SecuritiesCmd.Command("show").Flags, "--security-id", "US0378331005"), + }, + wantRec: func(t *testing.T, rec *clitest.CommandRecorder) bool { + return assert.Equals(t, `{ + "security": { + "id": "US0378331005", + "displayName": "Apple Inc.", + "listedAs": [ + { + "ticker": "AAPL" + }, + { + "ticker": "APC.F" + } + ] + } +} +`, rec.String()) + }, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + rec := clitest.Record(tt.args.cmd) + if err := ShowSecurity(tt.args.ctx, tt.args.cmd); (err != nil) != tt.wantErr { + t.Errorf("ShowSecurity() error = %v, wantErr %v", err, tt.wantErr) + } + + if tt.wantRec != nil { + tt.wantRec(t, rec) + } + }) + } +} + func TestPredictSecurities(t *testing.T) { srv := servertest.NewServer(internal.NewTestDB(t)) defer srv.Close() diff --git a/go.mod b/go.mod index 84022aab..05f65ff5 100644 --- a/go.mod +++ b/go.mod @@ -18,6 +18,7 @@ require ( github.com/oxisto/assert v0.1.2 github.com/oxisto/oauth2go v0.14.0 github.com/pressly/goose/v3 v3.24.0 + github.com/shurcooL/graphql v0.0.0-20230722043721-ed46e5a46466 github.com/urfave/cli/v3 v3.0.0-beta1 github.com/vektah/gqlparser/v2 v2.5.20 golang.org/x/net v0.33.0 diff --git a/go.sum b/go.sum index 623db40e..1b230d67 100644 --- a/go.sum +++ b/go.sum @@ -63,6 +63,8 @@ github.com/sergi/go-diff v1.3.1 h1:xkr+Oxo4BOQKmkn/B9eMK0g5Kg/983T9DqqPHwYqD+8= github.com/sergi/go-diff v1.3.1/go.mod h1:aMJSSKb2lpPvRNec0+w3fl7LP9IOFzdc9Pa4NFbPK1I= github.com/sethvargo/go-retry v0.3.0 h1:EEt31A35QhrcRZtrYFDTBg91cqZVnFL2navjDrah2SE= github.com/sethvargo/go-retry v0.3.0/go.mod h1:mNX17F0C/HguQMyMyJxcnU471gOZGxCLyYaFyAZraas= +github.com/shurcooL/graphql v0.0.0-20230722043721-ed46e5a46466 h1:17JxqqJY66GmZVHkmAsGEkcIu0oCe3AM420QDgGwZx0= +github.com/shurcooL/graphql v0.0.0-20230722043721-ed46e5a46466/go.mod h1:9dIRpgIY7hVhoqfe0/FcYp0bpInZaT7dc3BYOprrIUE= github.com/sosodev/duration v1.3.1 h1:qtHBDMQ6lvMQsL15g4aopM4HEfOaYuhWBw3NPTtlqq4= github.com/sosodev/duration v1.3.1/go.mod h1:RQIBBX0+fMLc/D9+Jb/fwvVmo0eZvDDEERAikUR6SDg= github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= diff --git a/internal/testing/servertest/servertest.go b/internal/testing/servertest/servertest.go index 02489a99..bde60948 100644 --- a/internal/testing/servertest/servertest.go +++ b/internal/testing/servertest/servertest.go @@ -6,6 +6,7 @@ import ( "github.com/oxisto/money-gopher/gen/portfoliov1connect" "github.com/oxisto/money-gopher/persistence" + "github.com/oxisto/money-gopher/server" "github.com/oxisto/money-gopher/service/portfolio" "github.com/oxisto/money-gopher/service/securities" @@ -16,6 +17,7 @@ import ( func NewServer(db *persistence.DB) *httptest.Server { mux := http.NewServeMux() srv := httptest.NewServer(h2c.NewHandler(mux, &http2.Server{})) + server.ConfigureGraphQL(mux, db) mux.Handle(portfoliov1connect.NewPortfolioServiceHandler(portfolio.NewService( portfolio.Options{ diff --git a/server/commands/init.go b/server/commands/init.go index 4be7606b..5239dd86 100644 --- a/server/commands/init.go +++ b/server/commands/init.go @@ -89,7 +89,5 @@ func RunServer(ctx context.Context, cmd *cli.Command) error { return err } - go server.StartGraphQLServer(db) - return server.StartServer(db, opts) } diff --git a/server/server.go b/server/server.go index dbbe99da..5428eca5 100644 --- a/server/server.go +++ b/server/server.go @@ -18,7 +18,6 @@ package server import ( "crypto/ecdsa" - "log" "log/slog" "net/http" @@ -105,6 +104,7 @@ func StartServer(pdb *persistence.DB, opts Options) (err error) { mux := http.NewServeMux() mux.Handle("/", transcoder) + ConfigureGraphQL(mux, pdb) err = http.ListenAndServe( ":8080", @@ -115,8 +115,8 @@ func StartServer(pdb *persistence.DB, opts Options) (err error) { return err } -func StartGraphQLServer(db *persistence.DB) (err error) { - port := "9090" +// ConfigureGraphQL configures the GraphQL server for a [http.ServeMux]. +func ConfigureGraphQL(mux *http.ServeMux, db *persistence.DB) (err error) { srv := handler.New(graph.NewExecutableSchema(graph.Config{Resolvers: &graph.Resolver{ DB: db, }})) @@ -127,10 +127,8 @@ func StartGraphQLServer(db *persistence.DB) (err error) { srv.Use(extension.Introspection{}) - http.Handle("/", playground.Handler("GraphQL playground", "/query")) - http.Handle("/query", srv) + mux.Handle("/graphql", playground.Handler("GraphQL playground", "/graphql/query")) + mux.Handle("/graphql/query", srv) - log.Printf("connect to http://localhost:%s/ for GraphQL playground", port) - log.Fatal(http.ListenAndServe(":"+port, nil)) return err } From b2f8e81f90b2e0fbb6f460d250218c50aeac3944 Mon Sep 17 00:00:00 2001 From: Christian Banse Date: Fri, 27 Dec 2024 17:36:21 +0100 Subject: [PATCH 03/35] Ignore gqlgen generated files for introspection for coverage --- .codecov.yml | 1 + cli/commands/securities.go | 2 +- cli/commands/securities_test.go | 3 ++- 3 files changed, 4 insertions(+), 2 deletions(-) diff --git a/.codecov.yml b/.codecov.yml index 494bafc0..48641086 100644 --- a/.codecov.yml +++ b/.codecov.yml @@ -1,6 +1,7 @@ ignore: - "**/*.pb.go" # ignore protoc generated files - "**/*.connect.go" # ignore connect-go generated files + - "graph/generated.go" # ignore gqlgen generated files for introspection coverage: range: "70...85" status: diff --git a/cli/commands/securities.go b/cli/commands/securities.go index d48b3ec3..b9c0acfb 100644 --- a/cli/commands/securities.go +++ b/cli/commands/securities.go @@ -23,9 +23,9 @@ import ( mcli "github.com/oxisto/money-gopher/cli" portfoliov1 "github.com/oxisto/money-gopher/gen" - "github.com/shurcooL/graphql" "connectrpc.com/connect" + "github.com/shurcooL/graphql" "github.com/urfave/cli/v3" ) diff --git a/cli/commands/securities_test.go b/cli/commands/securities_test.go index 366356a0..c49942be 100644 --- a/cli/commands/securities_test.go +++ b/cli/commands/securities_test.go @@ -21,13 +21,14 @@ import ( "context" "testing" - "github.com/oxisto/assert" moneygopher "github.com/oxisto/money-gopher" portfoliov1 "github.com/oxisto/money-gopher/gen" "github.com/oxisto/money-gopher/internal" "github.com/oxisto/money-gopher/internal/testing/clitest" "github.com/oxisto/money-gopher/internal/testing/servertest" "github.com/oxisto/money-gopher/persistence" + + "github.com/oxisto/assert" "github.com/urfave/cli/v3" ) From e854bb0e0a54d6e8982841c1059b63b54978cedd Mon Sep 17 00:00:00 2001 From: Christian Banse Date: Fri, 27 Dec 2024 22:55:34 +0100 Subject: [PATCH 04/35] Compiles, but tests fail --- cli/commands/securities.go | 18 +- cli/commands/securities_test.go | 1 + graph/generated.go | 547 +++++++++++++++++- graph/resolver.go | 4 +- graph/schema.graphqls | 14 + graph/schema.resolvers.go | 47 +- internal/testing/servertest/servertest.go | 6 +- persistence/currency.go | 20 + persistence/relationships.go | 8 + persistence/securities.sql.go | 30 +- .../sql/migrations/0001_create_securities.sql | 3 +- persistence/sql/queries/securities.sql | 14 +- server/server.go | 14 +- service/securities/quote.go | 68 ++- service/securities/quote_provider.go | 4 +- service/securities/quote_provider_ing.go | 10 +- service/securities/quote_provider_ing_test.go | 20 +- service/securities/quote_provider_yf.go | 6 +- service/securities/quote_provider_yf_test.go | 24 +- service/securities/quote_test.go | 18 +- service/securities/service.go | 10 + 21 files changed, 787 insertions(+), 99 deletions(-) create mode 100644 persistence/currency.go create mode 100644 persistence/relationships.go diff --git a/cli/commands/securities.go b/cli/commands/securities.go index b9c0acfb..a398598e 100644 --- a/cli/commands/securities.go +++ b/cli/commands/securities.go @@ -114,15 +114,19 @@ func ShowSecurity(ctx context.Context, cmd *cli.Command) (err error) { } // UpdateQuote triggers an update of one or more securities' quotes. -func UpdateQuote(ctx context.Context, cmd *cli.Command) error { +func UpdateQuote(ctx context.Context, cmd *cli.Command) (err error) { s := mcli.FromContext(ctx) - _, err := s.SecuritiesClient.TriggerSecurityQuoteUpdate( - context.Background(), - connect.NewRequest(&portfoliov1.TriggerQuoteUpdateRequest{ - SecurityIds: cmd.StringSlice("security-ids"), - }), - ) + var query struct { + TriggerQuoteUpdate bool `graphql:"triggerQuoteUpdate(securityIDs: $IDs)" json:"security"` + } + + err = s.GraphQL.Mutate(context.Background(), &query, map[string]interface{}{ + "IDs": []graphql.String{"1"}, + }) + if err != nil { + return err + } return err } diff --git a/cli/commands/securities_test.go b/cli/commands/securities_test.go index c49942be..4ba2f6ec 100644 --- a/cli/commands/securities_test.go +++ b/cli/commands/securities_test.go @@ -62,6 +62,7 @@ func TestUpdateQuote(t *testing.T) { }, }, } + for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { if err := UpdateQuote(tt.args.ctx, tt.args.cmd); (err != nil) != tt.wantErr { diff --git a/graph/generated.go b/graph/generated.go index 93aaffe2..8ec6f6b7 100644 --- a/graph/generated.go +++ b/graph/generated.go @@ -39,6 +39,7 @@ type Config struct { } type ResolverRoot interface { + Currency() CurrencyResolver ListedSecurity() ListedSecurityResolver Mutation() MutationResolver Query() QueryResolver @@ -49,15 +50,23 @@ type DirectiveRoot struct { } type ComplexityRoot struct { - ListedSecurity struct { + Currency struct { Currency func(childComplexity int) int - Security func(childComplexity int) int - Ticker func(childComplexity int) int + Value func(childComplexity int) int + } + + ListedSecurity struct { + Currency func(childComplexity int) int + LatestQuote func(childComplexity int) int + LatestQuoteTimestamp func(childComplexity int) int + Security func(childComplexity int) int + Ticker func(childComplexity int) int } Mutation struct { - CreateSecurity func(childComplexity int, input SecurityInput) int - UpdateSecurity func(childComplexity int, id string, input SecurityInput) int + CreateSecurity func(childComplexity int, input SecurityInput) int + TriggerQuoteUpdate func(childComplexity int, securityIDs []string) int + UpdateSecurity func(childComplexity int, id string, input SecurityInput) int } Query struct { @@ -73,12 +82,18 @@ type ComplexityRoot struct { } } +type CurrencyResolver interface { + Currency(ctx context.Context, obj *persistence.Currency) (string, error) +} type ListedSecurityResolver interface { Security(ctx context.Context, obj *persistence.ListedSecurity) (*persistence.Security, error) + LatestQuote(ctx context.Context, obj *persistence.ListedSecurity) (*persistence.Currency, error) + LatestQuoteTimestamp(ctx context.Context, obj *persistence.ListedSecurity) (*string, error) } type MutationResolver interface { CreateSecurity(ctx context.Context, input SecurityInput) (*persistence.Security, error) UpdateSecurity(ctx context.Context, id string, input SecurityInput) (*persistence.Security, error) + TriggerQuoteUpdate(ctx context.Context, securityIDs []string) (bool, error) } type QueryResolver interface { Security(ctx context.Context, id string) (*persistence.Security, error) @@ -108,6 +123,20 @@ func (e *executableSchema) Complexity(typeName, field string, childComplexity in _ = ec switch typeName + "." + field { + case "Currency.currency": + if e.complexity.Currency.Currency == nil { + break + } + + return e.complexity.Currency.Currency(childComplexity), true + + case "Currency.value": + if e.complexity.Currency.Value == nil { + break + } + + return e.complexity.Currency.Value(childComplexity), true + case "ListedSecurity.currency": if e.complexity.ListedSecurity.Currency == nil { break @@ -115,6 +144,20 @@ func (e *executableSchema) Complexity(typeName, field string, childComplexity in return e.complexity.ListedSecurity.Currency(childComplexity), true + case "ListedSecurity.latestQuote": + if e.complexity.ListedSecurity.LatestQuote == nil { + break + } + + return e.complexity.ListedSecurity.LatestQuote(childComplexity), true + + case "ListedSecurity.latestQuoteTimestamp": + if e.complexity.ListedSecurity.LatestQuoteTimestamp == nil { + break + } + + return e.complexity.ListedSecurity.LatestQuoteTimestamp(childComplexity), true + case "ListedSecurity.security": if e.complexity.ListedSecurity.Security == nil { break @@ -141,6 +184,18 @@ func (e *executableSchema) Complexity(typeName, field string, childComplexity in return e.complexity.Mutation.CreateSecurity(childComplexity, args["input"].(SecurityInput)), true + case "Mutation.triggerQuoteUpdate": + if e.complexity.Mutation.TriggerQuoteUpdate == nil { + break + } + + args, err := ec.field_Mutation_triggerQuoteUpdate_args(context.TODO(), rawArgs) + if err != nil { + return 0, false + } + + return e.complexity.Mutation.TriggerQuoteUpdate(childComplexity, args["securityIDs"].([]string)), true + case "Mutation.updateSecurity": if e.complexity.Mutation.UpdateSecurity == nil { break @@ -349,6 +404,29 @@ func (ec *executionContext) field_Mutation_createSecurity_argsInput( return zeroVal, nil } +func (ec *executionContext) field_Mutation_triggerQuoteUpdate_args(ctx context.Context, rawArgs map[string]any) (map[string]any, error) { + var err error + args := map[string]any{} + arg0, err := ec.field_Mutation_triggerQuoteUpdate_argsSecurityIDs(ctx, rawArgs) + if err != nil { + return nil, err + } + args["securityIDs"] = arg0 + return args, nil +} +func (ec *executionContext) field_Mutation_triggerQuoteUpdate_argsSecurityIDs( + ctx context.Context, + rawArgs map[string]any, +) ([]string, error) { + ctx = graphql.WithPathContext(ctx, graphql.NewPathWithField("securityIDs")) + if tmp, ok := rawArgs["securityIDs"]; ok { + return ec.unmarshalOString2ᚕstringᚄ(ctx, tmp) + } + + var zeroVal []string + return zeroVal, nil +} + func (ec *executionContext) field_Mutation_updateSecurity_args(ctx context.Context, rawArgs map[string]any) (map[string]any, error) { var err error args := map[string]any{} @@ -490,6 +568,94 @@ func (ec *executionContext) field___Type_fields_argsIncludeDeprecated( // region **************************** field.gotpl ***************************** +func (ec *executionContext) _Currency_value(ctx context.Context, field graphql.CollectedField, obj *persistence.Currency) (ret graphql.Marshaler) { + fc, err := ec.fieldContext_Currency_value(ctx, field) + if err != nil { + return graphql.Null + } + ctx = graphql.WithFieldContext(ctx, fc) + defer func() { + if r := recover(); r != nil { + ec.Error(ctx, ec.Recover(ctx, r)) + ret = graphql.Null + } + }() + resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (any, error) { + ctx = rctx // use context from middleware stack in children + return obj.Value, nil + }) + if err != nil { + ec.Error(ctx, err) + return graphql.Null + } + if resTmp == nil { + if !graphql.HasFieldError(ctx, fc) { + ec.Errorf(ctx, "must not be null") + } + return graphql.Null + } + res := resTmp.(int32) + fc.Result = res + return ec.marshalNInt2int32(ctx, field.Selections, res) +} + +func (ec *executionContext) fieldContext_Currency_value(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { + fc = &graphql.FieldContext{ + Object: "Currency", + Field: field, + IsMethod: false, + IsResolver: false, + Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + return nil, errors.New("field of type Int does not have child fields") + }, + } + return fc, nil +} + +func (ec *executionContext) _Currency_currency(ctx context.Context, field graphql.CollectedField, obj *persistence.Currency) (ret graphql.Marshaler) { + fc, err := ec.fieldContext_Currency_currency(ctx, field) + if err != nil { + return graphql.Null + } + ctx = graphql.WithFieldContext(ctx, fc) + defer func() { + if r := recover(); r != nil { + ec.Error(ctx, ec.Recover(ctx, r)) + ret = graphql.Null + } + }() + resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (any, error) { + ctx = rctx // use context from middleware stack in children + return ec.resolvers.Currency().Currency(rctx, obj) + }) + if err != nil { + ec.Error(ctx, err) + return graphql.Null + } + if resTmp == nil { + if !graphql.HasFieldError(ctx, fc) { + ec.Errorf(ctx, "must not be null") + } + return graphql.Null + } + res := resTmp.(string) + fc.Result = res + return ec.marshalNString2string(ctx, field.Selections, res) +} + +func (ec *executionContext) fieldContext_Currency_currency(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { + fc = &graphql.FieldContext{ + Object: "Currency", + Field: field, + IsMethod: true, + IsResolver: true, + Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + return nil, errors.New("field of type String does not have child fields") + }, + } + return fc, nil +} + func (ec *executionContext) _ListedSecurity_ticker(ctx context.Context, field graphql.CollectedField, obj *persistence.ListedSecurity) (ret graphql.Marshaler) { fc, err := ec.fieldContext_ListedSecurity_ticker(ctx, field) if err != nil { @@ -632,6 +798,94 @@ func (ec *executionContext) fieldContext_ListedSecurity_security(_ context.Conte return fc, nil } +func (ec *executionContext) _ListedSecurity_latestQuote(ctx context.Context, field graphql.CollectedField, obj *persistence.ListedSecurity) (ret graphql.Marshaler) { + fc, err := ec.fieldContext_ListedSecurity_latestQuote(ctx, field) + if err != nil { + return graphql.Null + } + ctx = graphql.WithFieldContext(ctx, fc) + defer func() { + if r := recover(); r != nil { + ec.Error(ctx, ec.Recover(ctx, r)) + ret = graphql.Null + } + }() + resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (any, error) { + ctx = rctx // use context from middleware stack in children + return ec.resolvers.ListedSecurity().LatestQuote(rctx, obj) + }) + if err != nil { + ec.Error(ctx, err) + return graphql.Null + } + if resTmp == nil { + return graphql.Null + } + res := resTmp.(*persistence.Currency) + fc.Result = res + return ec.marshalOCurrency2ᚖgithubᚗcomᚋoxistoᚋmoneyᚑgopherᚋpersistenceᚐCurrency(ctx, field.Selections, res) +} + +func (ec *executionContext) fieldContext_ListedSecurity_latestQuote(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { + fc = &graphql.FieldContext{ + Object: "ListedSecurity", + Field: field, + IsMethod: true, + IsResolver: true, + Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + switch field.Name { + case "value": + return ec.fieldContext_Currency_value(ctx, field) + case "currency": + return ec.fieldContext_Currency_currency(ctx, field) + } + return nil, fmt.Errorf("no field named %q was found under type Currency", field.Name) + }, + } + return fc, nil +} + +func (ec *executionContext) _ListedSecurity_latestQuoteTimestamp(ctx context.Context, field graphql.CollectedField, obj *persistence.ListedSecurity) (ret graphql.Marshaler) { + fc, err := ec.fieldContext_ListedSecurity_latestQuoteTimestamp(ctx, field) + if err != nil { + return graphql.Null + } + ctx = graphql.WithFieldContext(ctx, fc) + defer func() { + if r := recover(); r != nil { + ec.Error(ctx, ec.Recover(ctx, r)) + ret = graphql.Null + } + }() + resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (any, error) { + ctx = rctx // use context from middleware stack in children + return ec.resolvers.ListedSecurity().LatestQuoteTimestamp(rctx, obj) + }) + if err != nil { + ec.Error(ctx, err) + return graphql.Null + } + if resTmp == nil { + return graphql.Null + } + res := resTmp.(*string) + fc.Result = res + return ec.marshalODate2ᚖstring(ctx, field.Selections, res) +} + +func (ec *executionContext) fieldContext_ListedSecurity_latestQuoteTimestamp(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { + fc = &graphql.FieldContext{ + Object: "ListedSecurity", + Field: field, + IsMethod: true, + IsResolver: true, + Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + return nil, errors.New("field of type Date does not have child fields") + }, + } + return fc, nil +} + func (ec *executionContext) _Mutation_createSecurity(ctx context.Context, field graphql.CollectedField) (ret graphql.Marshaler) { fc, err := ec.fieldContext_Mutation_createSecurity(ctx, field) if err != nil { @@ -762,6 +1016,61 @@ func (ec *executionContext) fieldContext_Mutation_updateSecurity(ctx context.Con return fc, nil } +func (ec *executionContext) _Mutation_triggerQuoteUpdate(ctx context.Context, field graphql.CollectedField) (ret graphql.Marshaler) { + fc, err := ec.fieldContext_Mutation_triggerQuoteUpdate(ctx, field) + if err != nil { + return graphql.Null + } + ctx = graphql.WithFieldContext(ctx, fc) + defer func() { + if r := recover(); r != nil { + ec.Error(ctx, ec.Recover(ctx, r)) + ret = graphql.Null + } + }() + resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (any, error) { + ctx = rctx // use context from middleware stack in children + return ec.resolvers.Mutation().TriggerQuoteUpdate(rctx, fc.Args["securityIDs"].([]string)) + }) + if err != nil { + ec.Error(ctx, err) + return graphql.Null + } + if resTmp == nil { + if !graphql.HasFieldError(ctx, fc) { + ec.Errorf(ctx, "must not be null") + } + return graphql.Null + } + res := resTmp.(bool) + fc.Result = res + return ec.marshalNBoolean2bool(ctx, field.Selections, res) +} + +func (ec *executionContext) fieldContext_Mutation_triggerQuoteUpdate(ctx context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { + fc = &graphql.FieldContext{ + Object: "Mutation", + Field: field, + IsMethod: true, + IsResolver: true, + Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + return nil, errors.New("field of type Boolean does not have child fields") + }, + } + defer func() { + if r := recover(); r != nil { + err = ec.Recover(ctx, r) + ec.Error(ctx, err) + } + }() + ctx = graphql.WithFieldContext(ctx, fc) + if fc.Args, err = ec.field_Mutation_triggerQuoteUpdate_args(ctx, field.ArgumentMap(ec.Variables)); err != nil { + ec.Error(ctx, err) + return fc, err + } + return fc, nil +} + func (ec *executionContext) _Query_security(ctx context.Context, field graphql.CollectedField) (ret graphql.Marshaler) { fc, err := ec.fieldContext_Query_security(ctx, field) if err != nil { @@ -1178,6 +1487,10 @@ func (ec *executionContext) fieldContext_Security_listedAs(_ context.Context, fi return ec.fieldContext_ListedSecurity_currency(ctx, field) case "security": return ec.fieldContext_ListedSecurity_security(ctx, field) + case "latestQuote": + return ec.fieldContext_ListedSecurity_latestQuote(ctx, field) + case "latestQuoteTimestamp": + return ec.fieldContext_ListedSecurity_latestQuoteTimestamp(ctx, field) } return nil, fmt.Errorf("no field named %q was found under type ListedSecurity", field.Name) }, @@ -3041,6 +3354,81 @@ func (ec *executionContext) unmarshalInputSecurityInput(ctx context.Context, obj // region **************************** object.gotpl **************************** +var currencyImplementors = []string{"Currency"} + +func (ec *executionContext) _Currency(ctx context.Context, sel ast.SelectionSet, obj *persistence.Currency) graphql.Marshaler { + fields := graphql.CollectFields(ec.OperationContext, sel, currencyImplementors) + + out := graphql.NewFieldSet(fields) + deferred := make(map[string]*graphql.FieldSet) + for i, field := range fields { + switch field.Name { + case "__typename": + out.Values[i] = graphql.MarshalString("Currency") + case "value": + out.Values[i] = ec._Currency_value(ctx, field, obj) + if out.Values[i] == graphql.Null { + atomic.AddUint32(&out.Invalids, 1) + } + case "currency": + field := field + + innerFunc := func(ctx context.Context, fs *graphql.FieldSet) (res graphql.Marshaler) { + defer func() { + if r := recover(); r != nil { + ec.Error(ctx, ec.Recover(ctx, r)) + } + }() + res = ec._Currency_currency(ctx, field, obj) + if res == graphql.Null { + atomic.AddUint32(&fs.Invalids, 1) + } + return res + } + + if field.Deferrable != nil { + dfs, ok := deferred[field.Deferrable.Label] + di := 0 + if ok { + dfs.AddField(field) + di = len(dfs.Values) - 1 + } else { + dfs = graphql.NewFieldSet([]graphql.CollectedField{field}) + deferred[field.Deferrable.Label] = dfs + } + dfs.Concurrently(di, func(ctx context.Context) graphql.Marshaler { + return innerFunc(ctx, dfs) + }) + + // don't run the out.Concurrently() call below + out.Values[i] = graphql.Null + continue + } + + out.Concurrently(i, func(ctx context.Context) graphql.Marshaler { return innerFunc(ctx, out) }) + default: + panic("unknown field " + strconv.Quote(field.Name)) + } + } + out.Dispatch(ctx) + if out.Invalids > 0 { + return graphql.Null + } + + atomic.AddInt32(&ec.deferred, int32(len(deferred))) + + for label, dfs := range deferred { + ec.processDeferredGroup(graphql.DeferredGroup{ + Label: label, + Path: graphql.GetPath(ctx), + FieldSet: dfs, + Context: ctx, + }) + } + + return out +} + var listedSecurityImplementors = []string{"ListedSecurity"} func (ec *executionContext) _ListedSecurity(ctx context.Context, sel ast.SelectionSet, obj *persistence.ListedSecurity) graphql.Marshaler { @@ -3097,6 +3485,72 @@ func (ec *executionContext) _ListedSecurity(ctx context.Context, sel ast.Selecti continue } + out.Concurrently(i, func(ctx context.Context) graphql.Marshaler { return innerFunc(ctx, out) }) + case "latestQuote": + field := field + + innerFunc := func(ctx context.Context, _ *graphql.FieldSet) (res graphql.Marshaler) { + defer func() { + if r := recover(); r != nil { + ec.Error(ctx, ec.Recover(ctx, r)) + } + }() + res = ec._ListedSecurity_latestQuote(ctx, field, obj) + return res + } + + if field.Deferrable != nil { + dfs, ok := deferred[field.Deferrable.Label] + di := 0 + if ok { + dfs.AddField(field) + di = len(dfs.Values) - 1 + } else { + dfs = graphql.NewFieldSet([]graphql.CollectedField{field}) + deferred[field.Deferrable.Label] = dfs + } + dfs.Concurrently(di, func(ctx context.Context) graphql.Marshaler { + return innerFunc(ctx, dfs) + }) + + // don't run the out.Concurrently() call below + out.Values[i] = graphql.Null + continue + } + + out.Concurrently(i, func(ctx context.Context) graphql.Marshaler { return innerFunc(ctx, out) }) + case "latestQuoteTimestamp": + field := field + + innerFunc := func(ctx context.Context, _ *graphql.FieldSet) (res graphql.Marshaler) { + defer func() { + if r := recover(); r != nil { + ec.Error(ctx, ec.Recover(ctx, r)) + } + }() + res = ec._ListedSecurity_latestQuoteTimestamp(ctx, field, obj) + return res + } + + if field.Deferrable != nil { + dfs, ok := deferred[field.Deferrable.Label] + di := 0 + if ok { + dfs.AddField(field) + di = len(dfs.Values) - 1 + } else { + dfs = graphql.NewFieldSet([]graphql.CollectedField{field}) + deferred[field.Deferrable.Label] = dfs + } + dfs.Concurrently(di, func(ctx context.Context) graphql.Marshaler { + return innerFunc(ctx, dfs) + }) + + // don't run the out.Concurrently() call below + out.Values[i] = graphql.Null + continue + } + out.Concurrently(i, func(ctx context.Context) graphql.Marshaler { return innerFunc(ctx, out) }) default: panic("unknown field " + strconv.Quote(field.Name)) @@ -3154,6 +3608,13 @@ func (ec *executionContext) _Mutation(ctx context.Context, sel ast.SelectionSet) if out.Values[i] == graphql.Null { out.Invalids++ } + case "triggerQuoteUpdate": + out.Values[i] = ec.OperationContext.RootResolverMiddleware(innerCtx, func(ctx context.Context) (res graphql.Marshaler) { + return ec._Mutation_triggerQuoteUpdate(ctx, field) + }) + if out.Values[i] == graphql.Null { + out.Invalids++ + } default: panic("unknown field " + strconv.Quote(field.Name)) } @@ -3734,6 +4195,21 @@ func (ec *executionContext) marshalNID2string(ctx context.Context, sel ast.Selec return res } +func (ec *executionContext) unmarshalNInt2int32(ctx context.Context, v any) (int32, error) { + res, err := graphql.UnmarshalInt32(v) + return res, graphql.ErrorOnPath(ctx, err) +} + +func (ec *executionContext) marshalNInt2int32(ctx context.Context, sel ast.SelectionSet, v int32) graphql.Marshaler { + res := graphql.MarshalInt32(v) + if res == graphql.Null { + if !graphql.HasFieldError(ctx, graphql.GetFieldContext(ctx)) { + ec.Errorf(ctx, "the requested element is null which the schema does not allow") + } + } + return res +} + func (ec *executionContext) marshalNListedSecurity2ᚖgithubᚗcomᚋoxistoᚋmoneyᚑgopherᚋpersistenceᚐListedSecurity(ctx context.Context, sel ast.SelectionSet, v *persistence.ListedSecurity) graphql.Marshaler { if v == nil { if !graphql.HasFieldError(ctx, graphql.GetFieldContext(ctx)) { @@ -4106,6 +4582,29 @@ func (ec *executionContext) marshalOBoolean2ᚖbool(ctx context.Context, sel ast return res } +func (ec *executionContext) marshalOCurrency2ᚖgithubᚗcomᚋoxistoᚋmoneyᚑgopherᚋpersistenceᚐCurrency(ctx context.Context, sel ast.SelectionSet, v *persistence.Currency) graphql.Marshaler { + if v == nil { + return graphql.Null + } + return ec._Currency(ctx, sel, v) +} + +func (ec *executionContext) unmarshalODate2ᚖstring(ctx context.Context, v any) (*string, error) { + if v == nil { + return nil, nil + } + res, err := graphql.UnmarshalString(v) + return &res, graphql.ErrorOnPath(ctx, err) +} + +func (ec *executionContext) marshalODate2ᚖstring(ctx context.Context, sel ast.SelectionSet, v *string) graphql.Marshaler { + if v == nil { + return graphql.Null + } + res := graphql.MarshalString(*v) + return res +} + func (ec *executionContext) marshalOListedSecurity2ᚕᚖgithubᚗcomᚋoxistoᚋmoneyᚑgopherᚋpersistenceᚐListedSecurityᚄ(ctx context.Context, sel ast.SelectionSet, v []*persistence.ListedSecurity) graphql.Marshaler { if v == nil { return graphql.Null @@ -4180,6 +4679,44 @@ func (ec *executionContext) marshalOSecurity2ᚖgithubᚗcomᚋoxistoᚋmoneyᚑ return ec._Security(ctx, sel, v) } +func (ec *executionContext) unmarshalOString2ᚕstringᚄ(ctx context.Context, v any) ([]string, error) { + if v == nil { + return nil, nil + } + var vSlice []any + if v != nil { + vSlice = graphql.CoerceList(v) + } + var err error + res := make([]string, len(vSlice)) + for i := range vSlice { + ctx := graphql.WithPathContext(ctx, graphql.NewPathWithIndex(i)) + res[i], err = ec.unmarshalNString2string(ctx, vSlice[i]) + if err != nil { + return nil, err + } + } + return res, nil +} + +func (ec *executionContext) marshalOString2ᚕstringᚄ(ctx context.Context, sel ast.SelectionSet, v []string) graphql.Marshaler { + if v == nil { + return graphql.Null + } + ret := make(graphql.Array, len(v)) + for i := range v { + ret[i] = ec.marshalNString2string(ctx, sel, v[i]) + } + + for _, e := range ret { + if e == graphql.Null { + return graphql.Null + } + } + + return ret +} + func (ec *executionContext) unmarshalOString2ᚖstring(ctx context.Context, v any) (*string, error) { if v == nil { return nil, nil diff --git a/graph/resolver.go b/graph/resolver.go index 34db2539..c5c5b9f4 100644 --- a/graph/resolver.go +++ b/graph/resolver.go @@ -2,6 +2,7 @@ package graph import ( "github.com/oxisto/money-gopher/persistence" + "github.com/oxisto/money-gopher/service/securities" ) // This file will not be regenerated automatically. @@ -9,7 +10,8 @@ import ( // It serves as dependency injection for your app, add any dependencies you require here. type Resolver struct { - DB *persistence.DB + DB *persistence.DB + QuoteUpdater securities.QuoteUpdater } func withTx[T any](r *Resolver, f func(qtx *persistence.Queries) (*T, error)) (res *T, err error) { diff --git a/graph/schema.graphqls b/graph/schema.graphqls index d36fea52..124f4db2 100644 --- a/graph/schema.graphqls +++ b/graph/schema.graphqls @@ -1,3 +1,10 @@ +scalar Date + +type Currency { + value: Int! + currency: String! +} + type Security { id: String! displayName: String! @@ -9,6 +16,8 @@ type ListedSecurity { ticker: String! currency: String! security: Security! + latestQuote: Currency + latestQuoteTimestamp: Date } input SecurityInput { @@ -25,6 +34,11 @@ input ListedSecurityInput { type Mutation { createSecurity(input: SecurityInput!): Security! updateSecurity(id: ID!, input: SecurityInput!): Security! + """ + Triggers a quote update for the given security IDs. If no security IDs are + provided, all securities will be updated. + """ + triggerQuoteUpdate(securityIDs: [String!]): Boolean! } type Query { diff --git a/graph/schema.resolvers.go b/graph/schema.resolvers.go index f6080aeb..3c199aa4 100644 --- a/graph/schema.resolvers.go +++ b/graph/schema.resolvers.go @@ -6,16 +6,47 @@ package graph import ( "context" + "fmt" "slices" + "time" "github.com/oxisto/money-gopher/persistence" ) +// Currency is the resolver for the currency field. +func (r *currencyResolver) Currency(ctx context.Context, obj *persistence.Currency) (string, error) { + panic(fmt.Errorf("not implemented: Currency - currency")) +} + // Security is the resolver for the security field. func (r *listedSecurityResolver) Security(ctx context.Context, obj *persistence.ListedSecurity) (*persistence.Security, error) { return r.DB.GetSecurity(ctx, obj.SecurityID) } +// LatestQuote is the resolver for the latestQuote field. +func (r *listedSecurityResolver) LatestQuote(ctx context.Context, obj *persistence.ListedSecurity) (*persistence.Currency, error) { + if obj.LatestQuote.Valid { + return &persistence.Currency{ + Value: int32(obj.LatestQuote.Int64), + Symbol: obj.Currency, + }, nil + } else { + return nil, nil + } +} + +// LatestQuoteTimestamp is the resolver for the latestQuoteTimestamp field. +func (r *listedSecurityResolver) LatestQuoteTimestamp(ctx context.Context, obj *persistence.ListedSecurity) (*string, error) { + var s string + + if obj.LatestQuoteTimestamp.Valid { + s = obj.LatestQuoteTimestamp.Time.Format(time.RFC3339) + return &s, nil + } else { + return nil, nil + } +} + // CreateSecurity is the resolver for the createSecurity field. func (r *mutationResolver) CreateSecurity(ctx context.Context, input SecurityInput) (*persistence.Security, error) { return withTx(r.Resolver, func(qtx *persistence.Queries) (*persistence.Security, error) { @@ -91,6 +122,16 @@ func (r *mutationResolver) UpdateSecurity(ctx context.Context, id string, input }) } +// TriggerQuoteUpdate is the resolver for the triggerQuoteUpdate field. +func (r *mutationResolver) TriggerQuoteUpdate(ctx context.Context, securityIDs []string) (b bool, err error) { + err = r.QuoteUpdater.UpdateQuotes(ctx, securityIDs) + if err != nil { + return false, err + } + + return true, nil +} + // Security is the resolver for the security field. func (r *queryResolver) Security(ctx context.Context, id string) (*persistence.Security, error) { return r.DB.GetSecurity(ctx, id) @@ -112,9 +153,12 @@ func (r *securityResolver) QuoteProvider(ctx context.Context, obj *persistence.S // ListedAs is the resolver for the listedAs field. func (r *securityResolver) ListedAs(ctx context.Context, obj *persistence.Security) ([]*persistence.ListedSecurity, error) { - return r.DB.ListListedSecuritiesBySecurityID(ctx, obj.ID) + return obj.ListedAs(ctx, r.DB) } +// Currency returns CurrencyResolver implementation. +func (r *Resolver) Currency() CurrencyResolver { return ¤cyResolver{r} } + // ListedSecurity returns ListedSecurityResolver implementation. func (r *Resolver) ListedSecurity() ListedSecurityResolver { return &listedSecurityResolver{r} } @@ -127,6 +171,7 @@ func (r *Resolver) Query() QueryResolver { return &queryResolver{r} } // Security returns SecurityResolver implementation. func (r *Resolver) Security() SecurityResolver { return &securityResolver{r} } +type currencyResolver struct{ *Resolver } type listedSecurityResolver struct{ *Resolver } type mutationResolver struct{ *Resolver } type queryResolver struct{ *Resolver } diff --git a/internal/testing/servertest/servertest.go b/internal/testing/servertest/servertest.go index bde60948..12bba2b2 100644 --- a/internal/testing/servertest/servertest.go +++ b/internal/testing/servertest/servertest.go @@ -17,7 +17,7 @@ import ( func NewServer(db *persistence.DB) *httptest.Server { mux := http.NewServeMux() srv := httptest.NewServer(h2c.NewHandler(mux, &http2.Server{})) - server.ConfigureGraphQL(mux, db) + svc := securities.NewService(db) mux.Handle(portfoliov1connect.NewPortfolioServiceHandler(portfolio.NewService( portfolio.Options{ @@ -25,7 +25,9 @@ func NewServer(db *persistence.DB) *httptest.Server { SecuritiesClient: portfoliov1connect.NewSecuritiesServiceClient(srv.Client(), srv.URL), }, ))) - mux.Handle(portfoliov1connect.NewSecuritiesServiceHandler(securities.NewService(db))) + mux.Handle(portfoliov1connect.NewSecuritiesServiceHandler(svc)) + + server.ConfigureGraphQL(mux, db, svc.(securities.QuoteUpdater)) return srv } diff --git a/persistence/currency.go b/persistence/currency.go new file mode 100644 index 00000000..1a616d44 --- /dev/null +++ b/persistence/currency.go @@ -0,0 +1,20 @@ +package persistence + +// Currency represents a currency with a value and a symbol. +type Currency struct { + // Value is the value of the currency. + Value int32 `json:"value"` + + // Symbol is the symbol of the currency. + Symbol string `json:"symbol"` +} + +func Zero() *Currency { + // TODO(oxisto): Somehow make it possible to change default currency + return &Currency{Symbol: "EUR"} +} + +func Value(v int32) *Currency { + // TODO(oxisto): Somehow make it possible to change default currency + return &Currency{Symbol: "EUR", Value: v} +} diff --git a/persistence/relationships.go b/persistence/relationships.go new file mode 100644 index 00000000..6869f92d --- /dev/null +++ b/persistence/relationships.go @@ -0,0 +1,8 @@ +package persistence + +import "context" + +// ListedAs returns the listed securities for the security. +func (s *Security) ListedAs(ctx context.Context, db *DB) ([]*ListedSecurity, error) { + return db.ListListedSecuritiesBySecurityID(ctx, s.ID) +} diff --git a/persistence/securities.sql.go b/persistence/securities.sql.go index 7718c7a2..b536933e 100644 --- a/persistence/securities.sql.go +++ b/persistence/securities.sql.go @@ -166,23 +166,39 @@ func (q *Queries) UpdateSecurity(ctx context.Context, arg UpdateSecurityParams) const upsertListedSecurity = `-- name: UpsertListedSecurity :one INSERT INTO - listed_securities (security_id, ticker, currency) + listed_securities ( + security_id, + ticker, + currency, + latest_quote, + latest_quote_timestamp + ) VALUES - (?, ?, ?) ON CONFLICT (security_id, ticker) DO + (?, ?, ?, ?, ?) ON CONFLICT (security_id, ticker) DO UPDATE SET ticker = excluded.ticker, - currency = excluded.currency RETURNING security_id, ticker, currency, latest_quote, latest_quote_timestamp + currency = excluded.currency, + latest_quote = excluded.latest_quote, + latest_quote_timestamp = excluded.latest_quote_timestamp RETURNING security_id, ticker, currency, latest_quote, latest_quote_timestamp ` type UpsertListedSecurityParams struct { - SecurityID string - Ticker string - Currency string + SecurityID string + Ticker string + Currency string + LatestQuote sql.NullInt64 + LatestQuoteTimestamp sql.NullTime } func (q *Queries) UpsertListedSecurity(ctx context.Context, arg UpsertListedSecurityParams) (*ListedSecurity, error) { - row := q.db.QueryRowContext(ctx, upsertListedSecurity, arg.SecurityID, arg.Ticker, arg.Currency) + row := q.db.QueryRowContext(ctx, upsertListedSecurity, + arg.SecurityID, + arg.Ticker, + arg.Currency, + arg.LatestQuote, + arg.LatestQuoteTimestamp, + ) var i ListedSecurity err := row.Scan( &i.SecurityID, diff --git a/persistence/sql/migrations/0001_create_securities.sql b/persistence/sql/migrations/0001_create_securities.sql index 10ab88a3..4599739f 100644 --- a/persistence/sql/migrations/0001_create_securities.sql +++ b/persistence/sql/migrations/0001_create_securities.sql @@ -21,4 +21,5 @@ CREATE TABLE -- +goose Down DROP TABLE securities; -DROP TABLE listed_securities; + +DROP TABLE listed_securities; \ No newline at end of file diff --git a/persistence/sql/queries/securities.sql b/persistence/sql/queries/securities.sql index 9deec40d..ecf979b4 100644 --- a/persistence/sql/queries/securities.sql +++ b/persistence/sql/queries/securities.sql @@ -36,13 +36,21 @@ WHERE -- name: UpsertListedSecurity :one INSERT INTO - listed_securities (security_id, ticker, currency) + listed_securities ( + security_id, + ticker, + currency, + latest_quote, + latest_quote_timestamp + ) VALUES - (?, ?, ?) ON CONFLICT (security_id, ticker) DO + (?, ?, ?, ?, ?) ON CONFLICT (security_id, ticker) DO UPDATE SET ticker = excluded.ticker, - currency = excluded.currency RETURNING *; + currency = excluded.currency, + latest_quote = excluded.latest_quote, + latest_quote_timestamp = excluded.latest_quote_timestamp RETURNING *; -- name: ListListedSecuritiesBySecurityID :many SELECT diff --git a/server/server.go b/server/server.go index 5428eca5..8485bcef 100644 --- a/server/server.go +++ b/server/server.go @@ -85,8 +85,9 @@ func StartServer(pdb *persistence.DB, opts Options) (err error) { SecuritiesClient: portfoliov1connect.NewSecuritiesServiceClient(http.DefaultClient, portfolio.DefaultSecuritiesServiceURL), }, ), interceptors)) + svc := securities.NewService(pdb) securitiesService := vanguard.NewService( - portfoliov1connect.NewSecuritiesServiceHandler(securities.NewService(pdb), interceptors), + portfoliov1connect.NewSecuritiesServiceHandler(svc, interceptors), ) transcoder, err = vanguard.NewTranscoder([]*vanguard.Service{ @@ -104,7 +105,7 @@ func StartServer(pdb *persistence.DB, opts Options) (err error) { mux := http.NewServeMux() mux.Handle("/", transcoder) - ConfigureGraphQL(mux, pdb) + ConfigureGraphQL(mux, pdb, svc.(securities.QuoteUpdater)) err = http.ListenAndServe( ":8080", @@ -116,9 +117,14 @@ func StartServer(pdb *persistence.DB, opts Options) (err error) { } // ConfigureGraphQL configures the GraphQL server for a [http.ServeMux]. -func ConfigureGraphQL(mux *http.ServeMux, db *persistence.DB) (err error) { +func ConfigureGraphQL( + mux *http.ServeMux, + db *persistence.DB, + qu securities.QuoteUpdater, +) (err error) { srv := handler.New(graph.NewExecutableSchema(graph.Config{Resolvers: &graph.Resolver{ - DB: db, + DB: db, + QuoteUpdater: qu, }})) srv.AddTransport(transport.Options{}) diff --git a/service/securities/quote.go b/service/securities/quote.go index 13b5107f..ef63b45b 100644 --- a/service/securities/quote.go +++ b/service/securities/quote.go @@ -18,51 +18,53 @@ package securities import ( "context" + "database/sql" "log/slog" "time" portfoliov1 "github.com/oxisto/money-gopher/gen" + "github.com/oxisto/money-gopher/persistence" "connectrpc.com/connect" "github.com/lmittmann/tint" - "google.golang.org/protobuf/types/known/timestamppb" ) -func (svc *service) TriggerSecurityQuoteUpdate(ctx context.Context, req *connect.Request[portfoliov1.TriggerQuoteUpdateRequest]) (res *connect.Response[portfoliov1.TriggerQuoteUpdateResponse], err error) { +// UpdateQuotes triggers an update of the quotes for the given securities. +func (svc *service) UpdateQuotes(ctx context.Context, IDs []string) (err error) { var ( - sec *portfoliov1.Security - qp QuoteProvider - ok bool + sec *persistence.Security + listed []*persistence.ListedSecurity + qp QuoteProvider + ok bool ) - // TODO(oxisto): Support a "list" with filtered values instead - for _, name := range req.Msg.SecurityIds { + for _, id := range IDs { // Fetch security - sec, err = svc.fetchSecurity(name) + sec, err = svc.db.GetSecurity(ctx, id) if err != nil { - return nil, connect.NewError(connect.CodeInternal, err) + return err } - res = connect.NewResponse(&portfoliov1.TriggerQuoteUpdateResponse{}) - - if sec.QuoteProvider == nil { - slog.Warn("No quote provider configured for security", "security", sec.Id) + if !sec.QuoteProvider.Valid { + slog.Warn("No quote provider configured for security", "security", sec.ID) return } - qp, ok = providers[*sec.QuoteProvider] + qp, ok = providers[sec.QuoteProvider.String] if !ok { return } + listed, err = sec.ListedAs(ctx, svc.db) + if err != nil { + return err + } + // Trigger update from quote provider in separate go-routine // TODO(oxisto): Use sync/errgroup instead - for idx := range sec.ListedOn { - idx := idx + for _, ls := range listed { go func() { - ls := sec.ListedOn[idx] - - slog.Debug("Triggering quote update", "security", ls, "provider", *sec.QuoteProvider) + slog.Debug("Triggering quote update", "security", ls, "provider", sec.QuoteProvider) err = svc.updateQuote(qp, ls) if err != nil { @@ -75,9 +77,20 @@ func (svc *service) TriggerSecurityQuoteUpdate(ctx context.Context, req *connect return } -func (svc *service) updateQuote(qp QuoteProvider, ls *portfoliov1.ListedSecurity) (err error) { +func (svc *service) TriggerSecurityQuoteUpdate(ctx context.Context, req *connect.Request[portfoliov1.TriggerQuoteUpdateRequest]) (res *connect.Response[portfoliov1.TriggerQuoteUpdateResponse], err error) { + err = svc.UpdateQuotes(ctx, req.Msg.SecurityIds) + if err != nil { + return nil, connect.NewError(connect.CodeInternal, err) + } + + res = connect.NewResponse(&portfoliov1.TriggerQuoteUpdateResponse{}) + + return +} + +func (svc *service) updateQuote(qp QuoteProvider, ls *persistence.ListedSecurity) (err error) { var ( - quote *portfoliov1.Currency + quote *persistence.Currency t time.Time ctx context.Context cancel context.CancelFunc @@ -91,13 +104,14 @@ func (svc *service) updateQuote(qp QuoteProvider, ls *portfoliov1.ListedSecurity return err } - ls.LatestQuote = quote - ls.LatestQuoteTimestamp = timestamppb.New(t) + ls.LatestQuote = sql.NullInt64{Int64: int64(quote.Value), Valid: true} + ls.LatestQuoteTimestamp = sql.NullTime{Time: t, Valid: true} - _, err = svc.listedSecurities.Update( - []any{ls.SecurityId, ls.Ticker}, - ls, []string{"latest_quote", "latest_quote_timestamp"}, - ) + _, err = svc.db.UpsertListedSecurity(ctx, persistence.UpsertListedSecurityParams{ + SecurityID: ls.SecurityID, + Ticker: ls.Ticker, + Currency: ls.Currency, + }) if err != nil { return err } diff --git a/service/securities/quote_provider.go b/service/securities/quote_provider.go index 0cc3d9a1..698e7f37 100644 --- a/service/securities/quote_provider.go +++ b/service/securities/quote_provider.go @@ -20,7 +20,7 @@ import ( "context" "time" - portfoliov1 "github.com/oxisto/money-gopher/gen" + "github.com/oxisto/money-gopher/persistence" ) // providers contains a map of all quote providers @@ -39,5 +39,5 @@ func RegisterQuoteProvider(name string, qp QuoteProvider) { // QuoteProvider is an interface that retrieves quotes for a [ListedSecurity]. They // can either be historical quotes or the latest quote. type QuoteProvider interface { - LatestQuote(ctx context.Context, ls *portfoliov1.ListedSecurity) (quote *portfoliov1.Currency, t time.Time, err error) + LatestQuote(ctx context.Context, ls *persistence.ListedSecurity) (quote *persistence.Currency, t time.Time, err error) } diff --git a/service/securities/quote_provider_ing.go b/service/securities/quote_provider_ing.go index a5dbefd8..cc7a579d 100644 --- a/service/securities/quote_provider_ing.go +++ b/service/securities/quote_provider_ing.go @@ -23,7 +23,7 @@ import ( "net/http" "time" - portfoliov1 "github.com/oxisto/money-gopher/gen" + "github.com/oxisto/money-gopher/persistence" ) const QuoteProviderING = "ing" @@ -45,13 +45,13 @@ type header struct { WKN string `json:"wkn"` } -func (ing *ing) LatestQuote(ctx context.Context, ls *portfoliov1.ListedSecurity) (quote *portfoliov1.Currency, t time.Time, err error) { +func (ing *ing) LatestQuote(ctx context.Context, ls *persistence.ListedSecurity) (quote *persistence.Currency, t time.Time, err error) { var ( res *http.Response h header ) - res, err = ing.Get(fmt.Sprintf("https://component-api.wertpapiere.ing.de/api/v1/components/instrumentheader/%s", ls.SecurityId)) + res, err = ing.Get(fmt.Sprintf("https://component-api.wertpapiere.ing.de/api/v1/components/instrumentheader/%s", ls.SecurityID)) if err != nil { return nil, t, fmt.Errorf("could not fetch quote: %w", err) } @@ -62,8 +62,8 @@ func (ing *ing) LatestQuote(ctx context.Context, ls *portfoliov1.ListedSecurity) } if h.HasBidAsk { - return portfoliov1.Value(int32(h.Bid * 100)), h.BidDate, nil + return persistence.Value(int32(h.Bid * 100)), h.BidDate, nil } else { - return portfoliov1.Value(int32(h.Price * 100)), h.PriceChangedDate, nil + return persistence.Value(int32(h.Price * 100)), h.PriceChangedDate, nil } } diff --git a/service/securities/quote_provider_ing_test.go b/service/securities/quote_provider_ing_test.go index f7cf8393..dce44953 100644 --- a/service/securities/quote_provider_ing_test.go +++ b/service/securities/quote_provider_ing_test.go @@ -25,7 +25,7 @@ import ( "testing" "time" - portfoliov1 "github.com/oxisto/money-gopher/gen" + "github.com/oxisto/money-gopher/persistence" "google.golang.org/protobuf/testing/protocmp" "github.com/oxisto/assert" @@ -37,13 +37,13 @@ func Test_ing_LatestQuote(t *testing.T) { } type args struct { ctx context.Context - ls *portfoliov1.ListedSecurity + ls *persistence.ListedSecurity } tests := []struct { name string fields fields args args - wantQuote *portfoliov1.Currency + wantQuote *persistence.Currency wantTime time.Time wantErr assert.Want[error] }{ @@ -56,8 +56,8 @@ func Test_ing_LatestQuote(t *testing.T) { }, args: args{ ctx: context.TODO(), - ls: &portfoliov1.ListedSecurity{ - SecurityId: "My Security", + ls: &persistence.ListedSecurity{ + SecurityID: "My Security", Ticker: "TICK", Currency: "USD", }, @@ -76,8 +76,8 @@ func Test_ing_LatestQuote(t *testing.T) { }), }, args: args{ - ls: &portfoliov1.ListedSecurity{ - SecurityId: "My Security", + ls: &persistence.ListedSecurity{ + SecurityID: "My Security", Ticker: "TICK", Currency: "USD", }, @@ -96,13 +96,13 @@ func Test_ing_LatestQuote(t *testing.T) { }), }, args: args{ - ls: &portfoliov1.ListedSecurity{ - SecurityId: "DE0000000001", + ls: &persistence.ListedSecurity{ + SecurityID: "DE0000000001", Ticker: "", Currency: "EUR", }, }, - wantQuote: portfoliov1.Value(10000), + wantQuote: persistence.Value(10000), wantTime: time.Date(2023, 05, 04, 20, 0, 0, 0, time.UTC), wantErr: func(t *testing.T, err error) bool { return true }, }, diff --git a/service/securities/quote_provider_yf.go b/service/securities/quote_provider_yf.go index 7f5f4dc1..412d9691 100644 --- a/service/securities/quote_provider_yf.go +++ b/service/securities/quote_provider_yf.go @@ -24,7 +24,7 @@ import ( "net/http" "time" - portfoliov1 "github.com/oxisto/money-gopher/gen" + "github.com/oxisto/money-gopher/persistence" ) var ErrEmptyResult = errors.New("empty result") @@ -46,7 +46,7 @@ type chart struct { } `json:"chart"` } -func (yf *yf) LatestQuote(ctx context.Context, ls *portfoliov1.ListedSecurity) (quote *portfoliov1.Currency, t time.Time, err error) { +func (yf *yf) LatestQuote(ctx context.Context, ls *persistence.ListedSecurity) (quote *persistence.Currency, t time.Time, err error) { var ( res *http.Response ch chart @@ -66,6 +66,6 @@ func (yf *yf) LatestQuote(ctx context.Context, ls *portfoliov1.ListedSecurity) ( return nil, t, ErrEmptyResult } - return portfoliov1.Value(int32(ch.Chart.Results[0].Meta.RegularMarketPrice * 100)), + return persistence.Value(int32(ch.Chart.Results[0].Meta.RegularMarketPrice * 100)), time.Unix(ch.Chart.Results[0].Meta.RegularMarketTime, 0), nil } diff --git a/service/securities/quote_provider_yf_test.go b/service/securities/quote_provider_yf_test.go index 63d69709..582d991f 100644 --- a/service/securities/quote_provider_yf_test.go +++ b/service/securities/quote_provider_yf_test.go @@ -25,7 +25,7 @@ import ( "testing" "time" - portfoliov1 "github.com/oxisto/money-gopher/gen" + "github.com/oxisto/money-gopher/persistence" "google.golang.org/protobuf/testing/protocmp" "github.com/oxisto/assert" @@ -51,13 +51,13 @@ func Test_yf_LatestQuote(t *testing.T) { } type args struct { ctx context.Context - ls *portfoliov1.ListedSecurity + ls *persistence.ListedSecurity } tests := []struct { name string fields fields args args - wantQuote *portfoliov1.Currency + wantQuote *persistence.Currency wantTime time.Time wantErr assert.Want[error] }{ @@ -70,8 +70,8 @@ func Test_yf_LatestQuote(t *testing.T) { }, args: args{ ctx: context.TODO(), - ls: &portfoliov1.ListedSecurity{ - SecurityId: "My Security", + ls: &persistence.ListedSecurity{ + SecurityID: "My Security", Ticker: "TICK", Currency: "USD", }, @@ -90,8 +90,8 @@ func Test_yf_LatestQuote(t *testing.T) { }), }, args: args{ - ls: &portfoliov1.ListedSecurity{ - SecurityId: "My Security", + ls: &persistence.ListedSecurity{ + SecurityID: "My Security", Ticker: "TICK", Currency: "USD", }, @@ -110,8 +110,8 @@ func Test_yf_LatestQuote(t *testing.T) { }), }, args: args{ - ls: &portfoliov1.ListedSecurity{ - SecurityId: "My Security", + ls: &persistence.ListedSecurity{ + SecurityID: "My Security", Ticker: "TICK", Currency: "USD", }, @@ -130,13 +130,13 @@ func Test_yf_LatestQuote(t *testing.T) { }), }, args: args{ - ls: &portfoliov1.ListedSecurity{ - SecurityId: "My Security", + ls: &persistence.ListedSecurity{ + SecurityID: "My Security", Ticker: "TICK", Currency: "USD", }, }, - wantQuote: portfoliov1.Value(10000), + wantQuote: persistence.Value(10000), wantTime: time.Date(2023, 05, 04, 20, 0, 0, 0, time.UTC), wantErr: func(t *testing.T, err error) bool { return true }, }, diff --git a/service/securities/quote_test.go b/service/securities/quote_test.go index d36dc530..b42a7c1e 100644 --- a/service/securities/quote_test.go +++ b/service/securities/quote_test.go @@ -36,8 +36,8 @@ const QuoteProviderMock = "mock" type mockQP struct { } -func (m *mockQP) LatestQuote(ctx context.Context, ls *portfoliov1.ListedSecurity) (quote *portfoliov1.Currency, t time.Time, err error) { - return portfoliov1.Value(100), time.Now(), nil +func (m *mockQP) LatestQuote(ctx context.Context, ls *persistence.ListedSecurity) (quote *persistence.Currency, t time.Time, err error) { + return persistence.Value(100), time.Now(), nil } func Test_service_TriggerSecurityQuoteUpdate(t *testing.T) { @@ -101,8 +101,8 @@ func Test_service_TriggerSecurityQuoteUpdate(t *testing.T) { type mockQuoteProvider struct{} -func (mockQuoteProvider) LatestQuote(_ context.Context, _ *portfoliov1.ListedSecurity) (quote *portfoliov1.Currency, t time.Time, err error) { - return portfoliov1.Value(100), time.Date(1, 0, 0, 0, 0, 0, 0, time.UTC), nil +func (mockQuoteProvider) LatestQuote(_ context.Context, _ *persistence.ListedSecurity) (quote *persistence.Currency, t time.Time, err error) { + return persistence.Value(100), time.Date(1, 0, 0, 0, 0, 0, 0, time.UTC), nil } func Test_service_updateQuote(t *testing.T) { @@ -111,13 +111,13 @@ func Test_service_updateQuote(t *testing.T) { } type args struct { qp QuoteProvider - ls *portfoliov1.ListedSecurity + ls *persistence.ListedSecurity } tests := []struct { name string fields fields args args - want assert.Want[*portfoliov1.ListedSecurity] + want assert.Want[*persistence.ListedSecurity] wantErr bool }{ { @@ -131,10 +131,10 @@ func Test_service_updateQuote(t *testing.T) { }, args: args{ qp: &mockQuoteProvider{}, - ls: &portfoliov1.ListedSecurity{SecurityId: "My Security", Ticker: "SEC", Currency: currency.EUR.String()}, + ls: &persistence.ListedSecurity{SecurityID: "My Security", Ticker: "SEC", Currency: currency.EUR.String()}, }, - want: func(t *testing.T, ls *portfoliov1.ListedSecurity) bool { - return assert.Equals(t, 100, int(ls.LatestQuote.Value)) + want: func(t *testing.T, ls *persistence.ListedSecurity) bool { + return assert.Equals(t, 100, int(ls.LatestQuote.Int64)) }, }, } diff --git a/service/securities/service.go b/service/securities/service.go index 1be7611e..09824da5 100644 --- a/service/securities/service.go +++ b/service/securities/service.go @@ -18,6 +18,7 @@ package securities import ( + "context" "time" moneygopher "github.com/oxisto/money-gopher" @@ -34,6 +35,14 @@ type service struct { listedSecurities persistence.StorageOperations[*portfoliov1.ListedSecurity] portfoliov1connect.UnimplementedSecuritiesServiceHandler + db *persistence.DB +} + +// QuoteUpdater is an interface that allows to trigger an update of the quotes +// for the given securities. +type QuoteUpdater interface { + // UpdateQuotes triggers an update of the quotes for the given securities. + UpdateQuotes(ctx context.Context, IDs []string) error } func NewService(db *persistence.DB) portfoliov1connect.SecuritiesServiceHandler { @@ -74,5 +83,6 @@ func NewService(db *persistence.DB) portfoliov1connect.SecuritiesServiceHandler return &service{ securities: securities, listedSecurities: listedSecurities, + db: db, } } From 43fcb2a966eeb498545011889d17645e03d23339 Mon Sep 17 00:00:00 2001 From: Christian Banse Date: Fri, 27 Dec 2024 23:15:22 +0100 Subject: [PATCH 05/35] Fixed some tests --- persistence/securities.sql.go | 11 +++--- persistence/sql/queries/securities.sql | 4 +-- service/securities/quote_test.go | 46 +++++++++++++++----------- 3 files changed, 35 insertions(+), 26 deletions(-) diff --git a/persistence/securities.sql.go b/persistence/securities.sql.go index b536933e..60ddd916 100644 --- a/persistence/securities.sql.go +++ b/persistence/securities.sql.go @@ -12,18 +12,19 @@ import ( const createSecurity = `-- name: CreateSecurity :one INSERT INTO - securities (id, display_name) + securities (id, display_name, quote_provider) VALUES - (?, ?) RETURNING id, display_name, quote_provider + (?, ?, ?) RETURNING id, display_name, quote_provider ` type CreateSecurityParams struct { - ID string - DisplayName string + ID string + DisplayName string + QuoteProvider sql.NullString } func (q *Queries) CreateSecurity(ctx context.Context, arg CreateSecurityParams) (*Security, error) { - row := q.db.QueryRowContext(ctx, createSecurity, arg.ID, arg.DisplayName) + row := q.db.QueryRowContext(ctx, createSecurity, arg.ID, arg.DisplayName, arg.QuoteProvider) var i Security err := row.Scan(&i.ID, &i.DisplayName, &i.QuoteProvider) return &i, err diff --git a/persistence/sql/queries/securities.sql b/persistence/sql/queries/securities.sql index ecf979b4..e7bda499 100644 --- a/persistence/sql/queries/securities.sql +++ b/persistence/sql/queries/securities.sql @@ -16,9 +16,9 @@ ORDER BY -- name: CreateSecurity :one INSERT INTO - securities (id, display_name) + securities (id, display_name, quote_provider) VALUES - (?, ?) RETURNING *; + (?, ?, ?) RETURNING *; -- name: UpdateSecurity :one UPDATE securities diff --git a/service/securities/quote_test.go b/service/securities/quote_test.go index b42a7c1e..c5e88bc4 100644 --- a/service/securities/quote_test.go +++ b/service/securities/quote_test.go @@ -18,10 +18,10 @@ package securities import ( "context" + "database/sql" "testing" "time" - moneygopher "github.com/oxisto/money-gopher" portfoliov1 "github.com/oxisto/money-gopher/gen" "github.com/oxisto/money-gopher/internal" "github.com/oxisto/money-gopher/persistence" @@ -44,7 +44,7 @@ func Test_service_TriggerSecurityQuoteUpdate(t *testing.T) { RegisterQuoteProvider(QuoteProviderMock, &mockQP{}) type fields struct { - securities persistence.StorageOperations[*portfoliov1.Security] + db *persistence.DB } type args struct { ctx context.Context @@ -60,20 +60,22 @@ func Test_service_TriggerSecurityQuoteUpdate(t *testing.T) { { name: "happy path", fields: fields{ - securities: internal.NewTestDBOps(t, func(ops persistence.StorageOperations[*portfoliov1.Security]) { - ops.Replace(&portfoliov1.Security{ - Id: "My Security", - QuoteProvider: moneygopher.Ref("mock"), + db: internal.NewTestDB(t, func(db *persistence.DB) { + _, err := db.CreateSecurity(context.Background(), persistence.CreateSecurityParams{ + ID: "My Security", + QuoteProvider: sql.NullString{String: QuoteProviderMock, Valid: true}, }) - rel := persistence.Relationship[*portfoliov1.ListedSecurity](ops) - assert.NoError(t, rel.Replace(&portfoliov1.ListedSecurity{ - SecurityId: "My Security", + assert.NoError(t, err) + _, err = db.UpsertListedSecurity(context.Background(), persistence.UpsertListedSecurityParams{ + SecurityID: "My Security", Ticker: "SEC", Currency: currency.EUR.String(), - })) + }) + assert.NoError(t, err) }), }, args: args{ + ctx: context.TODO(), req: connect.NewRequest(&portfoliov1.TriggerQuoteUpdateRequest{ SecurityIds: []string{"My Security"}, }), @@ -86,8 +88,7 @@ func Test_service_TriggerSecurityQuoteUpdate(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { svc := &service{ - securities: tt.fields.securities, - listedSecurities: persistence.Relationship[*portfoliov1.ListedSecurity](tt.fields.securities), + db: tt.fields.db, } gotRes, err := svc.TriggerSecurityQuoteUpdate(tt.args.ctx, tt.args.req) if (err != nil) != tt.wantErr { @@ -107,7 +108,7 @@ func (mockQuoteProvider) LatestQuote(_ context.Context, _ *persistence.ListedSec func Test_service_updateQuote(t *testing.T) { type fields struct { - securities persistence.StorageOperations[*portfoliov1.Security] + db *persistence.DB } type args struct { qp QuoteProvider @@ -123,10 +124,18 @@ func Test_service_updateQuote(t *testing.T) { { name: "happy path", fields: fields{ - securities: internal.NewTestDBOps(t, func(ops persistence.StorageOperations[*portfoliov1.Security]) { - ops.Replace(&portfoliov1.Security{Id: "My Security"}) - rel := persistence.Relationship[*portfoliov1.ListedSecurity](ops) - assert.NoError(t, rel.Replace(&portfoliov1.ListedSecurity{SecurityId: "My Security", Ticker: "SEC", Currency: currency.EUR.String()})) + db: internal.NewTestDB(t, func(db *persistence.DB) { + _, err := db.CreateSecurity(context.Background(), persistence.CreateSecurityParams{ + ID: "My Security", + QuoteProvider: sql.NullString{String: QuoteProviderMock, Valid: true}, + }) + assert.NoError(t, err) + _, err = db.UpsertListedSecurity(context.Background(), persistence.UpsertListedSecurityParams{ + SecurityID: "My Security", + Ticker: "SEC", + Currency: currency.EUR.String(), + }) + assert.NoError(t, err) }), }, args: args{ @@ -141,8 +150,7 @@ func Test_service_updateQuote(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { svc := &service{ - securities: tt.fields.securities, - listedSecurities: persistence.Relationship[*portfoliov1.ListedSecurity](tt.fields.securities), + db: tt.fields.db, } if err := svc.updateQuote(tt.args.qp, tt.args.ls); (err != nil) != tt.wantErr { t.Errorf("updateQuote() error = %v, wantErr %v", err, tt.wantErr) From bf36e8d29995d19ac1048740a970424467274142 Mon Sep 17 00:00:00 2001 From: Christian Banse Date: Fri, 27 Dec 2024 23:24:29 +0100 Subject: [PATCH 06/35] Fixed tests --- cli/commands/securities.go | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/cli/commands/securities.go b/cli/commands/securities.go index a398598e..bc90ca74 100644 --- a/cli/commands/securities.go +++ b/cli/commands/securities.go @@ -121,8 +121,13 @@ func UpdateQuote(ctx context.Context, cmd *cli.Command) (err error) { TriggerQuoteUpdate bool `graphql:"triggerQuoteUpdate(securityIDs: $IDs)" json:"security"` } + var ids []graphql.String + for _, id := range cmd.StringSlice("security-ids") { + ids = append(ids, graphql.String(id)) + } + err = s.GraphQL.Mutate(context.Background(), &query, map[string]interface{}{ - "IDs": []graphql.String{"1"}, + "IDs": ids, }) if err != nil { return err From e23afdd26a746d074ff4f14ce89bdcdfbc1b2742 Mon Sep 17 00:00:00 2001 From: Christian Banse Date: Fri, 27 Dec 2024 23:41:09 +0100 Subject: [PATCH 07/35] Added tools dependencies --- .github/workflows/build.yml | 8 +- go.mod | 2 +- go.sum | 4 +- tools/go.mod | 82 ++++++++++++ tools/go.sum | 242 ++++++++++++++++++++++++++++++++++++ tools/tools.go | 10 ++ 6 files changed, 344 insertions(+), 4 deletions(-) create mode 100644 tools/go.mod create mode 100644 tools/go.sum create mode 100644 tools/tools.go diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 68dc97c8..5e698080 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -16,6 +16,7 @@ jobs: uses: actions/setup-go@v5 with: go-version-file: go.mod + cache-dependency-path: "**/*.sum" - name: Set up node.js uses: actions/setup-node@v4 with: @@ -24,9 +25,14 @@ jobs: uses: bufbuild/buf-setup-action@v1.48.0 with: github_token: ${{ secrets.GITHUB_TOKEN }} + - name: Set up tools + run: | + go install github.com/mfridman/tparse + go install github.com/sqlc-dev/sqlc/cmd/sqlc + go install github.com/99designs/gqlgen + working-directory: ./tools - name: Build Backend run: | - go install github.com/sqlc-dev/sqlc/cmd/sqlc@latest go generate ./... go build -v ./... - name: Test Backend diff --git a/go.mod b/go.mod index 05f65ff5..8fdc8256 100644 --- a/go.mod +++ b/go.mod @@ -38,7 +38,7 @@ require ( github.com/sosodev/duration v1.3.1 // indirect go.uber.org/multierr v1.11.0 // indirect golang.org/x/crypto v0.31.0 // indirect - golang.org/x/oauth2 v0.20.0 // indirect + golang.org/x/oauth2 v0.22.0 // indirect golang.org/x/sync v0.10.0 // indirect golang.org/x/sys v0.28.0 // indirect golang.org/x/time v0.5.0 // indirect diff --git a/go.sum b/go.sum index 1b230d67..7ca54c31 100644 --- a/go.sum +++ b/go.sum @@ -79,8 +79,8 @@ golang.org/x/crypto v0.31.0 h1:ihbySMvVjLAeSH1IbfcRTkD/iNscyz8rGzjF/E5hV6U= golang.org/x/crypto v0.31.0/go.mod h1:kDsLvtWBEx7MV9tJOj9bnXsPbxwJQ6csT/x4KIN4Ssk= golang.org/x/net v0.33.0 h1:74SYHlV8BIgHIFC/LrYkOGIwL19eTYXQ5wc6TBuO36I= golang.org/x/net v0.33.0/go.mod h1:HXLR5J+9DxmrqMwG9qjGCxZ+zKXxBru04zlTvWlWuN4= -golang.org/x/oauth2 v0.20.0 h1:4mQdhULixXKP1rwYBW0vAijoXnkTG0BLCDRzfe1idMo= -golang.org/x/oauth2 v0.20.0/go.mod h1:XYTD2NtWslqkgxebSiOHnXEap4TF09sJSc7H1sXbhtI= +golang.org/x/oauth2 v0.22.0 h1:BzDx2FehcG7jJwgWLELCdmLuxk2i+x9UDpSiss2u0ZA= +golang.org/x/oauth2 v0.22.0/go.mod h1:XYTD2NtWslqkgxebSiOHnXEap4TF09sJSc7H1sXbhtI= golang.org/x/sync v0.10.0 h1:3NQrjDixjgGwUOCaF8w2+VYHv0Ve/vGYSbdkTa98gmQ= golang.org/x/sync v0.10.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= diff --git a/tools/go.mod b/tools/go.mod new file mode 100644 index 00000000..0dc0f6e7 --- /dev/null +++ b/tools/go.mod @@ -0,0 +1,82 @@ +module tools + +go 1.23.4 + +require ( + github.com/99designs/gqlgen v0.17.61 + github.com/mfridman/tparse v0.16.0 + github.com/sqlc-dev/sqlc v1.27.0 +) + +require ( + filippo.io/edwards25519 v1.1.0 // indirect + github.com/agnivade/levenshtein v1.2.0 // indirect + github.com/antlr4-go/antlr/v4 v4.13.1 // indirect + github.com/aymanbagabas/go-osc52/v2 v2.0.1 // indirect + github.com/charmbracelet/lipgloss v1.0.0 // indirect + github.com/charmbracelet/x/ansi v0.4.2 // indirect + github.com/cpuguy83/go-md2man/v2 v2.0.5 // indirect + github.com/cubicdaiya/gonp v1.0.4 // indirect + github.com/cznic/mathutil v0.0.0-20181122101859-297441e03548 // indirect + github.com/davecgh/go-spew v1.1.1 // indirect + github.com/dustin/go-humanize v1.0.1 // indirect + github.com/fatih/structtag v1.2.0 // indirect + github.com/go-sql-driver/mysql v1.8.1 // indirect + github.com/google/cel-go v0.21.0 // indirect + github.com/google/uuid v1.6.0 // indirect + github.com/hashicorp/golang-lru/v2 v2.0.7 // indirect + github.com/inconshreveable/mousetrap v1.1.0 // indirect + github.com/jackc/pgpassfile v1.0.0 // indirect + github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a // indirect + github.com/jackc/pgx/v5 v5.6.0 // indirect + github.com/jackc/puddle/v2 v2.2.1 // indirect + github.com/jinzhu/inflection v1.0.0 // indirect + github.com/lucasb-eyer/go-colorful v1.2.0 // indirect + github.com/mattn/go-isatty v0.0.20 // indirect + github.com/mfridman/buildversion v0.3.0 // indirect + github.com/muesli/termenv v0.15.3-0.20240618155329-98d742f6907a // indirect + github.com/ncruces/go-strftime v0.1.9 // indirect + github.com/pganalyze/pg_query_go/v5 v5.1.0 // indirect + github.com/pingcap/errors v0.11.5-0.20210425183316-da1aaba5fb63 // indirect + github.com/pingcap/failpoint v0.0.0-20220801062533-2eaa32854a6c // indirect + github.com/pingcap/log v1.1.0 // indirect + github.com/pingcap/tidb/pkg/parser v0.0.0-20231103154709-4f00ece106b1 // indirect + github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect + github.com/rivo/uniseg v0.4.7 // indirect + github.com/riza-io/grpc-go v0.2.0 // indirect + github.com/russross/blackfriday/v2 v2.1.0 // indirect + github.com/sosodev/duration v1.3.1 // indirect + github.com/spf13/cobra v1.8.1 // indirect + github.com/spf13/pflag v1.0.5 // indirect + github.com/stoewer/go-strcase v1.2.0 // indirect + github.com/tetratelabs/wazero v1.7.3 // indirect + github.com/urfave/cli/v2 v2.27.5 // indirect + github.com/vektah/gqlparser/v2 v2.5.20 // indirect + github.com/wasilibs/go-pgquery v0.0.0-20240606042535-c0843d6592cc // indirect + github.com/wasilibs/wazero-helpers v0.0.0-20240604052452-61d7981e9a38 // indirect + github.com/xrash/smetrics v0.0.0-20240521201337-686a1a2994c1 // indirect + go.uber.org/atomic v1.11.0 // indirect + go.uber.org/multierr v1.11.0 // indirect + go.uber.org/zap v1.26.0 // indirect + golang.org/x/crypto v0.31.0 // indirect + golang.org/x/exp v0.0.0-20240506185415-9bf2ced13842 // indirect + golang.org/x/mod v0.20.0 // indirect + golang.org/x/net v0.33.0 // indirect + golang.org/x/sync v0.10.0 // indirect + golang.org/x/sys v0.28.0 // indirect + golang.org/x/text v0.21.0 // indirect + golang.org/x/tools v0.24.0 // indirect + google.golang.org/genproto/googleapis/api v0.0.0-20240528184218-531527333157 // indirect + google.golang.org/genproto/googleapis/rpc v0.0.0-20240528184218-531527333157 // indirect + google.golang.org/grpc v1.65.0 // indirect + google.golang.org/protobuf v1.35.2 // indirect + gopkg.in/natefinch/lumberjack.v2 v2.2.1 // indirect + gopkg.in/yaml.v3 v3.0.1 // indirect + modernc.org/gc/v3 v3.0.0-20240107210532-573471604cb6 // indirect + modernc.org/libc v1.55.3 // indirect + modernc.org/mathutil v1.6.0 // indirect + modernc.org/memory v1.8.0 // indirect + modernc.org/sqlite v1.31.1 // indirect + modernc.org/strutil v1.2.0 // indirect + modernc.org/token v1.1.0 // indirect +) diff --git a/tools/go.sum b/tools/go.sum new file mode 100644 index 00000000..533889cf --- /dev/null +++ b/tools/go.sum @@ -0,0 +1,242 @@ +filippo.io/edwards25519 v1.1.0 h1:FNf4tywRC1HmFuKW5xopWpigGjJKiJSV0Cqo0cJWDaA= +filippo.io/edwards25519 v1.1.0/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4VDT4= +github.com/99designs/gqlgen v0.17.61 h1:vE7xLRC066n9wehgjeplILOWtwz75zbzcV2/Iv9i3pw= +github.com/99designs/gqlgen v0.17.61/go.mod h1:rFU1T3lhv/tPeAlww/DJ4ol2YxT/pPpue+xxPbkd3r4= +github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= +github.com/agnivade/levenshtein v1.2.0 h1:U9L4IOT0Y3i0TIlUIDJ7rVUziKi/zPbrJGaFrtYH3SY= +github.com/agnivade/levenshtein v1.2.0/go.mod h1:QVVI16kDrtSuwcpd0p1+xMC6Z/VfhtCyDIjcwga4/DU= +github.com/andreyvit/diff v0.0.0-20170406064948-c7f18ee00883 h1:bvNMNQO63//z+xNgfBlViaCIJKLlCJ6/fmUseuG0wVQ= +github.com/andreyvit/diff v0.0.0-20170406064948-c7f18ee00883/go.mod h1:rCTlJbsFo29Kk6CurOXKm700vrz8f0KW0JNfpkRJY/8= +github.com/antlr4-go/antlr/v4 v4.13.1 h1:SqQKkuVZ+zWkMMNkjy5FZe5mr5WURWnlpmOuzYWrPrQ= +github.com/antlr4-go/antlr/v4 v4.13.1/go.mod h1:GKmUxMtwp6ZgGwZSva4eWPC5mS6vUAmOABFgjdkM7Nw= +github.com/arbovm/levenshtein v0.0.0-20160628152529-48b4e1c0c4d0 h1:jfIu9sQUG6Ig+0+Ap1h4unLjW6YQJpKZVmUzxsD4E/Q= +github.com/arbovm/levenshtein v0.0.0-20160628152529-48b4e1c0c4d0/go.mod h1:t2tdKJDJF9BV14lnkjHmOQgcvEKgtqs5a1N3LNdJhGE= +github.com/aymanbagabas/go-osc52/v2 v2.0.1 h1:HwpRHbFMcZLEVr42D4p7XBqjyuxQH5SMiErDT4WkJ2k= +github.com/aymanbagabas/go-osc52/v2 v2.0.1/go.mod h1:uYgXzlJ7ZpABp8OJ+exZzJJhRNQ2ASbcXHWsFqH8hp8= +github.com/aymanbagabas/go-udiff v0.2.0 h1:TK0fH4MteXUDspT88n8CKzvK0X9O2xu9yQjWpi6yML8= +github.com/aymanbagabas/go-udiff v0.2.0/go.mod h1:RE4Ex0qsGkTAJoQdQQCA0uG+nAzJO/pI/QwceO5fgrA= +github.com/benbjohnson/clock v1.1.0/go.mod h1:J11/hYXuz8f4ySSvYwY0FKfm+ezbsZBKZxNJlLklBHA= +github.com/charmbracelet/lipgloss v1.0.0 h1:O7VkGDvqEdGi93X+DeqsQ7PKHDgtQfF8j8/O2qFMQNg= +github.com/charmbracelet/lipgloss v1.0.0/go.mod h1:U5fy9Z+C38obMs+T+tJqst9VGzlOYGj4ri9reL3qUlo= +github.com/charmbracelet/x/ansi v0.4.2 h1:0JM6Aj/g/KC154/gOP4vfxun0ff6itogDYk41kof+qk= +github.com/charmbracelet/x/ansi v0.4.2/go.mod h1:dk73KoMTT5AX5BsX0KrqhsTqAnhZZoCBjs7dGWp4Ktw= +github.com/charmbracelet/x/exp/golden v0.0.0-20240806155701-69247e0abc2a h1:G99klV19u0QnhiizODirwVksQB91TJKV/UaTnACcG30= +github.com/charmbracelet/x/exp/golden v0.0.0-20240806155701-69247e0abc2a/go.mod h1:wDlXFlCrmJ8J+swcL/MnGUuYnqgQdW9rhSD61oNMb6U= +github.com/cpuguy83/go-md2man/v2 v2.0.4/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= +github.com/cpuguy83/go-md2man/v2 v2.0.5 h1:ZtcqGrnekaHpVLArFSe4HK5DoKx1T0rq2DwVB0alcyc= +github.com/cpuguy83/go-md2man/v2 v2.0.5/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= +github.com/cubicdaiya/gonp v1.0.4 h1:ky2uIAJh81WiLcGKBVD5R7KsM/36W6IqqTy6Bo6rGws= +github.com/cubicdaiya/gonp v1.0.4/go.mod h1:iWGuP/7+JVTn02OWhRemVbMmG1DOUnmrGTYYACpOI0I= +github.com/cznic/mathutil v0.0.0-20181122101859-297441e03548 h1:iwZdTE0PVqJCos1vaoKsclOGD3ADKpshg3SRtYBbwso= +github.com/cznic/mathutil v0.0.0-20181122101859-297441e03548/go.mod h1:e6NPNENfs9mPDVNRekM7lKScauxd5kXTr1Mfyig6TDM= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/dgryski/trifles v0.0.0-20230903005119-f50d829f2e54 h1:SG7nF6SRlWhcT7cNTs5R6Hk4V2lcmLz2NsG2VnInyNo= +github.com/dgryski/trifles v0.0.0-20230903005119-f50d829f2e54/go.mod h1:if7Fbed8SFyPtHLHbg49SI7NAdJiC5WIA09pe59rfAA= +github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY= +github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto= +github.com/fatih/structtag v1.2.0 h1:/OdNE99OxoI/PqaW/SuSK9uxxT3f/tcSZgon/ssNSx4= +github.com/fatih/structtag v1.2.0/go.mod h1:mBJUNpUnHmRKrKlQQlmCrh5PuhftFbNv8Ys4/aAZl94= +github.com/go-sql-driver/mysql v1.8.1 h1:LedoTUt/eveggdHS9qUFC1EFSa8bU2+1pZjSRpvNJ1Y= +github.com/go-sql-driver/mysql v1.8.1/go.mod h1:wEBSXgmK//2ZFJyE+qWnIsVGmvmEKlqwuVSjsCm7DZg= +github.com/go-viper/mapstructure/v2 v2.2.1 h1:ZAaOCxANMuZx5RCeg0mBdEZk7DZasvvZIxtHqx8aGss= +github.com/go-viper/mapstructure/v2 v2.2.1/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM= +github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk= +github.com/google/cel-go v0.21.0 h1:cl6uW/gxN+Hy50tNYvI691+sXxioCnstFzLp2WO4GCI= +github.com/google/cel-go v0.21.0/go.mod h1:rHUlWCcBKgyEk+eV03RPdZUekPp6YcJwV0FxuUksYxc= +github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= +github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= +github.com/google/pprof v0.0.0-20240409012703-83162a5b38cd h1:gbpYu9NMq8jhDVbvlGkMFWCjLFlqqEZjEmObmhUy6Vo= +github.com/google/pprof v0.0.0-20240409012703-83162a5b38cd/go.mod h1:kf6iHlnVGwgKolg33glAes7Yg/8iWP8ukqeldJSO7jw= +github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= +github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/gorilla/websocket v1.5.0 h1:PPwGk2jz7EePpoHN/+ClbZu8SPxiqlu12wZP/3sWmnc= +github.com/gorilla/websocket v1.5.0/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= +github.com/hashicorp/golang-lru/v2 v2.0.7 h1:a+bsQ5rvGLjzHuww6tVxozPZFVghXaHOwFs4luLUK2k= +github.com/hashicorp/golang-lru/v2 v2.0.7/go.mod h1:QeFd9opnmA6QUJc5vARoKUSoFhyfM2/ZepoAG6RGpeM= +github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= +github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= +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-20221227161230-091c0ba34f0a h1:bbPeKD0xmW/Y25WS6cokEszi5g+S0QxI/d45PkRi7Nk= +github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a/go.mod h1:5TJZWKEWniPve33vlWYSoGYefn3gLQRzjfDlhSJ9ZKM= +github.com/jackc/pgx/v5 v5.6.0 h1:SWJzexBzPL5jb0GEsrPMLIsi/3jOo7RHlzTjcAeDrPY= +github.com/jackc/pgx/v5 v5.6.0/go.mod h1:DNZ/vlrUnhWCoFGxHAG8U2ljioxukquj7utPDgtQdTw= +github.com/jackc/puddle/v2 v2.2.1 h1:RhxXJtFG022u4ibrCSMSiu5aOq1i77R3OHKNJj77OAk= +github.com/jackc/puddle/v2 v2.2.1/go.mod h1:vriiEXHvEE654aYKXXjOvZM39qJ0q+azkZFrfEOc3H4= +github.com/jinzhu/inflection v1.0.0 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD/E= +github.com/jinzhu/inflection v1.0.0/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkryuEj+Srlc= +github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= +github.com/kr/pretty v0.3.0 h1:WgNl7dwNpEZ6jJ9k1snq4pZsg7DOEN8hP9Xw0Tsjwk0= +github.com/kr/pretty v0.3.0/go.mod h1:640gp4NfQd8pI5XOwp5fnNeVWj67G7CFk/SaSQn7NBk= +github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= +github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= +github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= +github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= +github.com/lucasb-eyer/go-colorful v1.2.0 h1:1nnpGOrhyZZuNyfu1QjKiUICQ74+3FNCN69Aj6K7nkY= +github.com/lucasb-eyer/go-colorful v1.2.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0= +github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= +github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= +github.com/mfridman/buildversion v0.3.0 h1:hehEX3IbBZJBqquXctUEOWJfIM46P0ku9naK9h1BGuY= +github.com/mfridman/buildversion v0.3.0/go.mod h1:sfXvYxwfmLvkklTJLv9xJ0Wffw57z9ZFOK4KOGJYafU= +github.com/mfridman/tparse v0.16.0 h1:loy4AVPJPMdqdS6T9Xwnpfct8yhjmGOfTipHCFk62LE= +github.com/mfridman/tparse v0.16.0/go.mod h1:yw5mav2iN2rCf3/DSQxFQy3MuyQoWAQagjwOHdgCWpo= +github.com/muesli/termenv v0.15.3-0.20240618155329-98d742f6907a h1:2MaM6YC3mGu54x+RKAA6JiFFHlHDY1UbkxqppT7wYOg= +github.com/muesli/termenv v0.15.3-0.20240618155329-98d742f6907a/go.mod h1:hxSnBBYLK21Vtq/PHd0S2FYCxBXzBua8ov5s1RobyRQ= +github.com/ncruces/go-strftime v0.1.9 h1:bY0MQC28UADQmHmaF5dgpLmImcShSi2kHU9XLdhx/f4= +github.com/ncruces/go-strftime v0.1.9/go.mod h1:Fwc5htZGVVkseilnfgOVb9mKy6w1naJmn9CehxcKcls= +github.com/pganalyze/pg_query_go/v5 v5.1.0 h1:MlxQqHZnvA3cbRQYyIrjxEjzo560P6MyTgtlaf3pmXg= +github.com/pganalyze/pg_query_go/v5 v5.1.0/go.mod h1:FsglvxidZsVN+Ltw3Ai6nTgPVcK2BPukH3jCDEqc1Ug= +github.com/pingcap/errors v0.11.0/go.mod h1:Oi8TUi2kEtXXLMJk9l1cGmz20kV3TaQ0usTwv5KuLY8= +github.com/pingcap/errors v0.11.4/go.mod h1:Oi8TUi2kEtXXLMJk9l1cGmz20kV3TaQ0usTwv5KuLY8= +github.com/pingcap/errors v0.11.5-0.20210425183316-da1aaba5fb63 h1:+FZIDR/D97YOPik4N4lPDaUcLDF/EQPogxtlHB2ZZRM= +github.com/pingcap/errors v0.11.5-0.20210425183316-da1aaba5fb63/go.mod h1:X2r9ueLEUZgtx2cIogM0v4Zj5uvvzhuuiu7Pn8HzMPg= +github.com/pingcap/failpoint v0.0.0-20220801062533-2eaa32854a6c h1:CgbKAHto5CQgWM9fSBIvaxsJHuGP0uM74HXtv3MyyGQ= +github.com/pingcap/failpoint v0.0.0-20220801062533-2eaa32854a6c/go.mod h1:4qGtCB0QK0wBzKtFEGDhxXnSnbQApw1gc9siScUl8ew= +github.com/pingcap/log v1.1.0 h1:ELiPxACz7vdo1qAvvaWJg1NrYFoY6gqAh/+Uo6aXdD8= +github.com/pingcap/log v1.1.0/go.mod h1:DWQW5jICDR7UJh4HtxXSM20Churx4CQL0fwL/SoOSA4= +github.com/pingcap/tidb/pkg/parser v0.0.0-20231103154709-4f00ece106b1 h1:SwGY3zMnK4wO85vvRIqrR3Yh6VpIC9pydG0QNOUPHCY= +github.com/pingcap/tidb/pkg/parser v0.0.0-20231103154709-4f00ece106b1/go.mod h1:yRkiqLFwIqibYg2P7h4bclHjHcJiIFRLKhGRyBcKYus= +github.com/pkg/errors v0.8.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/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec h1:W09IVJc94icq4NjY3clb7Lk8O1qJ8BdBEF8z0ibU0rE= +github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo= +github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ= +github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88= +github.com/riza-io/grpc-go v0.2.0 h1:2HxQKFVE7VuYstcJ8zqpN84VnAoJ4dCL6YFhJewNcHQ= +github.com/riza-io/grpc-go v0.2.0/go.mod h1:2bDvR9KkKC3KhtlSHfR3dAXjUMT86kg4UfWFyVGWqi8= +github.com/rogpeppe/go-internal v1.10.0 h1:TMyTOH3F/DB16zRVcYyreMH6GnZZrwQVAoYjRBZyWFQ= +github.com/rogpeppe/go-internal v1.10.0/go.mod h1:UQnix2H7Ngw/k4C5ijL5+65zddjncjaFoBhdsK/akog= +github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk= +github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= +github.com/sergi/go-diff v1.1.0/go.mod h1:STckp+ISIX8hZLjrqAeVduY0gWCT9IjLuqbuNXdaHfM= +github.com/sergi/go-diff v1.3.1 h1:xkr+Oxo4BOQKmkn/B9eMK0g5Kg/983T9DqqPHwYqD+8= +github.com/sergi/go-diff v1.3.1/go.mod h1:aMJSSKb2lpPvRNec0+w3fl7LP9IOFzdc9Pa4NFbPK1I= +github.com/sosodev/duration v1.3.1 h1:qtHBDMQ6lvMQsL15g4aopM4HEfOaYuhWBw3NPTtlqq4= +github.com/sosodev/duration v1.3.1/go.mod h1:RQIBBX0+fMLc/D9+Jb/fwvVmo0eZvDDEERAikUR6SDg= +github.com/spf13/cobra v1.8.1 h1:e5/vxKd/rZsfSJMUX1agtjeTDf+qv1/JdBF8gg5k9ZM= +github.com/spf13/cobra v1.8.1/go.mod h1:wHxEcudfqmLYa8iTfL+OuZPbBZkmvliBWKIezN3kD9Y= +github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= +github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= +github.com/sqlc-dev/sqlc v1.27.0 h1:wWc+401GLh0whLa30WmDkkl11lMBZuqvDvgu5OsaDiQ= +github.com/sqlc-dev/sqlc v1.27.0/go.mod h1:wXAlx++Ed1eUhMeEKyXfeCO+ogPIN1adG5DdPavR4k0= +github.com/stoewer/go-strcase v1.2.0 h1:Z2iHWqGXH00XYgqDmNgQbIBxf3wrNq0F3feEy0ainaU= +github.com/stoewer/go-strcase v1.2.0/go.mod h1:IBiWB2sKIp3wVVQ3Y035++gc+knqhUQag1KpM8ahLw8= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +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.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA= +github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= +github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= +github.com/tetratelabs/wazero v1.7.3 h1:PBH5KVahrt3S2AHgEjKu4u+LlDbbk+nsGE3KLucy6Rw= +github.com/tetratelabs/wazero v1.7.3/go.mod h1:ytl6Zuh20R/eROuyDaGPkp82O9C/DJfXAwJfQ3X6/7Y= +github.com/urfave/cli/v2 v2.27.5 h1:WoHEJLdsXr6dDWoJgMq/CboDmyY/8HMMH1fTECbih+w= +github.com/urfave/cli/v2 v2.27.5/go.mod h1:3Sevf16NykTbInEnD0yKkjDAeZDS0A6bzhBH5hrMvTQ= +github.com/vektah/gqlparser/v2 v2.5.20 h1:kPaWbhBntxoZPaNdBaIPT1Kh0i1b/onb5kXgEdP5JCo= +github.com/vektah/gqlparser/v2 v2.5.20/go.mod h1:xMl+ta8a5M1Yo1A1Iwt/k7gSpscwSnHZdw7tfhEGfTM= +github.com/wasilibs/go-pgquery v0.0.0-20240606042535-c0843d6592cc h1:Hgim1Xgk1+viV7p0aZh9OOrMRfG+E4mGA+JsI2uB0+k= +github.com/wasilibs/go-pgquery v0.0.0-20240606042535-c0843d6592cc/go.mod h1:ah6UfXIl/oA0K3SbourB/UHggVJOBXwPZ2XudDmmFac= +github.com/wasilibs/wazero-helpers v0.0.0-20240604052452-61d7981e9a38 h1:RBu75fhabyxyGJ2zhkoNuRyObBMhVeMoXqmeaPTg2CQ= +github.com/wasilibs/wazero-helpers v0.0.0-20240604052452-61d7981e9a38/go.mod h1:Z80JvMwvze8KUlVQIdw9L7OSskZJ1yxlpi4AQhoQe4s= +github.com/xrash/smetrics v0.0.0-20240521201337-686a1a2994c1 h1:gEOO8jv9F4OT7lGCjxCBTO/36wtF6j2nSip77qHd4x4= +github.com/xrash/smetrics v0.0.0-20240521201337-686a1a2994c1/go.mod h1:Ohn+xnUBiLI6FVj/9LpzZWtj1/D6lUovWYBkxHVV3aM= +go.uber.org/atomic v1.6.0/go.mod h1:sABNBOSYdrvTF6hTgEIbc7YasKWGhgEQZyfxyTvoXHQ= +go.uber.org/atomic v1.7.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc= +go.uber.org/atomic v1.9.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc= +go.uber.org/atomic v1.11.0 h1:ZvwS0R+56ePWxUNi+Atn9dWONBPp/AUETXlHW0DxSjE= +go.uber.org/atomic v1.11.0/go.mod h1:LUxbIzbOniOlMKjJjyPfpl4v+PKK2cNJn91OQbhoJI0= +go.uber.org/goleak v1.1.10/go.mod h1:8a7PlsEVH3e/a/GLqe5IIrQx6GzcnRmZEufDUTk4A7A= +go.uber.org/goleak v1.2.0 h1:xqgm/S+aQvhWFTtR0XK3Jvg7z8kGV8P4X14IzwN3Eqk= +go.uber.org/goleak v1.2.0/go.mod h1:XJYK+MuIchqpmGmUSAzotztawfKvYLUIgg7guXrwVUo= +go.uber.org/multierr v1.6.0/go.mod h1:cdWPpRnG4AhwMwsgIHip0KRBQjJy5kYEpYjJxpXp9iU= +go.uber.org/multierr v1.7.0/go.mod h1:7EAYxJLBy9rStEaz58O2t4Uvip6FSURkq8/ppBp95ak= +go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0= +go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y= +go.uber.org/zap v1.19.0/go.mod h1:xg/QME4nWcxGxrpdeYfq7UvYrLh66cuVKdrbD1XF/NI= +go.uber.org/zap v1.26.0 h1:sI7k6L95XOKS281NhVKOFCUNIvv9e0w4BF8N3u+tCRo= +go.uber.org/zap v1.26.0/go.mod h1:dtElttAiwGvoJ/vj4IwHBS/gXsEu/pZ50mUIRWuG0so= +golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.31.0 h1:ihbySMvVjLAeSH1IbfcRTkD/iNscyz8rGzjF/E5hV6U= +golang.org/x/crypto v0.31.0/go.mod h1:kDsLvtWBEx7MV9tJOj9bnXsPbxwJQ6csT/x4KIN4Ssk= +golang.org/x/exp v0.0.0-20240506185415-9bf2ced13842 h1:vr/HnozRka3pE4EsMEg1lgkXJkTFJCVUX+S/ZT6wYzM= +golang.org/x/exp v0.0.0-20240506185415-9bf2ced13842/go.mod h1:XtvwrStGgqGPLc4cjQfWqZHG1YFdYs6swckp8vpsjnc= +golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= +golang.org/x/mod v0.20.0 h1:utOm6MM3R3dnawAiJgn0y+xvuYRsm1RKM/4giyfDgV0= +golang.org/x/mod v0.20.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= +golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.33.0 h1:74SYHlV8BIgHIFC/LrYkOGIwL19eTYXQ5wc6TBuO36I= +golang.org/x/net v0.33.0/go.mod h1:HXLR5J+9DxmrqMwG9qjGCxZ+zKXxBru04zlTvWlWuN4= +golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.10.0 h1:3NQrjDixjgGwUOCaF8w2+VYHv0Ve/vGYSbdkTa98gmQ= +golang.org/x/sync v0.10.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= +golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.28.0 h1:Fksou7UEQUWlKvIdsqzJmUmCX3cZuD2+P3XyyzwMhlA= +golang.org/x/sys v0.28.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.21.0 h1:zyQAAkrwaneQ066sspRyJaG9VNi/YJ1NfzcGB3hZ/qo= +golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ= +golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= +golang.org/x/tools v0.0.0-20191029041327-9cc4af7d6b2c/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191108193012-7d206e10da11/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.24.0 h1:J1shsA93PJUEVaUSaay7UXAyE8aimq3GW0pjlolpa24= +golang.org/x/tools v0.24.0/go.mod h1:YhNqVBIfWHdzvTLs0d8LCuMhkKUgSUKldakyV7W/WDQ= +golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +google.golang.org/genproto/googleapis/api v0.0.0-20240528184218-531527333157 h1:7whR9kGa5LUwFtpLm2ArCEejtnxlGeLbAyjFY8sGNFw= +google.golang.org/genproto/googleapis/api v0.0.0-20240528184218-531527333157/go.mod h1:99sLkeliLXfdj2J75X3Ho+rrVCaJze0uwN7zDDkjPVU= +google.golang.org/genproto/googleapis/rpc v0.0.0-20240528184218-531527333157 h1:Zy9XzmMEflZ/MAaA7vNcoebnRAld7FsPW1EeBB7V0m8= +google.golang.org/genproto/googleapis/rpc v0.0.0-20240528184218-531527333157/go.mod h1:EfXuqaE1J41VCDicxHzUDm+8rk+7ZdXzHV0IhO/I6s0= +google.golang.org/grpc v1.65.0 h1:bs/cUb4lp1G5iImFFd3u5ixQzweKizoZJAwBNLR42lc= +google.golang.org/grpc v1.65.0/go.mod h1:WgYC2ypjlB0EiQi6wdKixMqukr6lBc0Vo+oOgjrM5ZQ= +google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= +google.golang.org/protobuf v1.31.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I= +google.golang.org/protobuf v1.35.2 h1:8Ar7bF+apOIoThw1EdZl0p1oWvMqTHmpA2fRTyZO8io= +google.golang.org/protobuf v1.35.2/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/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/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= +gopkg.in/natefinch/lumberjack.v2 v2.0.0/go.mod h1:l0ndWWf7gzL7RNwBG7wST/UCcT4T24xpD6X8LsfU/+k= +gopkg.in/natefinch/lumberjack.v2 v2.2.1 h1:bBRl1b0OH9s/DuPhuXpNl+VtCaJXFZ5/uEFST95x9zc= +gopkg.in/natefinch/lumberjack.v2 v2.2.1/go.mod h1:YD8tP3GAjkrDg1eZH7EGmyESg/lsYskCTPBJVb9jqSc= +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/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +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= +modernc.org/cc/v4 v4.21.4 h1:3Be/Rdo1fpr8GrQ7IVw9OHtplU4gWbb+wNgeoBMmGLQ= +modernc.org/cc/v4 v4.21.4/go.mod h1:HM7VJTZbUCR3rV8EYBi9wxnJ0ZBRiGE5OeGXNA0IsLQ= +modernc.org/ccgo/v4 v4.19.2 h1:lwQZgvboKD0jBwdaeVCTouxhxAyN6iawF3STraAal8Y= +modernc.org/ccgo/v4 v4.19.2/go.mod h1:ysS3mxiMV38XGRTTcgo0DQTeTmAO4oCmJl1nX9VFI3s= +modernc.org/fileutil v1.3.0 h1:gQ5SIzK3H9kdfai/5x41oQiKValumqNTDXMvKo62HvE= +modernc.org/fileutil v1.3.0/go.mod h1:XatxS8fZi3pS8/hKG2GH/ArUogfxjpEKs3Ku3aK4JyQ= +modernc.org/gc/v2 v2.4.1 h1:9cNzOqPyMJBvrUipmynX0ZohMhcxPtMccYgGOJdOiBw= +modernc.org/gc/v2 v2.4.1/go.mod h1:wzN5dK1AzVGoH6XOzc3YZ+ey/jPgYHLuVckd62P0GYU= +modernc.org/gc/v3 v3.0.0-20240107210532-573471604cb6 h1:5D53IMaUuA5InSeMu9eJtlQXS2NxAhyWQvkKEgXZhHI= +modernc.org/gc/v3 v3.0.0-20240107210532-573471604cb6/go.mod h1:Qz0X07sNOR1jWYCrJMEnbW/X55x206Q7Vt4mz6/wHp4= +modernc.org/libc v1.55.3 h1:AzcW1mhlPNrRtjS5sS+eW2ISCgSOLLNyFzRh/V3Qj/U= +modernc.org/libc v1.55.3/go.mod h1:qFXepLhz+JjFThQ4kzwzOjA/y/artDeg+pcYnY+Q83w= +modernc.org/mathutil v1.6.0 h1:fRe9+AmYlaej+64JsEEhoWuAYBkOtQiMEU7n/XgfYi4= +modernc.org/mathutil v1.6.0/go.mod h1:Ui5Q9q1TR2gFm0AQRqQUaBWFLAhQpCwNcuhBOSedWPo= +modernc.org/memory v1.8.0 h1:IqGTL6eFMaDZZhEWwcREgeMXYwmW83LYW8cROZYkg+E= +modernc.org/memory v1.8.0/go.mod h1:XPZ936zp5OMKGWPqbD3JShgd/ZoQ7899TUuQqxY+peU= +modernc.org/opt v0.1.3 h1:3XOZf2yznlhC+ibLltsDGzABUGVx8J6pnFMS3E4dcq4= +modernc.org/opt v0.1.3/go.mod h1:WdSiB5evDcignE70guQKxYUl14mgWtbClRi5wmkkTX0= +modernc.org/sortutil v1.2.0 h1:jQiD3PfS2REGJNzNCMMaLSp/wdMNieTbKX920Cqdgqc= +modernc.org/sortutil v1.2.0/go.mod h1:TKU2s7kJMf1AE84OoiGppNHJwvB753OYfNl2WRb++Ss= +modernc.org/sqlite v1.31.1 h1:XVU0VyzxrYHlBhIs1DiEgSl0ZtdnPtbLVy8hSkzxGrs= +modernc.org/sqlite v1.31.1/go.mod h1:UqoylwmTb9F+IqXERT8bW9zzOWN8qwAIcLdzeBZs4hA= +modernc.org/strutil v1.2.0 h1:agBi9dp1I+eOnxXeiZawM8F4LawKv4NzGWSaLfyeNZA= +modernc.org/strutil v1.2.0/go.mod h1:/mdcBmfOibveCTBxUl5B5l6W+TTH1FXPLHZE6bTosX0= +modernc.org/token v1.1.0 h1:Xl7Ap9dKaEs5kLoOQeQmPWevfnk/DM5qcLcYlA8ys6Y= +modernc.org/token v1.1.0/go.mod h1:UGzOrNV1mAFSEB63lOFHIpNRUVMvYTc6yu1SMY/XTDM= diff --git a/tools/tools.go b/tools/tools.go new file mode 100644 index 00000000..ac12408e --- /dev/null +++ b/tools/tools.go @@ -0,0 +1,10 @@ +//go:build tools +// +build tools + +package tools + +import ( + _ "github.com/99designs/gqlgen" + _ "github.com/mfridman/tparse" + _ "github.com/sqlc-dev/sqlc/cmd/sqlc" +) From fc24b4aeab90d0725ebb9b3dd32f8f26b7351e25 Mon Sep 17 00:00:00 2001 From: Christian Banse Date: Fri, 27 Dec 2024 23:46:20 +0100 Subject: [PATCH 08/35] ++ --- .github/workflows/build.yml | 1 - 1 file changed, 1 deletion(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 5e698080..f9e9970b 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -37,7 +37,6 @@ jobs: go build -v ./... - name: Test Backend run: | - go install github.com/mfridman/tparse@latest go test -v -coverprofile=coverage.cov -coverpkg ./... -covermode=atomic ./... -json | tee output.json | tparse -follow || true tparse -format markdown -file output.json > $GITHUB_STEP_SUMMARY - name: Upload coverage reports to Codecov From 95a15892e507bd9df1f5560a7169c754b02fb41c Mon Sep 17 00:00:00 2001 From: Christian Banse Date: Sat, 28 Dec 2024 00:23:53 +0100 Subject: [PATCH 09/35] More testing, more graphql --- cli/commands/portfolio.go | 6 +- cli/commands/portfolio_test.go | 16 +- cli/commands/securities.go | 27 +- cli/commands/securities_test.go | 8 +- gen/portfolio_sql.go | 6 +- graph/generated.go | 707 +++++++++++++++--- graph/schema.graphqls | 15 +- graph/schema.resolvers.go | 20 +- persistence/models.go | 16 + persistence/portfolios.sql.go | 77 ++ .../sql/migrations/0002_create_portfolio.sql | 19 + persistence/sql/queries/portfolios.sql | 21 + service/securities/quote.go | 12 + 13 files changed, 818 insertions(+), 132 deletions(-) create mode 100644 persistence/portfolios.sql.go create mode 100644 persistence/sql/migrations/0002_create_portfolio.sql create mode 100644 persistence/sql/queries/portfolios.sql diff --git a/cli/commands/portfolio.go b/cli/commands/portfolio.go index 1fc0ba7c..c630eda2 100644 --- a/cli/commands/portfolio.go +++ b/cli/commands/portfolio.go @@ -46,6 +46,7 @@ var PortfolioCmd = &cli.Command{ Flags: []cli.Flag{ &cli.StringFlag{Name: "id", Usage: "The identifier of the portfolio, e.g. mybank-myportfolio", Required: true}, &cli.StringFlag{Name: "display-name", Usage: "The display name of the portfolio"}, + &cli.StringFlag{Name: "bank-account-id", Usage: "The bank account ID of the portfolio"}, }, }, { @@ -149,8 +150,9 @@ func CreatePortfolio(ctx context.Context, cmd *cli.Command) error { context.Background(), connect.NewRequest(&portfoliov1.CreatePortfolioRequest{ Portfolio: &portfoliov1.Portfolio{ - Id: cmd.String("id"), - DisplayName: cmd.String("display-name"), + Id: cmd.String("id"), + DisplayName: cmd.String("display-name"), + BankAccountId: cmd.String("bank-account-id"), }, }), ) diff --git a/cli/commands/portfolio_test.go b/cli/commands/portfolio_test.go index ff822ea0..b0015ad5 100644 --- a/cli/commands/portfolio_test.go +++ b/cli/commands/portfolio_test.go @@ -24,6 +24,7 @@ import ( "github.com/oxisto/money-gopher/internal" "github.com/oxisto/money-gopher/internal/testing/clitest" "github.com/oxisto/money-gopher/internal/testing/servertest" + "github.com/oxisto/money-gopher/persistence" "github.com/urfave/cli/v3" ) @@ -59,7 +60,13 @@ func TestListPortfolio(t *testing.T) { } func TestCreatePortfolio(t *testing.T) { - srv := servertest.NewServer(internal.NewTestDB(t)) + srv := servertest.NewServer(internal.NewTestDB(t, func(db *persistence.DB) { + _, err := db.Queries.CreateBankAccount(context.Background(), persistence.CreateBankAccountParams{ + ID: "mybank", + DisplayName: "My Bank", + }) + assert.NoError(t, err) + })) defer srv.Close() type args struct { @@ -75,7 +82,12 @@ func TestCreatePortfolio(t *testing.T) { name: "happy path", args: args{ ctx: clitest.NewSessionContext(t, srv), - cmd: &cli.Command{}, + cmd: clitest.MockCommand(t, + PortfolioCmd.Command("create").Flags, + "--bank-account-id", "mybank", + "--id", "mynewportfolio", + "--display-name", "My New Portfolio", + ), }, }, } diff --git a/cli/commands/securities.go b/cli/commands/securities.go index bc90ca74..1824a93e 100644 --- a/cli/commands/securities.go +++ b/cli/commands/securities.go @@ -132,31 +132,26 @@ func UpdateQuote(ctx context.Context, cmd *cli.Command) (err error) { if err != nil { return err } + return err } // UpdateAllQuotes triggers an update of all quotes. -func UpdateAllQuotes(ctx context.Context, cmd *cli.Command) error { +func UpdateAllQuotes(ctx context.Context, cmd *cli.Command) (err error) { s := mcli.FromContext(ctx) - res, err := s.SecuritiesClient.ListSecurities(context.Background(), connect.NewRequest(&portfoliov1.ListSecuritiesRequest{})) - if err != nil { - return err - } - var names []string - - for _, sec := range res.Msg.Securities { - names = append(names, sec.Id) + var query struct { + TriggerQuoteUpdate bool `graphql:"triggerQuoteUpdate(securityIDs: $IDs)" json:"security"` } - _, err = s.SecuritiesClient.TriggerSecurityQuoteUpdate( - context.Background(), - connect.NewRequest(&portfoliov1.TriggerQuoteUpdateRequest{ - SecurityIds: names, - }), - ) + err = s.GraphQL.Mutate(context.Background(), &query, map[string]interface{}{ + "IDs": []graphql.String{}, + }) + if err != nil { + return err + } - return err + return } // PredictSecurities predicts the securities for shell completion. diff --git a/cli/commands/securities_test.go b/cli/commands/securities_test.go index 4ba2f6ec..07660466 100644 --- a/cli/commands/securities_test.go +++ b/cli/commands/securities_test.go @@ -73,13 +73,7 @@ func TestUpdateQuote(t *testing.T) { } func TestUpdateAllQuotes(t *testing.T) { - srv := servertest.NewServer(internal.NewTestDB(t, func(db *persistence.DB) { - ops := persistence.Ops[*portfoliov1.Security](db) - ops.Replace(&portfoliov1.Security{ - Id: "mysecurity", - QuoteProvider: moneygopher.Ref("mock"), - }) - })) + srv := servertest.NewServer(internal.NewTestDB(t)) defer srv.Close() type args struct { diff --git a/gen/portfolio_sql.go b/gen/portfolio_sql.go index 0daa427e..2ca4eb75 100644 --- a/gen/portfolio_sql.go +++ b/gen/portfolio_sql.go @@ -40,7 +40,7 @@ display_name TEXT NOT NULL } func (*Portfolio) PrepareReplace(db *persistence.DB) (stmt *sql.Stmt, err error) { - return db.Prepare(`REPLACE INTO portfolios (id, display_name) VALUES (?,?);`) + return db.Prepare(`REPLACE INTO portfolios (id, display_name, bank_account_id) VALUES (?,?,?);`) } func (*Portfolio) PrepareList(db *persistence.DB) (stmt *sql.Stmt, err error) { @@ -73,7 +73,7 @@ func (*Portfolio) PrepareDelete(db *persistence.DB) (stmt *sql.Stmt, err error) } func (p *Portfolio) ReplaceIntoArgs() []any { - return []any{p.Id, p.DisplayName} + return []any{p.Id, p.DisplayName, p.BankAccountId} } func (p *Portfolio) UpdateArgs(columns []string) (args []any) { @@ -83,6 +83,8 @@ func (p *Portfolio) UpdateArgs(columns []string) (args []any) { args = append(args, p.Id) case "display_name": args = append(args, p.DisplayName) + case "bank_account_id": + args = append(args, p.BankAccountId) } } diff --git a/graph/generated.go b/graph/generated.go index 8ec6f6b7..2fe10684 100644 --- a/graph/generated.go +++ b/graph/generated.go @@ -39,7 +39,6 @@ type Config struct { } type ResolverRoot interface { - Currency() CurrencyResolver ListedSecurity() ListedSecurityResolver Mutation() MutationResolver Query() QueryResolver @@ -50,9 +49,14 @@ type DirectiveRoot struct { } type ComplexityRoot struct { + BankAccount struct { + DisplayName func(childComplexity int) int + ID func(childComplexity int) int + } + Currency struct { - Currency func(childComplexity int) int - Value func(childComplexity int) int + Symbol func(childComplexity int) int + Value func(childComplexity int) int } ListedSecurity struct { @@ -69,7 +73,14 @@ type ComplexityRoot struct { UpdateSecurity func(childComplexity int, id string, input SecurityInput) int } + Portfolio struct { + DisplayName func(childComplexity int) int + ID func(childComplexity int) int + } + Query struct { + Portfolio func(childComplexity int, id string) int + Portfolios func(childComplexity int) int Securities func(childComplexity int) int Security func(childComplexity int, id string) int } @@ -82,9 +93,6 @@ type ComplexityRoot struct { } } -type CurrencyResolver interface { - Currency(ctx context.Context, obj *persistence.Currency) (string, error) -} type ListedSecurityResolver interface { Security(ctx context.Context, obj *persistence.ListedSecurity) (*persistence.Security, error) LatestQuote(ctx context.Context, obj *persistence.ListedSecurity) (*persistence.Currency, error) @@ -98,6 +106,8 @@ type MutationResolver interface { type QueryResolver interface { Security(ctx context.Context, id string) (*persistence.Security, error) Securities(ctx context.Context) ([]*persistence.Security, error) + Portfolio(ctx context.Context, id string) (*persistence.Portfolio, error) + Portfolios(ctx context.Context) ([]*persistence.Portfolio, error) } type SecurityResolver interface { QuoteProvider(ctx context.Context, obj *persistence.Security) (*string, error) @@ -123,12 +133,26 @@ func (e *executableSchema) Complexity(typeName, field string, childComplexity in _ = ec switch typeName + "." + field { - case "Currency.currency": - if e.complexity.Currency.Currency == nil { + case "BankAccount.displayName": + if e.complexity.BankAccount.DisplayName == nil { break } - return e.complexity.Currency.Currency(childComplexity), true + return e.complexity.BankAccount.DisplayName(childComplexity), true + + case "BankAccount.id": + if e.complexity.BankAccount.ID == nil { + break + } + + return e.complexity.BankAccount.ID(childComplexity), true + + case "Currency.symbol": + if e.complexity.Currency.Symbol == nil { + break + } + + return e.complexity.Currency.Symbol(childComplexity), true case "Currency.value": if e.complexity.Currency.Value == nil { @@ -208,6 +232,39 @@ func (e *executableSchema) Complexity(typeName, field string, childComplexity in return e.complexity.Mutation.UpdateSecurity(childComplexity, args["id"].(string), args["input"].(SecurityInput)), true + case "Portfolio.displayName": + if e.complexity.Portfolio.DisplayName == nil { + break + } + + return e.complexity.Portfolio.DisplayName(childComplexity), true + + case "Portfolio.id": + if e.complexity.Portfolio.ID == nil { + break + } + + return e.complexity.Portfolio.ID(childComplexity), true + + case "Query.portfolio": + if e.complexity.Query.Portfolio == nil { + break + } + + args, err := ec.field_Query_portfolio_args(context.TODO(), rawArgs) + if err != nil { + return 0, false + } + + return e.complexity.Query.Portfolio(childComplexity, args["id"].(string)), true + + case "Query.portfolios": + if e.complexity.Query.Portfolios == nil { + break + } + + return e.complexity.Query.Portfolios(childComplexity), true + case "Query.securities": if e.complexity.Query.Securities == nil { break @@ -491,6 +548,29 @@ func (ec *executionContext) field_Query___type_argsName( return zeroVal, nil } +func (ec *executionContext) field_Query_portfolio_args(ctx context.Context, rawArgs map[string]any) (map[string]any, error) { + var err error + args := map[string]any{} + arg0, err := ec.field_Query_portfolio_argsID(ctx, rawArgs) + if err != nil { + return nil, err + } + args["id"] = arg0 + return args, nil +} +func (ec *executionContext) field_Query_portfolio_argsID( + ctx context.Context, + rawArgs map[string]any, +) (string, error) { + ctx = graphql.WithPathContext(ctx, graphql.NewPathWithField("id")) + if tmp, ok := rawArgs["id"]; ok { + return ec.unmarshalNString2string(ctx, tmp) + } + + var zeroVal string + return zeroVal, nil +} + func (ec *executionContext) field_Query_security_args(ctx context.Context, rawArgs map[string]any) (map[string]any, error) { var err error args := map[string]any{} @@ -568,6 +648,94 @@ func (ec *executionContext) field___Type_fields_argsIncludeDeprecated( // region **************************** field.gotpl ***************************** +func (ec *executionContext) _BankAccount_id(ctx context.Context, field graphql.CollectedField, obj *persistence.BankAccount) (ret graphql.Marshaler) { + fc, err := ec.fieldContext_BankAccount_id(ctx, field) + if err != nil { + return graphql.Null + } + ctx = graphql.WithFieldContext(ctx, fc) + defer func() { + if r := recover(); r != nil { + ec.Error(ctx, ec.Recover(ctx, r)) + ret = graphql.Null + } + }() + resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (any, error) { + ctx = rctx // use context from middleware stack in children + return obj.ID, nil + }) + if err != nil { + ec.Error(ctx, err) + return graphql.Null + } + if resTmp == nil { + if !graphql.HasFieldError(ctx, fc) { + ec.Errorf(ctx, "must not be null") + } + return graphql.Null + } + res := resTmp.(string) + fc.Result = res + return ec.marshalNString2string(ctx, field.Selections, res) +} + +func (ec *executionContext) fieldContext_BankAccount_id(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { + fc = &graphql.FieldContext{ + Object: "BankAccount", + Field: field, + IsMethod: false, + IsResolver: false, + Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + return nil, errors.New("field of type String does not have child fields") + }, + } + return fc, nil +} + +func (ec *executionContext) _BankAccount_displayName(ctx context.Context, field graphql.CollectedField, obj *persistence.BankAccount) (ret graphql.Marshaler) { + fc, err := ec.fieldContext_BankAccount_displayName(ctx, field) + if err != nil { + return graphql.Null + } + ctx = graphql.WithFieldContext(ctx, fc) + defer func() { + if r := recover(); r != nil { + ec.Error(ctx, ec.Recover(ctx, r)) + ret = graphql.Null + } + }() + resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (any, error) { + ctx = rctx // use context from middleware stack in children + return obj.DisplayName, nil + }) + if err != nil { + ec.Error(ctx, err) + return graphql.Null + } + if resTmp == nil { + if !graphql.HasFieldError(ctx, fc) { + ec.Errorf(ctx, "must not be null") + } + return graphql.Null + } + res := resTmp.(string) + fc.Result = res + return ec.marshalNString2string(ctx, field.Selections, res) +} + +func (ec *executionContext) fieldContext_BankAccount_displayName(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { + fc = &graphql.FieldContext{ + Object: "BankAccount", + Field: field, + IsMethod: false, + IsResolver: false, + Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + return nil, errors.New("field of type String does not have child fields") + }, + } + return fc, nil +} + func (ec *executionContext) _Currency_value(ctx context.Context, field graphql.CollectedField, obj *persistence.Currency) (ret graphql.Marshaler) { fc, err := ec.fieldContext_Currency_value(ctx, field) if err != nil { @@ -612,8 +780,8 @@ func (ec *executionContext) fieldContext_Currency_value(_ context.Context, field return fc, nil } -func (ec *executionContext) _Currency_currency(ctx context.Context, field graphql.CollectedField, obj *persistence.Currency) (ret graphql.Marshaler) { - fc, err := ec.fieldContext_Currency_currency(ctx, field) +func (ec *executionContext) _Currency_symbol(ctx context.Context, field graphql.CollectedField, obj *persistence.Currency) (ret graphql.Marshaler) { + fc, err := ec.fieldContext_Currency_symbol(ctx, field) if err != nil { return graphql.Null } @@ -626,7 +794,7 @@ func (ec *executionContext) _Currency_currency(ctx context.Context, field graphq }() resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (any, error) { ctx = rctx // use context from middleware stack in children - return ec.resolvers.Currency().Currency(rctx, obj) + return obj.Symbol, nil }) if err != nil { ec.Error(ctx, err) @@ -643,12 +811,12 @@ func (ec *executionContext) _Currency_currency(ctx context.Context, field graphq return ec.marshalNString2string(ctx, field.Selections, res) } -func (ec *executionContext) fieldContext_Currency_currency(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { +func (ec *executionContext) fieldContext_Currency_symbol(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { fc = &graphql.FieldContext{ Object: "Currency", Field: field, - IsMethod: true, - IsResolver: true, + IsMethod: false, + IsResolver: false, Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { return nil, errors.New("field of type String does not have child fields") }, @@ -836,8 +1004,8 @@ func (ec *executionContext) fieldContext_ListedSecurity_latestQuote(_ context.Co switch field.Name { case "value": return ec.fieldContext_Currency_value(ctx, field) - case "currency": - return ec.fieldContext_Currency_currency(ctx, field) + case "symbol": + return ec.fieldContext_Currency_symbol(ctx, field) } return nil, fmt.Errorf("no field named %q was found under type Currency", field.Name) }, @@ -1054,25 +1222,229 @@ func (ec *executionContext) fieldContext_Mutation_triggerQuoteUpdate(ctx context IsMethod: true, IsResolver: true, Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { - return nil, errors.New("field of type Boolean does not have child fields") + return nil, errors.New("field of type Boolean does not have child fields") + }, + } + defer func() { + if r := recover(); r != nil { + err = ec.Recover(ctx, r) + ec.Error(ctx, err) + } + }() + ctx = graphql.WithFieldContext(ctx, fc) + if fc.Args, err = ec.field_Mutation_triggerQuoteUpdate_args(ctx, field.ArgumentMap(ec.Variables)); err != nil { + ec.Error(ctx, err) + return fc, err + } + return fc, nil +} + +func (ec *executionContext) _Portfolio_id(ctx context.Context, field graphql.CollectedField, obj *persistence.Portfolio) (ret graphql.Marshaler) { + fc, err := ec.fieldContext_Portfolio_id(ctx, field) + if err != nil { + return graphql.Null + } + ctx = graphql.WithFieldContext(ctx, fc) + defer func() { + if r := recover(); r != nil { + ec.Error(ctx, ec.Recover(ctx, r)) + ret = graphql.Null + } + }() + resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (any, error) { + ctx = rctx // use context from middleware stack in children + return obj.ID, nil + }) + if err != nil { + ec.Error(ctx, err) + return graphql.Null + } + if resTmp == nil { + if !graphql.HasFieldError(ctx, fc) { + ec.Errorf(ctx, "must not be null") + } + return graphql.Null + } + res := resTmp.(string) + fc.Result = res + return ec.marshalNString2string(ctx, field.Selections, res) +} + +func (ec *executionContext) fieldContext_Portfolio_id(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { + fc = &graphql.FieldContext{ + Object: "Portfolio", + Field: field, + IsMethod: false, + IsResolver: false, + Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + return nil, errors.New("field of type String does not have child fields") + }, + } + return fc, nil +} + +func (ec *executionContext) _Portfolio_displayName(ctx context.Context, field graphql.CollectedField, obj *persistence.Portfolio) (ret graphql.Marshaler) { + fc, err := ec.fieldContext_Portfolio_displayName(ctx, field) + if err != nil { + return graphql.Null + } + ctx = graphql.WithFieldContext(ctx, fc) + defer func() { + if r := recover(); r != nil { + ec.Error(ctx, ec.Recover(ctx, r)) + ret = graphql.Null + } + }() + resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (any, error) { + ctx = rctx // use context from middleware stack in children + return obj.DisplayName, nil + }) + if err != nil { + ec.Error(ctx, err) + return graphql.Null + } + if resTmp == nil { + if !graphql.HasFieldError(ctx, fc) { + ec.Errorf(ctx, "must not be null") + } + return graphql.Null + } + res := resTmp.(string) + fc.Result = res + return ec.marshalNString2string(ctx, field.Selections, res) +} + +func (ec *executionContext) fieldContext_Portfolio_displayName(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { + fc = &graphql.FieldContext{ + Object: "Portfolio", + Field: field, + IsMethod: false, + IsResolver: false, + Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + return nil, errors.New("field of type String does not have child fields") + }, + } + return fc, nil +} + +func (ec *executionContext) _Query_security(ctx context.Context, field graphql.CollectedField) (ret graphql.Marshaler) { + fc, err := ec.fieldContext_Query_security(ctx, field) + if err != nil { + return graphql.Null + } + ctx = graphql.WithFieldContext(ctx, fc) + defer func() { + if r := recover(); r != nil { + ec.Error(ctx, ec.Recover(ctx, r)) + ret = graphql.Null + } + }() + resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (any, error) { + ctx = rctx // use context from middleware stack in children + return ec.resolvers.Query().Security(rctx, fc.Args["id"].(string)) + }) + if err != nil { + ec.Error(ctx, err) + return graphql.Null + } + if resTmp == nil { + return graphql.Null + } + res := resTmp.(*persistence.Security) + fc.Result = res + return ec.marshalOSecurity2ᚖgithubᚗcomᚋoxistoᚋmoneyᚑgopherᚋpersistenceᚐSecurity(ctx, field.Selections, res) +} + +func (ec *executionContext) fieldContext_Query_security(ctx context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { + fc = &graphql.FieldContext{ + Object: "Query", + Field: field, + IsMethod: true, + IsResolver: true, + Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + switch field.Name { + case "id": + return ec.fieldContext_Security_id(ctx, field) + case "displayName": + return ec.fieldContext_Security_displayName(ctx, field) + case "quoteProvider": + return ec.fieldContext_Security_quoteProvider(ctx, field) + case "listedAs": + return ec.fieldContext_Security_listedAs(ctx, field) + } + return nil, fmt.Errorf("no field named %q was found under type Security", field.Name) + }, + } + defer func() { + if r := recover(); r != nil { + err = ec.Recover(ctx, r) + ec.Error(ctx, err) + } + }() + ctx = graphql.WithFieldContext(ctx, fc) + if fc.Args, err = ec.field_Query_security_args(ctx, field.ArgumentMap(ec.Variables)); err != nil { + ec.Error(ctx, err) + return fc, err + } + return fc, nil +} + +func (ec *executionContext) _Query_securities(ctx context.Context, field graphql.CollectedField) (ret graphql.Marshaler) { + fc, err := ec.fieldContext_Query_securities(ctx, field) + if err != nil { + return graphql.Null + } + ctx = graphql.WithFieldContext(ctx, fc) + defer func() { + if r := recover(); r != nil { + ec.Error(ctx, ec.Recover(ctx, r)) + ret = graphql.Null + } + }() + resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (any, error) { + ctx = rctx // use context from middleware stack in children + return ec.resolvers.Query().Securities(rctx) + }) + if err != nil { + ec.Error(ctx, err) + return graphql.Null + } + if resTmp == nil { + if !graphql.HasFieldError(ctx, fc) { + ec.Errorf(ctx, "must not be null") + } + return graphql.Null + } + res := resTmp.([]*persistence.Security) + fc.Result = res + return ec.marshalNSecurity2ᚕᚖgithubᚗcomᚋoxistoᚋmoneyᚑgopherᚋpersistenceᚐSecurityᚄ(ctx, field.Selections, res) +} + +func (ec *executionContext) fieldContext_Query_securities(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { + fc = &graphql.FieldContext{ + Object: "Query", + Field: field, + IsMethod: true, + IsResolver: true, + Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + switch field.Name { + case "id": + return ec.fieldContext_Security_id(ctx, field) + case "displayName": + return ec.fieldContext_Security_displayName(ctx, field) + case "quoteProvider": + return ec.fieldContext_Security_quoteProvider(ctx, field) + case "listedAs": + return ec.fieldContext_Security_listedAs(ctx, field) + } + return nil, fmt.Errorf("no field named %q was found under type Security", field.Name) }, } - defer func() { - if r := recover(); r != nil { - err = ec.Recover(ctx, r) - ec.Error(ctx, err) - } - }() - ctx = graphql.WithFieldContext(ctx, fc) - if fc.Args, err = ec.field_Mutation_triggerQuoteUpdate_args(ctx, field.ArgumentMap(ec.Variables)); err != nil { - ec.Error(ctx, err) - return fc, err - } return fc, nil } -func (ec *executionContext) _Query_security(ctx context.Context, field graphql.CollectedField) (ret graphql.Marshaler) { - fc, err := ec.fieldContext_Query_security(ctx, field) +func (ec *executionContext) _Query_portfolio(ctx context.Context, field graphql.CollectedField) (ret graphql.Marshaler) { + fc, err := ec.fieldContext_Query_portfolio(ctx, field) if err != nil { return graphql.Null } @@ -1085,7 +1457,7 @@ func (ec *executionContext) _Query_security(ctx context.Context, field graphql.C }() resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (any, error) { ctx = rctx // use context from middleware stack in children - return ec.resolvers.Query().Security(rctx, fc.Args["id"].(string)) + return ec.resolvers.Query().Portfolio(rctx, fc.Args["id"].(string)) }) if err != nil { ec.Error(ctx, err) @@ -1094,12 +1466,12 @@ func (ec *executionContext) _Query_security(ctx context.Context, field graphql.C if resTmp == nil { return graphql.Null } - res := resTmp.(*persistence.Security) + res := resTmp.(*persistence.Portfolio) fc.Result = res - return ec.marshalOSecurity2ᚖgithubᚗcomᚋoxistoᚋmoneyᚑgopherᚋpersistenceᚐSecurity(ctx, field.Selections, res) + return ec.marshalOPortfolio2ᚖgithubᚗcomᚋoxistoᚋmoneyᚑgopherᚋpersistenceᚐPortfolio(ctx, field.Selections, res) } -func (ec *executionContext) fieldContext_Query_security(ctx context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { +func (ec *executionContext) fieldContext_Query_portfolio(ctx context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { fc = &graphql.FieldContext{ Object: "Query", Field: field, @@ -1108,15 +1480,11 @@ func (ec *executionContext) fieldContext_Query_security(ctx context.Context, fie Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { switch field.Name { case "id": - return ec.fieldContext_Security_id(ctx, field) + return ec.fieldContext_Portfolio_id(ctx, field) case "displayName": - return ec.fieldContext_Security_displayName(ctx, field) - case "quoteProvider": - return ec.fieldContext_Security_quoteProvider(ctx, field) - case "listedAs": - return ec.fieldContext_Security_listedAs(ctx, field) + return ec.fieldContext_Portfolio_displayName(ctx, field) } - return nil, fmt.Errorf("no field named %q was found under type Security", field.Name) + return nil, fmt.Errorf("no field named %q was found under type Portfolio", field.Name) }, } defer func() { @@ -1126,15 +1494,15 @@ func (ec *executionContext) fieldContext_Query_security(ctx context.Context, fie } }() ctx = graphql.WithFieldContext(ctx, fc) - if fc.Args, err = ec.field_Query_security_args(ctx, field.ArgumentMap(ec.Variables)); err != nil { + if fc.Args, err = ec.field_Query_portfolio_args(ctx, field.ArgumentMap(ec.Variables)); err != nil { ec.Error(ctx, err) return fc, err } return fc, nil } -func (ec *executionContext) _Query_securities(ctx context.Context, field graphql.CollectedField) (ret graphql.Marshaler) { - fc, err := ec.fieldContext_Query_securities(ctx, field) +func (ec *executionContext) _Query_portfolios(ctx context.Context, field graphql.CollectedField) (ret graphql.Marshaler) { + fc, err := ec.fieldContext_Query_portfolios(ctx, field) if err != nil { return graphql.Null } @@ -1147,7 +1515,7 @@ func (ec *executionContext) _Query_securities(ctx context.Context, field graphql }() resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (any, error) { ctx = rctx // use context from middleware stack in children - return ec.resolvers.Query().Securities(rctx) + return ec.resolvers.Query().Portfolios(rctx) }) if err != nil { ec.Error(ctx, err) @@ -1159,12 +1527,12 @@ func (ec *executionContext) _Query_securities(ctx context.Context, field graphql } return graphql.Null } - res := resTmp.([]*persistence.Security) + res := resTmp.([]*persistence.Portfolio) fc.Result = res - return ec.marshalNSecurity2ᚕᚖgithubᚗcomᚋoxistoᚋmoneyᚑgopherᚋpersistenceᚐSecurityᚄ(ctx, field.Selections, res) + return ec.marshalNPortfolio2ᚕᚖgithubᚗcomᚋoxistoᚋmoneyᚑgopherᚋpersistenceᚐPortfolioᚄ(ctx, field.Selections, res) } -func (ec *executionContext) fieldContext_Query_securities(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { +func (ec *executionContext) fieldContext_Query_portfolios(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { fc = &graphql.FieldContext{ Object: "Query", Field: field, @@ -1173,15 +1541,11 @@ func (ec *executionContext) fieldContext_Query_securities(_ context.Context, fie Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { switch field.Name { case "id": - return ec.fieldContext_Security_id(ctx, field) + return ec.fieldContext_Portfolio_id(ctx, field) case "displayName": - return ec.fieldContext_Security_displayName(ctx, field) - case "quoteProvider": - return ec.fieldContext_Security_quoteProvider(ctx, field) - case "listedAs": - return ec.fieldContext_Security_listedAs(ctx, field) + return ec.fieldContext_Portfolio_displayName(ctx, field) } - return nil, fmt.Errorf("no field named %q was found under type Security", field.Name) + return nil, fmt.Errorf("no field named %q was found under type Portfolio", field.Name) }, } return fc, nil @@ -3354,6 +3718,50 @@ func (ec *executionContext) unmarshalInputSecurityInput(ctx context.Context, obj // region **************************** object.gotpl **************************** +var bankAccountImplementors = []string{"BankAccount"} + +func (ec *executionContext) _BankAccount(ctx context.Context, sel ast.SelectionSet, obj *persistence.BankAccount) graphql.Marshaler { + fields := graphql.CollectFields(ec.OperationContext, sel, bankAccountImplementors) + + out := graphql.NewFieldSet(fields) + deferred := make(map[string]*graphql.FieldSet) + for i, field := range fields { + switch field.Name { + case "__typename": + out.Values[i] = graphql.MarshalString("BankAccount") + case "id": + out.Values[i] = ec._BankAccount_id(ctx, field, obj) + if out.Values[i] == graphql.Null { + out.Invalids++ + } + case "displayName": + out.Values[i] = ec._BankAccount_displayName(ctx, field, obj) + if out.Values[i] == graphql.Null { + out.Invalids++ + } + default: + panic("unknown field " + strconv.Quote(field.Name)) + } + } + out.Dispatch(ctx) + if out.Invalids > 0 { + return graphql.Null + } + + atomic.AddInt32(&ec.deferred, int32(len(deferred))) + + for label, dfs := range deferred { + ec.processDeferredGroup(graphql.DeferredGroup{ + Label: label, + Path: graphql.GetPath(ctx), + FieldSet: dfs, + Context: ctx, + }) + } + + return out +} + var currencyImplementors = []string{"Currency"} func (ec *executionContext) _Currency(ctx context.Context, sel ast.SelectionSet, obj *persistence.Currency) graphql.Marshaler { @@ -3368,44 +3776,13 @@ func (ec *executionContext) _Currency(ctx context.Context, sel ast.SelectionSet, case "value": out.Values[i] = ec._Currency_value(ctx, field, obj) if out.Values[i] == graphql.Null { - atomic.AddUint32(&out.Invalids, 1) - } - case "currency": - field := field - - innerFunc := func(ctx context.Context, fs *graphql.FieldSet) (res graphql.Marshaler) { - defer func() { - if r := recover(); r != nil { - ec.Error(ctx, ec.Recover(ctx, r)) - } - }() - res = ec._Currency_currency(ctx, field, obj) - if res == graphql.Null { - atomic.AddUint32(&fs.Invalids, 1) - } - return res + out.Invalids++ } - - if field.Deferrable != nil { - dfs, ok := deferred[field.Deferrable.Label] - di := 0 - if ok { - dfs.AddField(field) - di = len(dfs.Values) - 1 - } else { - dfs = graphql.NewFieldSet([]graphql.CollectedField{field}) - deferred[field.Deferrable.Label] = dfs - } - dfs.Concurrently(di, func(ctx context.Context) graphql.Marshaler { - return innerFunc(ctx, dfs) - }) - - // don't run the out.Concurrently() call below - out.Values[i] = graphql.Null - continue + case "symbol": + out.Values[i] = ec._Currency_symbol(ctx, field, obj) + if out.Values[i] == graphql.Null { + out.Invalids++ } - - out.Concurrently(i, func(ctx context.Context) graphql.Marshaler { return innerFunc(ctx, out) }) default: panic("unknown field " + strconv.Quote(field.Name)) } @@ -3638,6 +4015,50 @@ func (ec *executionContext) _Mutation(ctx context.Context, sel ast.SelectionSet) return out } +var portfolioImplementors = []string{"Portfolio"} + +func (ec *executionContext) _Portfolio(ctx context.Context, sel ast.SelectionSet, obj *persistence.Portfolio) graphql.Marshaler { + fields := graphql.CollectFields(ec.OperationContext, sel, portfolioImplementors) + + out := graphql.NewFieldSet(fields) + deferred := make(map[string]*graphql.FieldSet) + for i, field := range fields { + switch field.Name { + case "__typename": + out.Values[i] = graphql.MarshalString("Portfolio") + case "id": + out.Values[i] = ec._Portfolio_id(ctx, field, obj) + if out.Values[i] == graphql.Null { + out.Invalids++ + } + case "displayName": + out.Values[i] = ec._Portfolio_displayName(ctx, field, obj) + if out.Values[i] == graphql.Null { + out.Invalids++ + } + default: + panic("unknown field " + strconv.Quote(field.Name)) + } + } + out.Dispatch(ctx) + if out.Invalids > 0 { + return graphql.Null + } + + atomic.AddInt32(&ec.deferred, int32(len(deferred))) + + for label, dfs := range deferred { + ec.processDeferredGroup(graphql.DeferredGroup{ + Label: label, + Path: graphql.GetPath(ctx), + FieldSet: dfs, + Context: ctx, + }) + } + + return out +} + var queryImplementors = []string{"Query"} func (ec *executionContext) _Query(ctx context.Context, sel ast.SelectionSet) graphql.Marshaler { @@ -3697,6 +4118,47 @@ func (ec *executionContext) _Query(ctx context.Context, sel ast.SelectionSet) gr func(ctx context.Context) graphql.Marshaler { return innerFunc(ctx, out) }) } + out.Concurrently(i, func(ctx context.Context) graphql.Marshaler { return rrm(innerCtx) }) + case "portfolio": + field := field + + innerFunc := func(ctx context.Context, _ *graphql.FieldSet) (res graphql.Marshaler) { + defer func() { + if r := recover(); r != nil { + ec.Error(ctx, ec.Recover(ctx, r)) + } + }() + res = ec._Query_portfolio(ctx, field) + return res + } + + rrm := func(ctx context.Context) graphql.Marshaler { + return ec.OperationContext.RootResolverMiddleware(ctx, + func(ctx context.Context) graphql.Marshaler { return innerFunc(ctx, out) }) + } + + out.Concurrently(i, func(ctx context.Context) graphql.Marshaler { return rrm(innerCtx) }) + case "portfolios": + field := field + + innerFunc := func(ctx context.Context, fs *graphql.FieldSet) (res graphql.Marshaler) { + defer func() { + if r := recover(); r != nil { + ec.Error(ctx, ec.Recover(ctx, r)) + } + }() + res = ec._Query_portfolios(ctx, field) + if res == graphql.Null { + atomic.AddUint32(&fs.Invalids, 1) + } + return res + } + + rrm := func(ctx context.Context) graphql.Marshaler { + return ec.OperationContext.RootResolverMiddleware(ctx, + func(ctx context.Context) graphql.Marshaler { return innerFunc(ctx, out) }) + } + out.Concurrently(i, func(ctx context.Context) graphql.Marshaler { return rrm(innerCtx) }) case "__type": out.Values[i] = ec.OperationContext.RootResolverMiddleware(innerCtx, func(ctx context.Context) (res graphql.Marshaler) { @@ -4225,6 +4687,60 @@ func (ec *executionContext) unmarshalNListedSecurityInput2ᚖgithubᚗcomᚋoxis return &res, graphql.ErrorOnPath(ctx, err) } +func (ec *executionContext) marshalNPortfolio2ᚕᚖgithubᚗcomᚋoxistoᚋmoneyᚑgopherᚋpersistenceᚐPortfolioᚄ(ctx context.Context, sel ast.SelectionSet, v []*persistence.Portfolio) graphql.Marshaler { + ret := make(graphql.Array, len(v)) + var wg sync.WaitGroup + isLen1 := len(v) == 1 + if !isLen1 { + wg.Add(len(v)) + } + for i := range v { + i := i + fc := &graphql.FieldContext{ + Index: &i, + Result: &v[i], + } + ctx := graphql.WithFieldContext(ctx, fc) + f := func(i int) { + defer func() { + if r := recover(); r != nil { + ec.Error(ctx, ec.Recover(ctx, r)) + ret = nil + } + }() + if !isLen1 { + defer wg.Done() + } + ret[i] = ec.marshalNPortfolio2ᚖgithubᚗcomᚋoxistoᚋmoneyᚑgopherᚋpersistenceᚐPortfolio(ctx, sel, v[i]) + } + if isLen1 { + f(i) + } else { + go f(i) + } + + } + wg.Wait() + + for _, e := range ret { + if e == graphql.Null { + return graphql.Null + } + } + + return ret +} + +func (ec *executionContext) marshalNPortfolio2ᚖgithubᚗcomᚋoxistoᚋmoneyᚑgopherᚋpersistenceᚐPortfolio(ctx context.Context, sel ast.SelectionSet, v *persistence.Portfolio) graphql.Marshaler { + if v == nil { + if !graphql.HasFieldError(ctx, graphql.GetFieldContext(ctx)) { + ec.Errorf(ctx, "the requested element is null which the schema does not allow") + } + return graphql.Null + } + return ec._Portfolio(ctx, sel, v) +} + func (ec *executionContext) marshalNSecurity2githubᚗcomᚋoxistoᚋmoneyᚑgopherᚋpersistenceᚐSecurity(ctx context.Context, sel ast.SelectionSet, v persistence.Security) graphql.Marshaler { return ec._Security(ctx, sel, &v) } @@ -4672,6 +5188,13 @@ func (ec *executionContext) unmarshalOListedSecurityInput2ᚕᚖgithubᚗcomᚋo return res, nil } +func (ec *executionContext) marshalOPortfolio2ᚖgithubᚗcomᚋoxistoᚋmoneyᚑgopherᚋpersistenceᚐPortfolio(ctx context.Context, sel ast.SelectionSet, v *persistence.Portfolio) graphql.Marshaler { + if v == nil { + return graphql.Null + } + return ec._Portfolio(ctx, sel, v) +} + func (ec *executionContext) marshalOSecurity2ᚖgithubᚗcomᚋoxistoᚋmoneyᚑgopherᚋpersistenceᚐSecurity(ctx context.Context, sel ast.SelectionSet, v *persistence.Security) graphql.Marshaler { if v == nil { return graphql.Null diff --git a/graph/schema.graphqls b/graph/schema.graphqls index 124f4db2..3aaec502 100644 --- a/graph/schema.graphqls +++ b/graph/schema.graphqls @@ -2,7 +2,7 @@ scalar Date type Currency { value: Int! - currency: String! + symbol: String! } type Security { @@ -20,6 +20,16 @@ type ListedSecurity { latestQuoteTimestamp: Date } +type Portfolio { + id: String! + displayName: String! +} + +type BankAccount { + id: String! + displayName: String! +} + input SecurityInput { id: String! displayName: String! @@ -44,4 +54,7 @@ type Mutation { type Query { security(id: String!): Security securities: [Security!]! + + portfolio(id: String!): Portfolio + portfolios: [Portfolio!]! } diff --git a/graph/schema.resolvers.go b/graph/schema.resolvers.go index 3c199aa4..cb2475f4 100644 --- a/graph/schema.resolvers.go +++ b/graph/schema.resolvers.go @@ -6,18 +6,12 @@ package graph import ( "context" - "fmt" "slices" "time" "github.com/oxisto/money-gopher/persistence" ) -// Currency is the resolver for the currency field. -func (r *currencyResolver) Currency(ctx context.Context, obj *persistence.Currency) (string, error) { - panic(fmt.Errorf("not implemented: Currency - currency")) -} - // Security is the resolver for the security field. func (r *listedSecurityResolver) Security(ctx context.Context, obj *persistence.ListedSecurity) (*persistence.Security, error) { return r.DB.GetSecurity(ctx, obj.SecurityID) @@ -142,6 +136,16 @@ func (r *queryResolver) Securities(ctx context.Context) ([]*persistence.Security return r.DB.ListSecurities(ctx) } +// Portfolio is the resolver for the portfolio field. +func (r *queryResolver) Portfolio(ctx context.Context, id string) (*persistence.Portfolio, error) { + return r.DB.GetPortfolio(ctx, id) +} + +// Portfolios is the resolver for the portfolios field. +func (r *queryResolver) Portfolios(ctx context.Context) ([]*persistence.Portfolio, error) { + return r.DB.ListPortfolios(ctx) +} + // QuoteProvider is the resolver for the quoteProvider field. func (r *securityResolver) QuoteProvider(ctx context.Context, obj *persistence.Security) (*string, error) { if obj.QuoteProvider.Valid { @@ -156,9 +160,6 @@ func (r *securityResolver) ListedAs(ctx context.Context, obj *persistence.Securi return obj.ListedAs(ctx, r.DB) } -// Currency returns CurrencyResolver implementation. -func (r *Resolver) Currency() CurrencyResolver { return ¤cyResolver{r} } - // ListedSecurity returns ListedSecurityResolver implementation. func (r *Resolver) ListedSecurity() ListedSecurityResolver { return &listedSecurityResolver{r} } @@ -171,7 +172,6 @@ func (r *Resolver) Query() QueryResolver { return &queryResolver{r} } // Security returns SecurityResolver implementation. func (r *Resolver) Security() SecurityResolver { return &securityResolver{r} } -type currencyResolver struct{ *Resolver } type listedSecurityResolver struct{ *Resolver } type mutationResolver struct{ *Resolver } type queryResolver struct{ *Resolver } diff --git a/persistence/models.go b/persistence/models.go index c83e9d6c..7904a75e 100644 --- a/persistence/models.go +++ b/persistence/models.go @@ -8,6 +8,13 @@ import ( "database/sql" ) +type BankAccount struct { + // ID is the primary identifier for a bank account. + ID string + // DisplayName is the human-readable name of the bank account. + DisplayName string +} + // ListedSecurity represents a security that is listed on a particular exchange. type ListedSecurity struct { // SecurityID is the ID of the security. @@ -22,6 +29,15 @@ type ListedSecurity struct { LatestQuoteTimestamp sql.NullTime } +type Portfolio struct { + // ID is the primary identifier for a portfolio. + ID string + // DisplayName is the human-readable name of the portfolio. + DisplayName string + // BankAccountID is the ID of the bank account that holds the portfolio. + BankAccountID string +} + // Security represents a security that can be traded on an exchange. type Security struct { // ID is the primary identifier for a security. diff --git a/persistence/portfolios.sql.go b/persistence/portfolios.sql.go new file mode 100644 index 00000000..70371ac0 --- /dev/null +++ b/persistence/portfolios.sql.go @@ -0,0 +1,77 @@ +// Code generated by sqlc. DO NOT EDIT. +// versions: +// sqlc v1.27.0 +// source: portfolios.sql + +package persistence + +import ( + "context" +) + +const createBankAccount = `-- name: CreateBankAccount :one +INSERT INTO + bank_accounts (id, display_name) +VALUES + (?, ?) RETURNING id, display_name +` + +type CreateBankAccountParams struct { + ID string + DisplayName string +} + +func (q *Queries) CreateBankAccount(ctx context.Context, arg CreateBankAccountParams) (*BankAccount, error) { + row := q.db.QueryRowContext(ctx, createBankAccount, arg.ID, arg.DisplayName) + var i BankAccount + err := row.Scan(&i.ID, &i.DisplayName) + return &i, err +} + +const getPortfolio = `-- name: GetPortfolio :one +SELECT + id, display_name, bank_account_id +FROM + portfolios +WHERE + id = ? +` + +func (q *Queries) GetPortfolio(ctx context.Context, id string) (*Portfolio, error) { + row := q.db.QueryRowContext(ctx, getPortfolio, id) + var i Portfolio + err := row.Scan(&i.ID, &i.DisplayName, &i.BankAccountID) + return &i, err +} + +const listPortfolios = `-- name: ListPortfolios :many +SELECT + id, display_name, bank_account_id +FROM + portfolios +ORDER BY + id +` + +func (q *Queries) ListPortfolios(ctx context.Context) ([]*Portfolio, error) { + rows, err := q.db.QueryContext(ctx, listPortfolios) + if err != nil { + return nil, err + } + defer rows.Close() + var items []*Portfolio + for rows.Next() { + var i Portfolio + if err := rows.Scan(&i.ID, &i.DisplayName, &i.BankAccountID); err != nil { + return nil, err + } + items = append(items, &i) + } + if err := rows.Close(); err != nil { + return nil, err + } + if err := rows.Err(); err != nil { + return nil, err + } + return items, nil +} diff --git a/persistence/sql/migrations/0002_create_portfolio.sql b/persistence/sql/migrations/0002_create_portfolio.sql new file mode 100644 index 00000000..001ce224 --- /dev/null +++ b/persistence/sql/migrations/0002_create_portfolio.sql @@ -0,0 +1,19 @@ +-- +goose Up +CREATE TABLE + IF NOT EXISTS portfolios ( + id TEXT PRIMARY KEY, -- ID is the primary identifier for a portfolio. + display_name TEXT NOT NULL, -- DisplayName is the human-readable name of the portfolio. + bank_account_id TEXT NOT NULL, -- BankAccountID is the ID of the bank account that holds the portfolio. + FOREIGN KEY (bank_account_id) REFERENCES bank_accounts (id) ON DELETE RESTRICT + ); + +CREATE TABLE + IF NOT EXISTS bank_accounts ( + id TEXT PRIMARY KEY, -- ID is the primary identifier for a bank account. + display_name TEXT NOT NULL -- DisplayName is the human-readable name of the bank account. + ); + +-- +goose Down +DROP TABLE portfolios; + +DROP TABLE bank_accounts; \ No newline at end of file diff --git a/persistence/sql/queries/portfolios.sql b/persistence/sql/queries/portfolios.sql new file mode 100644 index 00000000..51c50e52 --- /dev/null +++ b/persistence/sql/queries/portfolios.sql @@ -0,0 +1,21 @@ +-- name: GetPortfolio :one +SELECT + * +FROM + portfolios +WHERE + id = ?; + +-- name: ListPortfolios :many +SELECT + * +FROM + portfolios +ORDER BY + id; + +-- name: CreateBankAccount :one +INSERT INTO + bank_accounts (id, display_name) +VALUES + (?, ?) RETURNING *; \ No newline at end of file diff --git a/service/securities/quote.go b/service/securities/quote.go index ef63b45b..a4c704eb 100644 --- a/service/securities/quote.go +++ b/service/securities/quote.go @@ -33,11 +33,23 @@ import ( func (svc *service) UpdateQuotes(ctx context.Context, IDs []string) (err error) { var ( sec *persistence.Security + secs []*persistence.Security listed []*persistence.ListedSecurity qp QuoteProvider ok bool ) + if len(IDs) == 0 { + secs, err = svc.db.ListSecurities(ctx) + if err != nil { + return err + } + + for _, sec := range secs { + IDs = append(IDs, sec.ID) + } + } + for _, id := range IDs { // Fetch security sec, err = svc.db.GetSecurity(ctx, id) From 04d251e9d1c164063a87505686b5876932c66d70 Mon Sep 17 00:00:00 2001 From: Christian Banse Date: Sat, 28 Dec 2024 10:21:08 +0100 Subject: [PATCH 10/35] Adding portfolio, breaking everything --- go.mod | 4 +- graph/generated.go | 1517 ++++++++++++++--- graph/models_gen.go | 33 + graph/schema.graphqls | 53 + graph/schema.resolvers.go | 15 + persistence/models.go | 16 + persistence/portfolios.sql.go | 16 + .../sql/migrations/0002_create_portfolio.sql | 18 + persistence/sql/queries/portfolios.sql | 8 + service/portfolio/snapshot.go | 15 +- 10 files changed, 1487 insertions(+), 208 deletions(-) diff --git a/go.mod b/go.mod index 8fdc8256..e3c41c77 100644 --- a/go.mod +++ b/go.mod @@ -1,8 +1,6 @@ module github.com/oxisto/money-gopher -go 1.22.5 - -toolchain go1.23.4 +go 1.23.4 require ( connectrpc.com/connect v1.16.2 diff --git a/graph/generated.go b/graph/generated.go index 2fe10684..c6780f6a 100644 --- a/graph/generated.go +++ b/graph/generated.go @@ -41,6 +41,7 @@ type Config struct { type ResolverRoot interface { ListedSecurity() ListedSecurityResolver Mutation() MutationResolver + Portfolio() PortfolioResolver Query() QueryResolver Security() SecurityResolver } @@ -74,8 +75,27 @@ type ComplexityRoot struct { } Portfolio struct { + BankAccount func(childComplexity int) int DisplayName func(childComplexity int) int ID func(childComplexity int) int + Snapshot func(childComplexity int, time string) int + } + + PortfolioPosition struct { + Gains func(childComplexity int) int + MarketPrice func(childComplexity int) int + MarketValue func(childComplexity int) int + ProfitOrLoss func(childComplexity int) int + PurchasePrice func(childComplexity int) int + PurchaseValue func(childComplexity int) int + Quantity func(childComplexity int) int + Security func(childComplexity int) int + TotalFees func(childComplexity int) int + } + + PortfolioSnapshot struct { + Position func(childComplexity int) int + Time func(childComplexity int) int } Query struct { @@ -103,6 +123,10 @@ type MutationResolver interface { UpdateSecurity(ctx context.Context, id string, input SecurityInput) (*persistence.Security, error) TriggerQuoteUpdate(ctx context.Context, securityIDs []string) (bool, error) } +type PortfolioResolver interface { + BankAccount(ctx context.Context, obj *persistence.Portfolio) (*persistence.BankAccount, error) + Snapshot(ctx context.Context, obj *persistence.Portfolio, time string) (*PortfolioSnapshot, error) +} type QueryResolver interface { Security(ctx context.Context, id string) (*persistence.Security, error) Securities(ctx context.Context) ([]*persistence.Security, error) @@ -232,6 +256,13 @@ func (e *executableSchema) Complexity(typeName, field string, childComplexity in return e.complexity.Mutation.UpdateSecurity(childComplexity, args["id"].(string), args["input"].(SecurityInput)), true + case "Portfolio.bankAccount": + if e.complexity.Portfolio.BankAccount == nil { + break + } + + return e.complexity.Portfolio.BankAccount(childComplexity), true + case "Portfolio.displayName": if e.complexity.Portfolio.DisplayName == nil { break @@ -246,6 +277,95 @@ func (e *executableSchema) Complexity(typeName, field string, childComplexity in return e.complexity.Portfolio.ID(childComplexity), true + case "Portfolio.snapshot": + if e.complexity.Portfolio.Snapshot == nil { + break + } + + args, err := ec.field_Portfolio_snapshot_args(context.TODO(), rawArgs) + if err != nil { + return 0, false + } + + return e.complexity.Portfolio.Snapshot(childComplexity, args["time"].(string)), true + + case "PortfolioPosition.gains": + if e.complexity.PortfolioPosition.Gains == nil { + break + } + + return e.complexity.PortfolioPosition.Gains(childComplexity), true + + case "PortfolioPosition.marketPrice": + if e.complexity.PortfolioPosition.MarketPrice == nil { + break + } + + return e.complexity.PortfolioPosition.MarketPrice(childComplexity), true + + case "PortfolioPosition.marketValue": + if e.complexity.PortfolioPosition.MarketValue == nil { + break + } + + return e.complexity.PortfolioPosition.MarketValue(childComplexity), true + + case "PortfolioPosition.profitOrLoss": + if e.complexity.PortfolioPosition.ProfitOrLoss == nil { + break + } + + return e.complexity.PortfolioPosition.ProfitOrLoss(childComplexity), true + + case "PortfolioPosition.purchasePrice": + if e.complexity.PortfolioPosition.PurchasePrice == nil { + break + } + + return e.complexity.PortfolioPosition.PurchasePrice(childComplexity), true + + case "PortfolioPosition.purchaseValue": + if e.complexity.PortfolioPosition.PurchaseValue == nil { + break + } + + return e.complexity.PortfolioPosition.PurchaseValue(childComplexity), true + + case "PortfolioPosition.quantity": + if e.complexity.PortfolioPosition.Quantity == nil { + break + } + + return e.complexity.PortfolioPosition.Quantity(childComplexity), true + + case "PortfolioPosition.security": + if e.complexity.PortfolioPosition.Security == nil { + break + } + + return e.complexity.PortfolioPosition.Security(childComplexity), true + + case "PortfolioPosition.totalFees": + if e.complexity.PortfolioPosition.TotalFees == nil { + break + } + + return e.complexity.PortfolioPosition.TotalFees(childComplexity), true + + case "PortfolioSnapshot.position": + if e.complexity.PortfolioSnapshot.Position == nil { + break + } + + return e.complexity.PortfolioSnapshot.Position(childComplexity), true + + case "PortfolioSnapshot.time": + if e.complexity.PortfolioSnapshot.Time == nil { + break + } + + return e.complexity.PortfolioSnapshot.Time(childComplexity), true + case "Query.portfolio": if e.complexity.Query.Portfolio == nil { break @@ -525,6 +645,29 @@ func (ec *executionContext) field_Mutation_updateSecurity_argsInput( return zeroVal, nil } +func (ec *executionContext) field_Portfolio_snapshot_args(ctx context.Context, rawArgs map[string]any) (map[string]any, error) { + var err error + args := map[string]any{} + arg0, err := ec.field_Portfolio_snapshot_argsTime(ctx, rawArgs) + if err != nil { + return nil, err + } + args["time"] = arg0 + return args, nil +} +func (ec *executionContext) field_Portfolio_snapshot_argsTime( + ctx context.Context, + rawArgs map[string]any, +) (string, error) { + ctx = graphql.WithPathContext(ctx, graphql.NewPathWithField("time")) + if tmp, ok := rawArgs["time"]; ok { + return ec.unmarshalNDate2string(ctx, tmp) + } + + var zeroVal string + return zeroVal, nil +} + func (ec *executionContext) field_Query___type_args(ctx context.Context, rawArgs map[string]any) (map[string]any, error) { var err error args := map[string]any{} @@ -1327,8 +1470,8 @@ func (ec *executionContext) fieldContext_Portfolio_displayName(_ context.Context return fc, nil } -func (ec *executionContext) _Query_security(ctx context.Context, field graphql.CollectedField) (ret graphql.Marshaler) { - fc, err := ec.fieldContext_Query_security(ctx, field) +func (ec *executionContext) _Portfolio_bankAccount(ctx context.Context, field graphql.CollectedField, obj *persistence.Portfolio) (ret graphql.Marshaler) { + fc, err := ec.fieldContext_Portfolio_bankAccount(ctx, field) if err != nil { return graphql.Null } @@ -1341,38 +1484,84 @@ func (ec *executionContext) _Query_security(ctx context.Context, field graphql.C }() resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (any, error) { ctx = rctx // use context from middleware stack in children - return ec.resolvers.Query().Security(rctx, fc.Args["id"].(string)) + return ec.resolvers.Portfolio().BankAccount(rctx, obj) }) if err != nil { ec.Error(ctx, err) return graphql.Null } if resTmp == nil { + if !graphql.HasFieldError(ctx, fc) { + ec.Errorf(ctx, "must not be null") + } return graphql.Null } - res := resTmp.(*persistence.Security) + res := resTmp.(*persistence.BankAccount) fc.Result = res - return ec.marshalOSecurity2ᚖgithubᚗcomᚋoxistoᚋmoneyᚑgopherᚋpersistenceᚐSecurity(ctx, field.Selections, res) + return ec.marshalNBankAccount2ᚖgithubᚗcomᚋoxistoᚋmoneyᚑgopherᚋpersistenceᚐBankAccount(ctx, field.Selections, res) } -func (ec *executionContext) fieldContext_Query_security(ctx context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { +func (ec *executionContext) fieldContext_Portfolio_bankAccount(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { fc = &graphql.FieldContext{ - Object: "Query", + Object: "Portfolio", Field: field, IsMethod: true, IsResolver: true, Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { switch field.Name { case "id": - return ec.fieldContext_Security_id(ctx, field) + return ec.fieldContext_BankAccount_id(ctx, field) case "displayName": - return ec.fieldContext_Security_displayName(ctx, field) - case "quoteProvider": - return ec.fieldContext_Security_quoteProvider(ctx, field) - case "listedAs": - return ec.fieldContext_Security_listedAs(ctx, field) + return ec.fieldContext_BankAccount_displayName(ctx, field) } - return nil, fmt.Errorf("no field named %q was found under type Security", field.Name) + return nil, fmt.Errorf("no field named %q was found under type BankAccount", field.Name) + }, + } + return fc, nil +} + +func (ec *executionContext) _Portfolio_snapshot(ctx context.Context, field graphql.CollectedField, obj *persistence.Portfolio) (ret graphql.Marshaler) { + fc, err := ec.fieldContext_Portfolio_snapshot(ctx, field) + if err != nil { + return graphql.Null + } + ctx = graphql.WithFieldContext(ctx, fc) + defer func() { + if r := recover(); r != nil { + ec.Error(ctx, ec.Recover(ctx, r)) + ret = graphql.Null + } + }() + resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (any, error) { + ctx = rctx // use context from middleware stack in children + return ec.resolvers.Portfolio().Snapshot(rctx, obj, fc.Args["time"].(string)) + }) + if err != nil { + ec.Error(ctx, err) + return graphql.Null + } + if resTmp == nil { + return graphql.Null + } + res := resTmp.(*PortfolioSnapshot) + fc.Result = res + return ec.marshalOPortfolioSnapshot2ᚖgithubᚗcomᚋoxistoᚋmoneyᚑgopherᚋgraphᚐPortfolioSnapshot(ctx, field.Selections, res) +} + +func (ec *executionContext) fieldContext_Portfolio_snapshot(ctx context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { + fc = &graphql.FieldContext{ + Object: "Portfolio", + Field: field, + IsMethod: true, + IsResolver: true, + Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + switch field.Name { + case "time": + return ec.fieldContext_PortfolioSnapshot_time(ctx, field) + case "position": + return ec.fieldContext_PortfolioSnapshot_position(ctx, field) + } + return nil, fmt.Errorf("no field named %q was found under type PortfolioSnapshot", field.Name) }, } defer func() { @@ -1382,15 +1571,15 @@ func (ec *executionContext) fieldContext_Query_security(ctx context.Context, fie } }() ctx = graphql.WithFieldContext(ctx, fc) - if fc.Args, err = ec.field_Query_security_args(ctx, field.ArgumentMap(ec.Variables)); err != nil { + if fc.Args, err = ec.field_Portfolio_snapshot_args(ctx, field.ArgumentMap(ec.Variables)); err != nil { ec.Error(ctx, err) return fc, err } return fc, nil } -func (ec *executionContext) _Query_securities(ctx context.Context, field graphql.CollectedField) (ret graphql.Marshaler) { - fc, err := ec.fieldContext_Query_securities(ctx, field) +func (ec *executionContext) _PortfolioPosition_security(ctx context.Context, field graphql.CollectedField, obj *PortfolioPosition) (ret graphql.Marshaler) { + fc, err := ec.fieldContext_PortfolioPosition_security(ctx, field) if err != nil { return graphql.Null } @@ -1403,7 +1592,7 @@ func (ec *executionContext) _Query_securities(ctx context.Context, field graphql }() resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (any, error) { ctx = rctx // use context from middleware stack in children - return ec.resolvers.Query().Securities(rctx) + return obj.Security, nil }) if err != nil { ec.Error(ctx, err) @@ -1415,17 +1604,17 @@ func (ec *executionContext) _Query_securities(ctx context.Context, field graphql } return graphql.Null } - res := resTmp.([]*persistence.Security) + res := resTmp.(*persistence.Security) fc.Result = res - return ec.marshalNSecurity2ᚕᚖgithubᚗcomᚋoxistoᚋmoneyᚑgopherᚋpersistenceᚐSecurityᚄ(ctx, field.Selections, res) + return ec.marshalNSecurity2ᚖgithubᚗcomᚋoxistoᚋmoneyᚑgopherᚋpersistenceᚐSecurity(ctx, field.Selections, res) } -func (ec *executionContext) fieldContext_Query_securities(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { +func (ec *executionContext) fieldContext_PortfolioPosition_security(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { fc = &graphql.FieldContext{ - Object: "Query", + Object: "PortfolioPosition", Field: field, - IsMethod: true, - IsResolver: true, + IsMethod: false, + IsResolver: false, Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { switch field.Name { case "id": @@ -1443,8 +1632,8 @@ func (ec *executionContext) fieldContext_Query_securities(_ context.Context, fie return fc, nil } -func (ec *executionContext) _Query_portfolio(ctx context.Context, field graphql.CollectedField) (ret graphql.Marshaler) { - fc, err := ec.fieldContext_Query_portfolio(ctx, field) +func (ec *executionContext) _PortfolioPosition_quantity(ctx context.Context, field graphql.CollectedField, obj *PortfolioPosition) (ret graphql.Marshaler) { + fc, err := ec.fieldContext_PortfolioPosition_quantity(ctx, field) if err != nil { return graphql.Null } @@ -1457,52 +1646,38 @@ func (ec *executionContext) _Query_portfolio(ctx context.Context, field graphql. }() resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (any, error) { ctx = rctx // use context from middleware stack in children - return ec.resolvers.Query().Portfolio(rctx, fc.Args["id"].(string)) + return obj.Quantity, nil }) if err != nil { ec.Error(ctx, err) return graphql.Null } if resTmp == nil { + if !graphql.HasFieldError(ctx, fc) { + ec.Errorf(ctx, "must not be null") + } return graphql.Null } - res := resTmp.(*persistence.Portfolio) + res := resTmp.(int) fc.Result = res - return ec.marshalOPortfolio2ᚖgithubᚗcomᚋoxistoᚋmoneyᚑgopherᚋpersistenceᚐPortfolio(ctx, field.Selections, res) + return ec.marshalNInt2int(ctx, field.Selections, res) } -func (ec *executionContext) fieldContext_Query_portfolio(ctx context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { +func (ec *executionContext) fieldContext_PortfolioPosition_quantity(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { fc = &graphql.FieldContext{ - Object: "Query", + Object: "PortfolioPosition", Field: field, - IsMethod: true, - IsResolver: true, + IsMethod: false, + IsResolver: false, Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { - switch field.Name { - case "id": - return ec.fieldContext_Portfolio_id(ctx, field) - case "displayName": - return ec.fieldContext_Portfolio_displayName(ctx, field) - } - return nil, fmt.Errorf("no field named %q was found under type Portfolio", field.Name) + return nil, errors.New("field of type Int does not have child fields") }, } - defer func() { - if r := recover(); r != nil { - err = ec.Recover(ctx, r) - ec.Error(ctx, err) - } - }() - ctx = graphql.WithFieldContext(ctx, fc) - if fc.Args, err = ec.field_Query_portfolio_args(ctx, field.ArgumentMap(ec.Variables)); err != nil { - ec.Error(ctx, err) - return fc, err - } return fc, nil } -func (ec *executionContext) _Query_portfolios(ctx context.Context, field graphql.CollectedField) (ret graphql.Marshaler) { - fc, err := ec.fieldContext_Query_portfolios(ctx, field) +func (ec *executionContext) _PortfolioPosition_purchaseValue(ctx context.Context, field graphql.CollectedField, obj *PortfolioPosition) (ret graphql.Marshaler) { + fc, err := ec.fieldContext_PortfolioPosition_purchaseValue(ctx, field) if err != nil { return graphql.Null } @@ -1515,7 +1690,7 @@ func (ec *executionContext) _Query_portfolios(ctx context.Context, field graphql }() resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (any, error) { ctx = rctx // use context from middleware stack in children - return ec.resolvers.Query().Portfolios(rctx) + return obj.PurchaseValue, nil }) if err != nil { ec.Error(ctx, err) @@ -1527,32 +1702,32 @@ func (ec *executionContext) _Query_portfolios(ctx context.Context, field graphql } return graphql.Null } - res := resTmp.([]*persistence.Portfolio) + res := resTmp.(*persistence.Currency) fc.Result = res - return ec.marshalNPortfolio2ᚕᚖgithubᚗcomᚋoxistoᚋmoneyᚑgopherᚋpersistenceᚐPortfolioᚄ(ctx, field.Selections, res) + return ec.marshalNCurrency2ᚖgithubᚗcomᚋoxistoᚋmoneyᚑgopherᚋpersistenceᚐCurrency(ctx, field.Selections, res) } -func (ec *executionContext) fieldContext_Query_portfolios(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { +func (ec *executionContext) fieldContext_PortfolioPosition_purchaseValue(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { fc = &graphql.FieldContext{ - Object: "Query", + Object: "PortfolioPosition", Field: field, - IsMethod: true, - IsResolver: true, + IsMethod: false, + IsResolver: false, Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { switch field.Name { - case "id": - return ec.fieldContext_Portfolio_id(ctx, field) - case "displayName": - return ec.fieldContext_Portfolio_displayName(ctx, field) + case "value": + return ec.fieldContext_Currency_value(ctx, field) + case "symbol": + return ec.fieldContext_Currency_symbol(ctx, field) } - return nil, fmt.Errorf("no field named %q was found under type Portfolio", field.Name) + return nil, fmt.Errorf("no field named %q was found under type Currency", field.Name) }, } return fc, nil } -func (ec *executionContext) _Query___type(ctx context.Context, field graphql.CollectedField) (ret graphql.Marshaler) { - fc, err := ec.fieldContext_Query___type(ctx, field) +func (ec *executionContext) _PortfolioPosition_purchasePrice(ctx context.Context, field graphql.CollectedField, obj *PortfolioPosition) (ret graphql.Marshaler) { + fc, err := ec.fieldContext_PortfolioPosition_purchasePrice(ctx, field) if err != nil { return graphql.Null } @@ -1565,68 +1740,44 @@ func (ec *executionContext) _Query___type(ctx context.Context, field graphql.Col }() resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (any, error) { ctx = rctx // use context from middleware stack in children - return ec.introspectType(fc.Args["name"].(string)) + return obj.PurchasePrice, nil }) if err != nil { ec.Error(ctx, err) return graphql.Null } if resTmp == nil { + if !graphql.HasFieldError(ctx, fc) { + ec.Errorf(ctx, "must not be null") + } return graphql.Null } - res := resTmp.(*introspection.Type) + res := resTmp.(*persistence.Currency) fc.Result = res - return ec.marshalO__Type2ᚖgithubᚗcomᚋ99designsᚋgqlgenᚋgraphqlᚋintrospectionᚐType(ctx, field.Selections, res) + return ec.marshalNCurrency2ᚖgithubᚗcomᚋoxistoᚋmoneyᚑgopherᚋpersistenceᚐCurrency(ctx, field.Selections, res) } -func (ec *executionContext) fieldContext_Query___type(ctx context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { +func (ec *executionContext) fieldContext_PortfolioPosition_purchasePrice(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { fc = &graphql.FieldContext{ - Object: "Query", + Object: "PortfolioPosition", Field: field, - IsMethod: true, + IsMethod: false, IsResolver: false, Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { switch field.Name { - case "kind": - return ec.fieldContext___Type_kind(ctx, field) - case "name": - return ec.fieldContext___Type_name(ctx, field) - case "description": - return ec.fieldContext___Type_description(ctx, field) - case "fields": - return ec.fieldContext___Type_fields(ctx, field) - case "interfaces": - return ec.fieldContext___Type_interfaces(ctx, field) - case "possibleTypes": - return ec.fieldContext___Type_possibleTypes(ctx, field) - case "enumValues": - return ec.fieldContext___Type_enumValues(ctx, field) - case "inputFields": - return ec.fieldContext___Type_inputFields(ctx, field) - case "ofType": - return ec.fieldContext___Type_ofType(ctx, field) - case "specifiedByURL": - return ec.fieldContext___Type_specifiedByURL(ctx, field) + case "value": + return ec.fieldContext_Currency_value(ctx, field) + case "symbol": + return ec.fieldContext_Currency_symbol(ctx, field) } - return nil, fmt.Errorf("no field named %q was found under type __Type", field.Name) + return nil, fmt.Errorf("no field named %q was found under type Currency", field.Name) }, } - defer func() { - if r := recover(); r != nil { - err = ec.Recover(ctx, r) - ec.Error(ctx, err) - } - }() - ctx = graphql.WithFieldContext(ctx, fc) - if fc.Args, err = ec.field_Query___type_args(ctx, field.ArgumentMap(ec.Variables)); err != nil { - ec.Error(ctx, err) - return fc, err - } return fc, nil } -func (ec *executionContext) _Query___schema(ctx context.Context, field graphql.CollectedField) (ret graphql.Marshaler) { - fc, err := ec.fieldContext_Query___schema(ctx, field) +func (ec *executionContext) _PortfolioPosition_marketValue(ctx context.Context, field graphql.CollectedField, obj *PortfolioPosition) (ret graphql.Marshaler) { + fc, err := ec.fieldContext_PortfolioPosition_marketValue(ctx, field) if err != nil { return graphql.Null } @@ -1639,7 +1790,665 @@ func (ec *executionContext) _Query___schema(ctx context.Context, field graphql.C }() resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (any, error) { ctx = rctx // use context from middleware stack in children - return ec.introspectSchema() + return obj.MarketValue, nil + }) + if err != nil { + ec.Error(ctx, err) + return graphql.Null + } + if resTmp == nil { + if !graphql.HasFieldError(ctx, fc) { + ec.Errorf(ctx, "must not be null") + } + return graphql.Null + } + res := resTmp.(*persistence.Currency) + fc.Result = res + return ec.marshalNCurrency2ᚖgithubᚗcomᚋoxistoᚋmoneyᚑgopherᚋpersistenceᚐCurrency(ctx, field.Selections, res) +} + +func (ec *executionContext) fieldContext_PortfolioPosition_marketValue(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { + fc = &graphql.FieldContext{ + Object: "PortfolioPosition", + Field: field, + IsMethod: false, + IsResolver: false, + Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + switch field.Name { + case "value": + return ec.fieldContext_Currency_value(ctx, field) + case "symbol": + return ec.fieldContext_Currency_symbol(ctx, field) + } + return nil, fmt.Errorf("no field named %q was found under type Currency", field.Name) + }, + } + return fc, nil +} + +func (ec *executionContext) _PortfolioPosition_marketPrice(ctx context.Context, field graphql.CollectedField, obj *PortfolioPosition) (ret graphql.Marshaler) { + fc, err := ec.fieldContext_PortfolioPosition_marketPrice(ctx, field) + if err != nil { + return graphql.Null + } + ctx = graphql.WithFieldContext(ctx, fc) + defer func() { + if r := recover(); r != nil { + ec.Error(ctx, ec.Recover(ctx, r)) + ret = graphql.Null + } + }() + resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (any, error) { + ctx = rctx // use context from middleware stack in children + return obj.MarketPrice, nil + }) + if err != nil { + ec.Error(ctx, err) + return graphql.Null + } + if resTmp == nil { + if !graphql.HasFieldError(ctx, fc) { + ec.Errorf(ctx, "must not be null") + } + return graphql.Null + } + res := resTmp.(*persistence.Currency) + fc.Result = res + return ec.marshalNCurrency2ᚖgithubᚗcomᚋoxistoᚋmoneyᚑgopherᚋpersistenceᚐCurrency(ctx, field.Selections, res) +} + +func (ec *executionContext) fieldContext_PortfolioPosition_marketPrice(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { + fc = &graphql.FieldContext{ + Object: "PortfolioPosition", + Field: field, + IsMethod: false, + IsResolver: false, + Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + switch field.Name { + case "value": + return ec.fieldContext_Currency_value(ctx, field) + case "symbol": + return ec.fieldContext_Currency_symbol(ctx, field) + } + return nil, fmt.Errorf("no field named %q was found under type Currency", field.Name) + }, + } + return fc, nil +} + +func (ec *executionContext) _PortfolioPosition_totalFees(ctx context.Context, field graphql.CollectedField, obj *PortfolioPosition) (ret graphql.Marshaler) { + fc, err := ec.fieldContext_PortfolioPosition_totalFees(ctx, field) + if err != nil { + return graphql.Null + } + ctx = graphql.WithFieldContext(ctx, fc) + defer func() { + if r := recover(); r != nil { + ec.Error(ctx, ec.Recover(ctx, r)) + ret = graphql.Null + } + }() + resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (any, error) { + ctx = rctx // use context from middleware stack in children + return obj.TotalFees, nil + }) + if err != nil { + ec.Error(ctx, err) + return graphql.Null + } + if resTmp == nil { + if !graphql.HasFieldError(ctx, fc) { + ec.Errorf(ctx, "must not be null") + } + return graphql.Null + } + res := resTmp.(*persistence.Currency) + fc.Result = res + return ec.marshalNCurrency2ᚖgithubᚗcomᚋoxistoᚋmoneyᚑgopherᚋpersistenceᚐCurrency(ctx, field.Selections, res) +} + +func (ec *executionContext) fieldContext_PortfolioPosition_totalFees(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { + fc = &graphql.FieldContext{ + Object: "PortfolioPosition", + Field: field, + IsMethod: false, + IsResolver: false, + Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + switch field.Name { + case "value": + return ec.fieldContext_Currency_value(ctx, field) + case "symbol": + return ec.fieldContext_Currency_symbol(ctx, field) + } + return nil, fmt.Errorf("no field named %q was found under type Currency", field.Name) + }, + } + return fc, nil +} + +func (ec *executionContext) _PortfolioPosition_profitOrLoss(ctx context.Context, field graphql.CollectedField, obj *PortfolioPosition) (ret graphql.Marshaler) { + fc, err := ec.fieldContext_PortfolioPosition_profitOrLoss(ctx, field) + if err != nil { + return graphql.Null + } + ctx = graphql.WithFieldContext(ctx, fc) + defer func() { + if r := recover(); r != nil { + ec.Error(ctx, ec.Recover(ctx, r)) + ret = graphql.Null + } + }() + resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (any, error) { + ctx = rctx // use context from middleware stack in children + return obj.ProfitOrLoss, nil + }) + if err != nil { + ec.Error(ctx, err) + return graphql.Null + } + if resTmp == nil { + if !graphql.HasFieldError(ctx, fc) { + ec.Errorf(ctx, "must not be null") + } + return graphql.Null + } + res := resTmp.(*persistence.Currency) + fc.Result = res + return ec.marshalNCurrency2ᚖgithubᚗcomᚋoxistoᚋmoneyᚑgopherᚋpersistenceᚐCurrency(ctx, field.Selections, res) +} + +func (ec *executionContext) fieldContext_PortfolioPosition_profitOrLoss(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { + fc = &graphql.FieldContext{ + Object: "PortfolioPosition", + Field: field, + IsMethod: false, + IsResolver: false, + Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + switch field.Name { + case "value": + return ec.fieldContext_Currency_value(ctx, field) + case "symbol": + return ec.fieldContext_Currency_symbol(ctx, field) + } + return nil, fmt.Errorf("no field named %q was found under type Currency", field.Name) + }, + } + return fc, nil +} + +func (ec *executionContext) _PortfolioPosition_gains(ctx context.Context, field graphql.CollectedField, obj *PortfolioPosition) (ret graphql.Marshaler) { + fc, err := ec.fieldContext_PortfolioPosition_gains(ctx, field) + if err != nil { + return graphql.Null + } + ctx = graphql.WithFieldContext(ctx, fc) + defer func() { + if r := recover(); r != nil { + ec.Error(ctx, ec.Recover(ctx, r)) + ret = graphql.Null + } + }() + resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (any, error) { + ctx = rctx // use context from middleware stack in children + return obj.Gains, nil + }) + if err != nil { + ec.Error(ctx, err) + return graphql.Null + } + if resTmp == nil { + if !graphql.HasFieldError(ctx, fc) { + ec.Errorf(ctx, "must not be null") + } + return graphql.Null + } + res := resTmp.(float64) + fc.Result = res + return ec.marshalNFloat2float64(ctx, field.Selections, res) +} + +func (ec *executionContext) fieldContext_PortfolioPosition_gains(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { + fc = &graphql.FieldContext{ + Object: "PortfolioPosition", + Field: field, + IsMethod: false, + IsResolver: false, + Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + return nil, errors.New("field of type Float does not have child fields") + }, + } + return fc, nil +} + +func (ec *executionContext) _PortfolioSnapshot_time(ctx context.Context, field graphql.CollectedField, obj *PortfolioSnapshot) (ret graphql.Marshaler) { + fc, err := ec.fieldContext_PortfolioSnapshot_time(ctx, field) + if err != nil { + return graphql.Null + } + ctx = graphql.WithFieldContext(ctx, fc) + defer func() { + if r := recover(); r != nil { + ec.Error(ctx, ec.Recover(ctx, r)) + ret = graphql.Null + } + }() + resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (any, error) { + ctx = rctx // use context from middleware stack in children + return obj.Time, nil + }) + if err != nil { + ec.Error(ctx, err) + return graphql.Null + } + if resTmp == nil { + if !graphql.HasFieldError(ctx, fc) { + ec.Errorf(ctx, "must not be null") + } + return graphql.Null + } + res := resTmp.(string) + fc.Result = res + return ec.marshalNDate2string(ctx, field.Selections, res) +} + +func (ec *executionContext) fieldContext_PortfolioSnapshot_time(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { + fc = &graphql.FieldContext{ + Object: "PortfolioSnapshot", + Field: field, + IsMethod: false, + IsResolver: false, + Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + return nil, errors.New("field of type Date does not have child fields") + }, + } + return fc, nil +} + +func (ec *executionContext) _PortfolioSnapshot_position(ctx context.Context, field graphql.CollectedField, obj *PortfolioSnapshot) (ret graphql.Marshaler) { + fc, err := ec.fieldContext_PortfolioSnapshot_position(ctx, field) + if err != nil { + return graphql.Null + } + ctx = graphql.WithFieldContext(ctx, fc) + defer func() { + if r := recover(); r != nil { + ec.Error(ctx, ec.Recover(ctx, r)) + ret = graphql.Null + } + }() + resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (any, error) { + ctx = rctx // use context from middleware stack in children + return obj.Position, nil + }) + if err != nil { + ec.Error(ctx, err) + return graphql.Null + } + if resTmp == nil { + if !graphql.HasFieldError(ctx, fc) { + ec.Errorf(ctx, "must not be null") + } + return graphql.Null + } + res := resTmp.([]*PortfolioPosition) + fc.Result = res + return ec.marshalNPortfolioPosition2ᚕᚖgithubᚗcomᚋoxistoᚋmoneyᚑgopherᚋgraphᚐPortfolioPositionᚄ(ctx, field.Selections, res) +} + +func (ec *executionContext) fieldContext_PortfolioSnapshot_position(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { + fc = &graphql.FieldContext{ + Object: "PortfolioSnapshot", + Field: field, + IsMethod: false, + IsResolver: false, + Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + switch field.Name { + case "security": + return ec.fieldContext_PortfolioPosition_security(ctx, field) + case "quantity": + return ec.fieldContext_PortfolioPosition_quantity(ctx, field) + case "purchaseValue": + return ec.fieldContext_PortfolioPosition_purchaseValue(ctx, field) + case "purchasePrice": + return ec.fieldContext_PortfolioPosition_purchasePrice(ctx, field) + case "marketValue": + return ec.fieldContext_PortfolioPosition_marketValue(ctx, field) + case "marketPrice": + return ec.fieldContext_PortfolioPosition_marketPrice(ctx, field) + case "totalFees": + return ec.fieldContext_PortfolioPosition_totalFees(ctx, field) + case "profitOrLoss": + return ec.fieldContext_PortfolioPosition_profitOrLoss(ctx, field) + case "gains": + return ec.fieldContext_PortfolioPosition_gains(ctx, field) + } + return nil, fmt.Errorf("no field named %q was found under type PortfolioPosition", field.Name) + }, + } + return fc, nil +} + +func (ec *executionContext) _Query_security(ctx context.Context, field graphql.CollectedField) (ret graphql.Marshaler) { + fc, err := ec.fieldContext_Query_security(ctx, field) + if err != nil { + return graphql.Null + } + ctx = graphql.WithFieldContext(ctx, fc) + defer func() { + if r := recover(); r != nil { + ec.Error(ctx, ec.Recover(ctx, r)) + ret = graphql.Null + } + }() + resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (any, error) { + ctx = rctx // use context from middleware stack in children + return ec.resolvers.Query().Security(rctx, fc.Args["id"].(string)) + }) + if err != nil { + ec.Error(ctx, err) + return graphql.Null + } + if resTmp == nil { + return graphql.Null + } + res := resTmp.(*persistence.Security) + fc.Result = res + return ec.marshalOSecurity2ᚖgithubᚗcomᚋoxistoᚋmoneyᚑgopherᚋpersistenceᚐSecurity(ctx, field.Selections, res) +} + +func (ec *executionContext) fieldContext_Query_security(ctx context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { + fc = &graphql.FieldContext{ + Object: "Query", + Field: field, + IsMethod: true, + IsResolver: true, + Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + switch field.Name { + case "id": + return ec.fieldContext_Security_id(ctx, field) + case "displayName": + return ec.fieldContext_Security_displayName(ctx, field) + case "quoteProvider": + return ec.fieldContext_Security_quoteProvider(ctx, field) + case "listedAs": + return ec.fieldContext_Security_listedAs(ctx, field) + } + return nil, fmt.Errorf("no field named %q was found under type Security", field.Name) + }, + } + defer func() { + if r := recover(); r != nil { + err = ec.Recover(ctx, r) + ec.Error(ctx, err) + } + }() + ctx = graphql.WithFieldContext(ctx, fc) + if fc.Args, err = ec.field_Query_security_args(ctx, field.ArgumentMap(ec.Variables)); err != nil { + ec.Error(ctx, err) + return fc, err + } + return fc, nil +} + +func (ec *executionContext) _Query_securities(ctx context.Context, field graphql.CollectedField) (ret graphql.Marshaler) { + fc, err := ec.fieldContext_Query_securities(ctx, field) + if err != nil { + return graphql.Null + } + ctx = graphql.WithFieldContext(ctx, fc) + defer func() { + if r := recover(); r != nil { + ec.Error(ctx, ec.Recover(ctx, r)) + ret = graphql.Null + } + }() + resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (any, error) { + ctx = rctx // use context from middleware stack in children + return ec.resolvers.Query().Securities(rctx) + }) + if err != nil { + ec.Error(ctx, err) + return graphql.Null + } + if resTmp == nil { + if !graphql.HasFieldError(ctx, fc) { + ec.Errorf(ctx, "must not be null") + } + return graphql.Null + } + res := resTmp.([]*persistence.Security) + fc.Result = res + return ec.marshalNSecurity2ᚕᚖgithubᚗcomᚋoxistoᚋmoneyᚑgopherᚋpersistenceᚐSecurityᚄ(ctx, field.Selections, res) +} + +func (ec *executionContext) fieldContext_Query_securities(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { + fc = &graphql.FieldContext{ + Object: "Query", + Field: field, + IsMethod: true, + IsResolver: true, + Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + switch field.Name { + case "id": + return ec.fieldContext_Security_id(ctx, field) + case "displayName": + return ec.fieldContext_Security_displayName(ctx, field) + case "quoteProvider": + return ec.fieldContext_Security_quoteProvider(ctx, field) + case "listedAs": + return ec.fieldContext_Security_listedAs(ctx, field) + } + return nil, fmt.Errorf("no field named %q was found under type Security", field.Name) + }, + } + return fc, nil +} + +func (ec *executionContext) _Query_portfolio(ctx context.Context, field graphql.CollectedField) (ret graphql.Marshaler) { + fc, err := ec.fieldContext_Query_portfolio(ctx, field) + if err != nil { + return graphql.Null + } + ctx = graphql.WithFieldContext(ctx, fc) + defer func() { + if r := recover(); r != nil { + ec.Error(ctx, ec.Recover(ctx, r)) + ret = graphql.Null + } + }() + resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (any, error) { + ctx = rctx // use context from middleware stack in children + return ec.resolvers.Query().Portfolio(rctx, fc.Args["id"].(string)) + }) + if err != nil { + ec.Error(ctx, err) + return graphql.Null + } + if resTmp == nil { + return graphql.Null + } + res := resTmp.(*persistence.Portfolio) + fc.Result = res + return ec.marshalOPortfolio2ᚖgithubᚗcomᚋoxistoᚋmoneyᚑgopherᚋpersistenceᚐPortfolio(ctx, field.Selections, res) +} + +func (ec *executionContext) fieldContext_Query_portfolio(ctx context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { + fc = &graphql.FieldContext{ + Object: "Query", + Field: field, + IsMethod: true, + IsResolver: true, + Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + switch field.Name { + case "id": + return ec.fieldContext_Portfolio_id(ctx, field) + case "displayName": + return ec.fieldContext_Portfolio_displayName(ctx, field) + case "bankAccount": + return ec.fieldContext_Portfolio_bankAccount(ctx, field) + case "snapshot": + return ec.fieldContext_Portfolio_snapshot(ctx, field) + } + return nil, fmt.Errorf("no field named %q was found under type Portfolio", field.Name) + }, + } + defer func() { + if r := recover(); r != nil { + err = ec.Recover(ctx, r) + ec.Error(ctx, err) + } + }() + ctx = graphql.WithFieldContext(ctx, fc) + if fc.Args, err = ec.field_Query_portfolio_args(ctx, field.ArgumentMap(ec.Variables)); err != nil { + ec.Error(ctx, err) + return fc, err + } + return fc, nil +} + +func (ec *executionContext) _Query_portfolios(ctx context.Context, field graphql.CollectedField) (ret graphql.Marshaler) { + fc, err := ec.fieldContext_Query_portfolios(ctx, field) + if err != nil { + return graphql.Null + } + ctx = graphql.WithFieldContext(ctx, fc) + defer func() { + if r := recover(); r != nil { + ec.Error(ctx, ec.Recover(ctx, r)) + ret = graphql.Null + } + }() + resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (any, error) { + ctx = rctx // use context from middleware stack in children + return ec.resolvers.Query().Portfolios(rctx) + }) + if err != nil { + ec.Error(ctx, err) + return graphql.Null + } + if resTmp == nil { + if !graphql.HasFieldError(ctx, fc) { + ec.Errorf(ctx, "must not be null") + } + return graphql.Null + } + res := resTmp.([]*persistence.Portfolio) + fc.Result = res + return ec.marshalNPortfolio2ᚕᚖgithubᚗcomᚋoxistoᚋmoneyᚑgopherᚋpersistenceᚐPortfolioᚄ(ctx, field.Selections, res) +} + +func (ec *executionContext) fieldContext_Query_portfolios(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { + fc = &graphql.FieldContext{ + Object: "Query", + Field: field, + IsMethod: true, + IsResolver: true, + Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + switch field.Name { + case "id": + return ec.fieldContext_Portfolio_id(ctx, field) + case "displayName": + return ec.fieldContext_Portfolio_displayName(ctx, field) + case "bankAccount": + return ec.fieldContext_Portfolio_bankAccount(ctx, field) + case "snapshot": + return ec.fieldContext_Portfolio_snapshot(ctx, field) + } + return nil, fmt.Errorf("no field named %q was found under type Portfolio", field.Name) + }, + } + return fc, nil +} + +func (ec *executionContext) _Query___type(ctx context.Context, field graphql.CollectedField) (ret graphql.Marshaler) { + fc, err := ec.fieldContext_Query___type(ctx, field) + if err != nil { + return graphql.Null + } + ctx = graphql.WithFieldContext(ctx, fc) + defer func() { + if r := recover(); r != nil { + ec.Error(ctx, ec.Recover(ctx, r)) + ret = graphql.Null + } + }() + resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (any, error) { + ctx = rctx // use context from middleware stack in children + return ec.introspectType(fc.Args["name"].(string)) + }) + if err != nil { + ec.Error(ctx, err) + return graphql.Null + } + if resTmp == nil { + return graphql.Null + } + res := resTmp.(*introspection.Type) + fc.Result = res + return ec.marshalO__Type2ᚖgithubᚗcomᚋ99designsᚋgqlgenᚋgraphqlᚋintrospectionᚐType(ctx, field.Selections, res) +} + +func (ec *executionContext) fieldContext_Query___type(ctx context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { + fc = &graphql.FieldContext{ + Object: "Query", + Field: field, + IsMethod: true, + IsResolver: false, + Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + switch field.Name { + case "kind": + return ec.fieldContext___Type_kind(ctx, field) + case "name": + return ec.fieldContext___Type_name(ctx, field) + case "description": + return ec.fieldContext___Type_description(ctx, field) + case "fields": + return ec.fieldContext___Type_fields(ctx, field) + case "interfaces": + return ec.fieldContext___Type_interfaces(ctx, field) + case "possibleTypes": + return ec.fieldContext___Type_possibleTypes(ctx, field) + case "enumValues": + return ec.fieldContext___Type_enumValues(ctx, field) + case "inputFields": + return ec.fieldContext___Type_inputFields(ctx, field) + case "ofType": + return ec.fieldContext___Type_ofType(ctx, field) + case "specifiedByURL": + return ec.fieldContext___Type_specifiedByURL(ctx, field) + } + return nil, fmt.Errorf("no field named %q was found under type __Type", field.Name) + }, + } + defer func() { + if r := recover(); r != nil { + err = ec.Recover(ctx, r) + ec.Error(ctx, err) + } + }() + ctx = graphql.WithFieldContext(ctx, fc) + if fc.Args, err = ec.field_Query___type_args(ctx, field.ArgumentMap(ec.Variables)); err != nil { + ec.Error(ctx, err) + return fc, err + } + return fc, nil +} + +func (ec *executionContext) _Query___schema(ctx context.Context, field graphql.CollectedField) (ret graphql.Marshaler) { + fc, err := ec.fieldContext_Query___schema(ctx, field) + if err != nil { + return graphql.Null + } + ctx = graphql.WithFieldContext(ctx, fc) + defer func() { + if r := recover(); r != nil { + ec.Error(ctx, ec.Recover(ctx, r)) + ret = graphql.Null + } + }() + resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (any, error) { + ctx = rctx // use context from middleware stack in children + return ec.introspectSchema() }) if err != nil { ec.Error(ctx, err) @@ -3762,24 +4571,233 @@ func (ec *executionContext) _BankAccount(ctx context.Context, sel ast.SelectionS return out } -var currencyImplementors = []string{"Currency"} +var currencyImplementors = []string{"Currency"} + +func (ec *executionContext) _Currency(ctx context.Context, sel ast.SelectionSet, obj *persistence.Currency) graphql.Marshaler { + fields := graphql.CollectFields(ec.OperationContext, sel, currencyImplementors) + + out := graphql.NewFieldSet(fields) + deferred := make(map[string]*graphql.FieldSet) + for i, field := range fields { + switch field.Name { + case "__typename": + out.Values[i] = graphql.MarshalString("Currency") + case "value": + out.Values[i] = ec._Currency_value(ctx, field, obj) + if out.Values[i] == graphql.Null { + out.Invalids++ + } + case "symbol": + out.Values[i] = ec._Currency_symbol(ctx, field, obj) + if out.Values[i] == graphql.Null { + out.Invalids++ + } + default: + panic("unknown field " + strconv.Quote(field.Name)) + } + } + out.Dispatch(ctx) + if out.Invalids > 0 { + return graphql.Null + } + + atomic.AddInt32(&ec.deferred, int32(len(deferred))) + + for label, dfs := range deferred { + ec.processDeferredGroup(graphql.DeferredGroup{ + Label: label, + Path: graphql.GetPath(ctx), + FieldSet: dfs, + Context: ctx, + }) + } + + return out +} + +var listedSecurityImplementors = []string{"ListedSecurity"} + +func (ec *executionContext) _ListedSecurity(ctx context.Context, sel ast.SelectionSet, obj *persistence.ListedSecurity) graphql.Marshaler { + fields := graphql.CollectFields(ec.OperationContext, sel, listedSecurityImplementors) + + out := graphql.NewFieldSet(fields) + deferred := make(map[string]*graphql.FieldSet) + for i, field := range fields { + switch field.Name { + case "__typename": + out.Values[i] = graphql.MarshalString("ListedSecurity") + case "ticker": + out.Values[i] = ec._ListedSecurity_ticker(ctx, field, obj) + if out.Values[i] == graphql.Null { + atomic.AddUint32(&out.Invalids, 1) + } + case "currency": + out.Values[i] = ec._ListedSecurity_currency(ctx, field, obj) + if out.Values[i] == graphql.Null { + atomic.AddUint32(&out.Invalids, 1) + } + case "security": + field := field + + innerFunc := func(ctx context.Context, fs *graphql.FieldSet) (res graphql.Marshaler) { + defer func() { + if r := recover(); r != nil { + ec.Error(ctx, ec.Recover(ctx, r)) + } + }() + res = ec._ListedSecurity_security(ctx, field, obj) + if res == graphql.Null { + atomic.AddUint32(&fs.Invalids, 1) + } + return res + } + + if field.Deferrable != nil { + dfs, ok := deferred[field.Deferrable.Label] + di := 0 + if ok { + dfs.AddField(field) + di = len(dfs.Values) - 1 + } else { + dfs = graphql.NewFieldSet([]graphql.CollectedField{field}) + deferred[field.Deferrable.Label] = dfs + } + dfs.Concurrently(di, func(ctx context.Context) graphql.Marshaler { + return innerFunc(ctx, dfs) + }) + + // don't run the out.Concurrently() call below + out.Values[i] = graphql.Null + continue + } + + out.Concurrently(i, func(ctx context.Context) graphql.Marshaler { return innerFunc(ctx, out) }) + case "latestQuote": + field := field + + innerFunc := func(ctx context.Context, _ *graphql.FieldSet) (res graphql.Marshaler) { + defer func() { + if r := recover(); r != nil { + ec.Error(ctx, ec.Recover(ctx, r)) + } + }() + res = ec._ListedSecurity_latestQuote(ctx, field, obj) + return res + } + + if field.Deferrable != nil { + dfs, ok := deferred[field.Deferrable.Label] + di := 0 + if ok { + dfs.AddField(field) + di = len(dfs.Values) - 1 + } else { + dfs = graphql.NewFieldSet([]graphql.CollectedField{field}) + deferred[field.Deferrable.Label] = dfs + } + dfs.Concurrently(di, func(ctx context.Context) graphql.Marshaler { + return innerFunc(ctx, dfs) + }) + + // don't run the out.Concurrently() call below + out.Values[i] = graphql.Null + continue + } + + out.Concurrently(i, func(ctx context.Context) graphql.Marshaler { return innerFunc(ctx, out) }) + case "latestQuoteTimestamp": + field := field + + innerFunc := func(ctx context.Context, _ *graphql.FieldSet) (res graphql.Marshaler) { + defer func() { + if r := recover(); r != nil { + ec.Error(ctx, ec.Recover(ctx, r)) + } + }() + res = ec._ListedSecurity_latestQuoteTimestamp(ctx, field, obj) + return res + } + + if field.Deferrable != nil { + dfs, ok := deferred[field.Deferrable.Label] + di := 0 + if ok { + dfs.AddField(field) + di = len(dfs.Values) - 1 + } else { + dfs = graphql.NewFieldSet([]graphql.CollectedField{field}) + deferred[field.Deferrable.Label] = dfs + } + dfs.Concurrently(di, func(ctx context.Context) graphql.Marshaler { + return innerFunc(ctx, dfs) + }) + + // don't run the out.Concurrently() call below + out.Values[i] = graphql.Null + continue + } + + out.Concurrently(i, func(ctx context.Context) graphql.Marshaler { return innerFunc(ctx, out) }) + default: + panic("unknown field " + strconv.Quote(field.Name)) + } + } + out.Dispatch(ctx) + if out.Invalids > 0 { + return graphql.Null + } + + atomic.AddInt32(&ec.deferred, int32(len(deferred))) + + for label, dfs := range deferred { + ec.processDeferredGroup(graphql.DeferredGroup{ + Label: label, + Path: graphql.GetPath(ctx), + FieldSet: dfs, + Context: ctx, + }) + } + + return out +} + +var mutationImplementors = []string{"Mutation"} -func (ec *executionContext) _Currency(ctx context.Context, sel ast.SelectionSet, obj *persistence.Currency) graphql.Marshaler { - fields := graphql.CollectFields(ec.OperationContext, sel, currencyImplementors) +func (ec *executionContext) _Mutation(ctx context.Context, sel ast.SelectionSet) graphql.Marshaler { + fields := graphql.CollectFields(ec.OperationContext, sel, mutationImplementors) + ctx = graphql.WithFieldContext(ctx, &graphql.FieldContext{ + Object: "Mutation", + }) out := graphql.NewFieldSet(fields) deferred := make(map[string]*graphql.FieldSet) for i, field := range fields { + innerCtx := graphql.WithRootFieldContext(ctx, &graphql.RootFieldContext{ + Object: field.Name, + Field: field, + }) + switch field.Name { case "__typename": - out.Values[i] = graphql.MarshalString("Currency") - case "value": - out.Values[i] = ec._Currency_value(ctx, field, obj) + out.Values[i] = graphql.MarshalString("Mutation") + case "createSecurity": + out.Values[i] = ec.OperationContext.RootResolverMiddleware(innerCtx, func(ctx context.Context) (res graphql.Marshaler) { + return ec._Mutation_createSecurity(ctx, field) + }) if out.Values[i] == graphql.Null { out.Invalids++ } - case "symbol": - out.Values[i] = ec._Currency_symbol(ctx, field, obj) + case "updateSecurity": + out.Values[i] = ec.OperationContext.RootResolverMiddleware(innerCtx, func(ctx context.Context) (res graphql.Marshaler) { + return ec._Mutation_updateSecurity(ctx, field) + }) + if out.Values[i] == graphql.Null { + out.Invalids++ + } + case "triggerQuoteUpdate": + out.Values[i] = ec.OperationContext.RootResolverMiddleware(innerCtx, func(ctx context.Context) (res graphql.Marshaler) { + return ec._Mutation_triggerQuoteUpdate(ctx, field) + }) if out.Values[i] == graphql.Null { out.Invalids++ } @@ -3806,28 +4824,28 @@ func (ec *executionContext) _Currency(ctx context.Context, sel ast.SelectionSet, return out } -var listedSecurityImplementors = []string{"ListedSecurity"} +var portfolioImplementors = []string{"Portfolio"} -func (ec *executionContext) _ListedSecurity(ctx context.Context, sel ast.SelectionSet, obj *persistence.ListedSecurity) graphql.Marshaler { - fields := graphql.CollectFields(ec.OperationContext, sel, listedSecurityImplementors) +func (ec *executionContext) _Portfolio(ctx context.Context, sel ast.SelectionSet, obj *persistence.Portfolio) graphql.Marshaler { + fields := graphql.CollectFields(ec.OperationContext, sel, portfolioImplementors) out := graphql.NewFieldSet(fields) deferred := make(map[string]*graphql.FieldSet) for i, field := range fields { switch field.Name { case "__typename": - out.Values[i] = graphql.MarshalString("ListedSecurity") - case "ticker": - out.Values[i] = ec._ListedSecurity_ticker(ctx, field, obj) + out.Values[i] = graphql.MarshalString("Portfolio") + case "id": + out.Values[i] = ec._Portfolio_id(ctx, field, obj) if out.Values[i] == graphql.Null { atomic.AddUint32(&out.Invalids, 1) } - case "currency": - out.Values[i] = ec._ListedSecurity_currency(ctx, field, obj) + case "displayName": + out.Values[i] = ec._Portfolio_displayName(ctx, field, obj) if out.Values[i] == graphql.Null { atomic.AddUint32(&out.Invalids, 1) } - case "security": + case "bankAccount": field := field innerFunc := func(ctx context.Context, fs *graphql.FieldSet) (res graphql.Marshaler) { @@ -3836,7 +4854,7 @@ func (ec *executionContext) _ListedSecurity(ctx context.Context, sel ast.Selecti ec.Error(ctx, ec.Recover(ctx, r)) } }() - res = ec._ListedSecurity_security(ctx, field, obj) + res = ec._Portfolio_bankAccount(ctx, field, obj) if res == graphql.Null { atomic.AddUint32(&fs.Invalids, 1) } @@ -3863,40 +4881,7 @@ func (ec *executionContext) _ListedSecurity(ctx context.Context, sel ast.Selecti } out.Concurrently(i, func(ctx context.Context) graphql.Marshaler { return innerFunc(ctx, out) }) - case "latestQuote": - field := field - - innerFunc := func(ctx context.Context, _ *graphql.FieldSet) (res graphql.Marshaler) { - defer func() { - if r := recover(); r != nil { - ec.Error(ctx, ec.Recover(ctx, r)) - } - }() - res = ec._ListedSecurity_latestQuote(ctx, field, obj) - return res - } - - if field.Deferrable != nil { - dfs, ok := deferred[field.Deferrable.Label] - di := 0 - if ok { - dfs.AddField(field) - di = len(dfs.Values) - 1 - } else { - dfs = graphql.NewFieldSet([]graphql.CollectedField{field}) - deferred[field.Deferrable.Label] = dfs - } - dfs.Concurrently(di, func(ctx context.Context) graphql.Marshaler { - return innerFunc(ctx, dfs) - }) - - // don't run the out.Concurrently() call below - out.Values[i] = graphql.Null - continue - } - - out.Concurrently(i, func(ctx context.Context) graphql.Marshaler { return innerFunc(ctx, out) }) - case "latestQuoteTimestamp": + case "snapshot": field := field innerFunc := func(ctx context.Context, _ *graphql.FieldSet) (res graphql.Marshaler) { @@ -3905,7 +4890,7 @@ func (ec *executionContext) _ListedSecurity(ctx context.Context, sel ast.Selecti ec.Error(ctx, ec.Recover(ctx, r)) } }() - res = ec._ListedSecurity_latestQuoteTimestamp(ctx, field, obj) + res = ec._Portfolio_snapshot(ctx, field, obj) return res } @@ -3952,43 +4937,59 @@ func (ec *executionContext) _ListedSecurity(ctx context.Context, sel ast.Selecti return out } -var mutationImplementors = []string{"Mutation"} +var portfolioPositionImplementors = []string{"PortfolioPosition"} -func (ec *executionContext) _Mutation(ctx context.Context, sel ast.SelectionSet) graphql.Marshaler { - fields := graphql.CollectFields(ec.OperationContext, sel, mutationImplementors) - ctx = graphql.WithFieldContext(ctx, &graphql.FieldContext{ - Object: "Mutation", - }) +func (ec *executionContext) _PortfolioPosition(ctx context.Context, sel ast.SelectionSet, obj *PortfolioPosition) graphql.Marshaler { + fields := graphql.CollectFields(ec.OperationContext, sel, portfolioPositionImplementors) out := graphql.NewFieldSet(fields) deferred := make(map[string]*graphql.FieldSet) for i, field := range fields { - innerCtx := graphql.WithRootFieldContext(ctx, &graphql.RootFieldContext{ - Object: field.Name, - Field: field, - }) - switch field.Name { case "__typename": - out.Values[i] = graphql.MarshalString("Mutation") - case "createSecurity": - out.Values[i] = ec.OperationContext.RootResolverMiddleware(innerCtx, func(ctx context.Context) (res graphql.Marshaler) { - return ec._Mutation_createSecurity(ctx, field) - }) + out.Values[i] = graphql.MarshalString("PortfolioPosition") + case "security": + out.Values[i] = ec._PortfolioPosition_security(ctx, field, obj) if out.Values[i] == graphql.Null { out.Invalids++ } - case "updateSecurity": - out.Values[i] = ec.OperationContext.RootResolverMiddleware(innerCtx, func(ctx context.Context) (res graphql.Marshaler) { - return ec._Mutation_updateSecurity(ctx, field) - }) + case "quantity": + out.Values[i] = ec._PortfolioPosition_quantity(ctx, field, obj) if out.Values[i] == graphql.Null { out.Invalids++ } - case "triggerQuoteUpdate": - out.Values[i] = ec.OperationContext.RootResolverMiddleware(innerCtx, func(ctx context.Context) (res graphql.Marshaler) { - return ec._Mutation_triggerQuoteUpdate(ctx, field) - }) + case "purchaseValue": + out.Values[i] = ec._PortfolioPosition_purchaseValue(ctx, field, obj) + if out.Values[i] == graphql.Null { + out.Invalids++ + } + case "purchasePrice": + out.Values[i] = ec._PortfolioPosition_purchasePrice(ctx, field, obj) + if out.Values[i] == graphql.Null { + out.Invalids++ + } + case "marketValue": + out.Values[i] = ec._PortfolioPosition_marketValue(ctx, field, obj) + if out.Values[i] == graphql.Null { + out.Invalids++ + } + case "marketPrice": + out.Values[i] = ec._PortfolioPosition_marketPrice(ctx, field, obj) + if out.Values[i] == graphql.Null { + out.Invalids++ + } + case "totalFees": + out.Values[i] = ec._PortfolioPosition_totalFees(ctx, field, obj) + if out.Values[i] == graphql.Null { + out.Invalids++ + } + case "profitOrLoss": + out.Values[i] = ec._PortfolioPosition_profitOrLoss(ctx, field, obj) + if out.Values[i] == graphql.Null { + out.Invalids++ + } + case "gains": + out.Values[i] = ec._PortfolioPosition_gains(ctx, field, obj) if out.Values[i] == graphql.Null { out.Invalids++ } @@ -4015,24 +5016,24 @@ func (ec *executionContext) _Mutation(ctx context.Context, sel ast.SelectionSet) return out } -var portfolioImplementors = []string{"Portfolio"} +var portfolioSnapshotImplementors = []string{"PortfolioSnapshot"} -func (ec *executionContext) _Portfolio(ctx context.Context, sel ast.SelectionSet, obj *persistence.Portfolio) graphql.Marshaler { - fields := graphql.CollectFields(ec.OperationContext, sel, portfolioImplementors) +func (ec *executionContext) _PortfolioSnapshot(ctx context.Context, sel ast.SelectionSet, obj *PortfolioSnapshot) graphql.Marshaler { + fields := graphql.CollectFields(ec.OperationContext, sel, portfolioSnapshotImplementors) out := graphql.NewFieldSet(fields) deferred := make(map[string]*graphql.FieldSet) for i, field := range fields { switch field.Name { case "__typename": - out.Values[i] = graphql.MarshalString("Portfolio") - case "id": - out.Values[i] = ec._Portfolio_id(ctx, field, obj) + out.Values[i] = graphql.MarshalString("PortfolioSnapshot") + case "time": + out.Values[i] = ec._PortfolioSnapshot_time(ctx, field, obj) if out.Values[i] == graphql.Null { out.Invalids++ } - case "displayName": - out.Values[i] = ec._Portfolio_displayName(ctx, field, obj) + case "position": + out.Values[i] = ec._PortfolioSnapshot_position(ctx, field, obj) if out.Values[i] == graphql.Null { out.Invalids++ } @@ -4627,6 +5628,20 @@ func (ec *executionContext) ___Type(ctx context.Context, sel ast.SelectionSet, o // region ***************************** type.gotpl ***************************** +func (ec *executionContext) marshalNBankAccount2githubᚗcomᚋoxistoᚋmoneyᚑgopherᚋpersistenceᚐBankAccount(ctx context.Context, sel ast.SelectionSet, v persistence.BankAccount) graphql.Marshaler { + return ec._BankAccount(ctx, sel, &v) +} + +func (ec *executionContext) marshalNBankAccount2ᚖgithubᚗcomᚋoxistoᚋmoneyᚑgopherᚋpersistenceᚐBankAccount(ctx context.Context, sel ast.SelectionSet, v *persistence.BankAccount) graphql.Marshaler { + if v == nil { + if !graphql.HasFieldError(ctx, graphql.GetFieldContext(ctx)) { + ec.Errorf(ctx, "the requested element is null which the schema does not allow") + } + return graphql.Null + } + return ec._BankAccount(ctx, sel, v) +} + func (ec *executionContext) unmarshalNBoolean2bool(ctx context.Context, v any) (bool, error) { res, err := graphql.UnmarshalBoolean(v) return res, graphql.ErrorOnPath(ctx, err) @@ -4642,6 +5657,46 @@ func (ec *executionContext) marshalNBoolean2bool(ctx context.Context, sel ast.Se return res } +func (ec *executionContext) marshalNCurrency2ᚖgithubᚗcomᚋoxistoᚋmoneyᚑgopherᚋpersistenceᚐCurrency(ctx context.Context, sel ast.SelectionSet, v *persistence.Currency) graphql.Marshaler { + if v == nil { + if !graphql.HasFieldError(ctx, graphql.GetFieldContext(ctx)) { + ec.Errorf(ctx, "the requested element is null which the schema does not allow") + } + return graphql.Null + } + return ec._Currency(ctx, sel, v) +} + +func (ec *executionContext) unmarshalNDate2string(ctx context.Context, v any) (string, error) { + res, err := graphql.UnmarshalString(v) + return res, graphql.ErrorOnPath(ctx, err) +} + +func (ec *executionContext) marshalNDate2string(ctx context.Context, sel ast.SelectionSet, v string) graphql.Marshaler { + res := graphql.MarshalString(v) + if res == graphql.Null { + if !graphql.HasFieldError(ctx, graphql.GetFieldContext(ctx)) { + ec.Errorf(ctx, "the requested element is null which the schema does not allow") + } + } + return res +} + +func (ec *executionContext) unmarshalNFloat2float64(ctx context.Context, v any) (float64, error) { + res, err := graphql.UnmarshalFloatContext(ctx, v) + return res, graphql.ErrorOnPath(ctx, err) +} + +func (ec *executionContext) marshalNFloat2float64(ctx context.Context, sel ast.SelectionSet, v float64) graphql.Marshaler { + res := graphql.MarshalFloatContext(v) + if res == graphql.Null { + if !graphql.HasFieldError(ctx, graphql.GetFieldContext(ctx)) { + ec.Errorf(ctx, "the requested element is null which the schema does not allow") + } + } + return graphql.WrapContextMarshaler(ctx, res) +} + func (ec *executionContext) unmarshalNID2string(ctx context.Context, v any) (string, error) { res, err := graphql.UnmarshalID(v) return res, graphql.ErrorOnPath(ctx, err) @@ -4657,6 +5712,21 @@ func (ec *executionContext) marshalNID2string(ctx context.Context, sel ast.Selec return res } +func (ec *executionContext) unmarshalNInt2int(ctx context.Context, v any) (int, error) { + res, err := graphql.UnmarshalInt(v) + return res, graphql.ErrorOnPath(ctx, err) +} + +func (ec *executionContext) marshalNInt2int(ctx context.Context, sel ast.SelectionSet, v int) graphql.Marshaler { + res := graphql.MarshalInt(v) + if res == graphql.Null { + if !graphql.HasFieldError(ctx, graphql.GetFieldContext(ctx)) { + ec.Errorf(ctx, "the requested element is null which the schema does not allow") + } + } + return res +} + func (ec *executionContext) unmarshalNInt2int32(ctx context.Context, v any) (int32, error) { res, err := graphql.UnmarshalInt32(v) return res, graphql.ErrorOnPath(ctx, err) @@ -4741,6 +5811,60 @@ func (ec *executionContext) marshalNPortfolio2ᚖgithubᚗcomᚋoxistoᚋmoney return ec._Portfolio(ctx, sel, v) } +func (ec *executionContext) marshalNPortfolioPosition2ᚕᚖgithubᚗcomᚋoxistoᚋmoneyᚑgopherᚋgraphᚐPortfolioPositionᚄ(ctx context.Context, sel ast.SelectionSet, v []*PortfolioPosition) graphql.Marshaler { + ret := make(graphql.Array, len(v)) + var wg sync.WaitGroup + isLen1 := len(v) == 1 + if !isLen1 { + wg.Add(len(v)) + } + for i := range v { + i := i + fc := &graphql.FieldContext{ + Index: &i, + Result: &v[i], + } + ctx := graphql.WithFieldContext(ctx, fc) + f := func(i int) { + defer func() { + if r := recover(); r != nil { + ec.Error(ctx, ec.Recover(ctx, r)) + ret = nil + } + }() + if !isLen1 { + defer wg.Done() + } + ret[i] = ec.marshalNPortfolioPosition2ᚖgithubᚗcomᚋoxistoᚋmoneyᚑgopherᚋgraphᚐPortfolioPosition(ctx, sel, v[i]) + } + if isLen1 { + f(i) + } else { + go f(i) + } + + } + wg.Wait() + + for _, e := range ret { + if e == graphql.Null { + return graphql.Null + } + } + + return ret +} + +func (ec *executionContext) marshalNPortfolioPosition2ᚖgithubᚗcomᚋoxistoᚋmoneyᚑgopherᚋgraphᚐPortfolioPosition(ctx context.Context, sel ast.SelectionSet, v *PortfolioPosition) graphql.Marshaler { + if v == nil { + if !graphql.HasFieldError(ctx, graphql.GetFieldContext(ctx)) { + ec.Errorf(ctx, "the requested element is null which the schema does not allow") + } + return graphql.Null + } + return ec._PortfolioPosition(ctx, sel, v) +} + func (ec *executionContext) marshalNSecurity2githubᚗcomᚋoxistoᚋmoneyᚑgopherᚋpersistenceᚐSecurity(ctx context.Context, sel ast.SelectionSet, v persistence.Security) graphql.Marshaler { return ec._Security(ctx, sel, &v) } @@ -5195,6 +6319,13 @@ func (ec *executionContext) marshalOPortfolio2ᚖgithubᚗcomᚋoxistoᚋmoney return ec._Portfolio(ctx, sel, v) } +func (ec *executionContext) marshalOPortfolioSnapshot2ᚖgithubᚗcomᚋoxistoᚋmoneyᚑgopherᚋgraphᚐPortfolioSnapshot(ctx context.Context, sel ast.SelectionSet, v *PortfolioSnapshot) graphql.Marshaler { + if v == nil { + return graphql.Null + } + return ec._PortfolioSnapshot(ctx, sel, v) +} + func (ec *executionContext) marshalOSecurity2ᚖgithubᚗcomᚋoxistoᚋmoneyᚑgopherᚋpersistenceᚐSecurity(ctx context.Context, sel ast.SelectionSet, v *persistence.Security) graphql.Marshaler { if v == nil { return graphql.Null diff --git a/graph/models_gen.go b/graph/models_gen.go index ffbaa5e3..e25ef230 100644 --- a/graph/models_gen.go +++ b/graph/models_gen.go @@ -2,6 +2,10 @@ package graph +import ( + "github.com/oxisto/money-gopher/persistence" +) + type ListedSecurityInput struct { Ticker string `json:"ticker"` Currency string `json:"currency"` @@ -10,6 +14,35 @@ type ListedSecurityInput struct { type Mutation struct { } +type PortfolioPosition struct { + Security *persistence.Security `json:"security"` + Quantity int `json:"quantity"` + // PurchaseValue was the market value of this position when it was bought (net; + // exclusive of any fees). + PurchaseValue *persistence.Currency `json:"purchaseValue"` + // PurchasePrice was the market price of this position when it was bought (net; + // exclusive of any fees). + PurchasePrice *persistence.Currency `json:"purchasePrice"` + // MarketValue is the current market value of this position, as retrieved from + // the securities service. + MarketValue *persistence.Currency `json:"marketValue"` + // MarketPrice is the current market price of this position, as retrieved from + // the securities service. + MarketPrice *persistence.Currency `json:"marketPrice"` + // TotalFees is the total amount of fees accumulating in this position through + // various transactions. + TotalFees *persistence.Currency `json:"totalFees"` + // ProfitOrLoss contains the absolute amount of profit or loss in this position. + ProfitOrLoss *persistence.Currency `json:"profitOrLoss"` + // Gains contains the relative amount of profit or loss in this position. + Gains float64 `json:"gains"` +} + +type PortfolioSnapshot struct { + Time string `json:"time"` + Position []*PortfolioPosition `json:"position"` +} + type Query struct { } diff --git a/graph/schema.graphqls b/graph/schema.graphqls index 3aaec502..e30ae9cd 100644 --- a/graph/schema.graphqls +++ b/graph/schema.graphqls @@ -23,6 +23,59 @@ type ListedSecurity { type Portfolio { id: String! displayName: String! + bankAccount: BankAccount! + snapshot(time: Date!): PortfolioSnapshot + events: [PortfolioEvent!]! +} + +type PortfolioSnapshot { + time: Date! + position: [PortfolioPosition!]! +} + +type PortfolioPosition { + security: Security! + quantity: Int! + + """ + PurchaseValue was the market value of this position when it was bought (net; + exclusive of any fees). + """ + purchaseValue: Currency! + + """ + PurchasePrice was the market price of this position when it was bought (net; + exclusive of any fees). + """ + purchasePrice: Currency! + + """ + MarketValue is the current market value of this position, as retrieved from + the securities service. + """ + marketValue: Currency! + + """ + MarketPrice is the current market price of this position, as retrieved from + the securities service. + """ + marketPrice: Currency! + + """ + TotalFees is the total amount of fees accumulating in this position through + various transactions. + """ + totalFees: Currency! + + """ + ProfitOrLoss contains the absolute amount of profit or loss in this position. + """ + profitOrLoss: Currency! + + """ + Gains contains the relative amount of profit or loss in this position. + """ + gains: Float! } type BankAccount { diff --git a/graph/schema.resolvers.go b/graph/schema.resolvers.go index cb2475f4..46445cc9 100644 --- a/graph/schema.resolvers.go +++ b/graph/schema.resolvers.go @@ -6,6 +6,7 @@ package graph import ( "context" + "fmt" "slices" "time" @@ -126,6 +127,16 @@ func (r *mutationResolver) TriggerQuoteUpdate(ctx context.Context, securityIDs [ return true, nil } +// BankAccount is the resolver for the bankAccount field. +func (r *portfolioResolver) BankAccount(ctx context.Context, obj *persistence.Portfolio) (*persistence.BankAccount, error) { + return r.DB.GetBankAccount(ctx, obj.BankAccountID) +} + +// Snapshot is the resolver for the snapshot field. +func (r *portfolioResolver) Snapshot(ctx context.Context, obj *persistence.Portfolio, time string) (*PortfolioSnapshot, error) { + panic(fmt.Errorf("not implemented: Snapshot - snapshot")) +} + // Security is the resolver for the security field. func (r *queryResolver) Security(ctx context.Context, id string) (*persistence.Security, error) { return r.DB.GetSecurity(ctx, id) @@ -166,6 +177,9 @@ func (r *Resolver) ListedSecurity() ListedSecurityResolver { return &listedSecur // Mutation returns MutationResolver implementation. func (r *Resolver) Mutation() MutationResolver { return &mutationResolver{r} } +// Portfolio returns PortfolioResolver implementation. +func (r *Resolver) Portfolio() PortfolioResolver { return &portfolioResolver{r} } + // Query returns QueryResolver implementation. func (r *Resolver) Query() QueryResolver { return &queryResolver{r} } @@ -174,5 +188,6 @@ func (r *Resolver) Security() SecurityResolver { return &securityResolver{r} } type listedSecurityResolver struct{ *Resolver } type mutationResolver struct{ *Resolver } +type portfolioResolver struct{ *Resolver } type queryResolver struct{ *Resolver } type securityResolver struct{ *Resolver } diff --git a/persistence/models.go b/persistence/models.go index 7904a75e..ee77081b 100644 --- a/persistence/models.go +++ b/persistence/models.go @@ -6,6 +6,7 @@ package persistence import ( "database/sql" + "time" ) type BankAccount struct { @@ -38,6 +39,21 @@ type Portfolio struct { BankAccountID string } +type PortfolioEvent struct { + ID string + Type int64 + Time time.Time + PortfolioID string + SecurityID string + Amount sql.NullFloat64 + Price sql.NullInt64 + PriceCurrency sql.NullString + Fees sql.NullInt64 + FeesCurrency sql.NullString + Taxes sql.NullInt64 + TaxesCurrency sql.NullString +} + // Security represents a security that can be traded on an exchange. type Security struct { // ID is the primary identifier for a security. diff --git a/persistence/portfolios.sql.go b/persistence/portfolios.sql.go index 70371ac0..f8cd096b 100644 --- a/persistence/portfolios.sql.go +++ b/persistence/portfolios.sql.go @@ -28,6 +28,22 @@ func (q *Queries) CreateBankAccount(ctx context.Context, arg CreateBankAccountPa return &i, err } +const getBankAccount = `-- name: GetBankAccount :one +SELECT + id, display_name +FROM + bank_accounts +WHERE + id = ? +` + +func (q *Queries) GetBankAccount(ctx context.Context, id string) (*BankAccount, error) { + row := q.db.QueryRowContext(ctx, getBankAccount, id) + var i BankAccount + err := row.Scan(&i.ID, &i.DisplayName) + return &i, err +} + const getPortfolio = `-- name: GetPortfolio :one SELECT id, display_name, bank_account_id diff --git a/persistence/sql/migrations/0002_create_portfolio.sql b/persistence/sql/migrations/0002_create_portfolio.sql index 001ce224..9e27024d 100644 --- a/persistence/sql/migrations/0002_create_portfolio.sql +++ b/persistence/sql/migrations/0002_create_portfolio.sql @@ -7,6 +7,22 @@ CREATE TABLE FOREIGN KEY (bank_account_id) REFERENCES bank_accounts (id) ON DELETE RESTRICT ); +CREATE TABLE + IF NOT EXISTS portfolio_events ( + id TEXT PRIMARY KEY, + type INTEGER NOT NULL, + time DATETIME NOT NULL, + portfolio_id TEXT NOT NULL, + security_id TEXT NOT NULL, + amount REAL, + price INTEGER, + price_currency TEXT, + fees INTEGER, + fees_currency TEXT, + taxes INTEGER, + taxes_currency TEXT + ); + CREATE TABLE IF NOT EXISTS bank_accounts ( id TEXT PRIMARY KEY, -- ID is the primary identifier for a bank account. @@ -16,4 +32,6 @@ CREATE TABLE -- +goose Down DROP TABLE portfolios; +DROP TABLE portfolio_events; + DROP TABLE bank_accounts; \ No newline at end of file diff --git a/persistence/sql/queries/portfolios.sql b/persistence/sql/queries/portfolios.sql index 51c50e52..4d23617f 100644 --- a/persistence/sql/queries/portfolios.sql +++ b/persistence/sql/queries/portfolios.sql @@ -14,6 +14,14 @@ FROM ORDER BY id; +-- name: GetBankAccount :one +SELECT + * +FROM + bank_accounts +WHERE + id = ?; + -- name: CreateBankAccount :one INSERT INTO bank_accounts (id, display_name) diff --git a/service/portfolio/snapshot.go b/service/portfolio/snapshot.go index 897b1876..b69996f6 100644 --- a/service/portfolio/snapshot.go +++ b/service/portfolio/snapshot.go @@ -19,6 +19,8 @@ package portfolio import ( "context" "fmt" + "maps" + "slices" moneygopher "github.com/oxisto/money-gopher" "github.com/oxisto/money-gopher/finance" @@ -66,7 +68,7 @@ func (svc *service) GetPortfolioSnapshot(ctx context.Context, req *connect.Reque // Retrieve the event map; a map of events indexed by their security ID m = p.EventMap() - names = keys(m) + names = slices.Collect(maps.Keys(m)) // Retrieve market value of filtered securities secres, err = svc.securities.ListSecurities( @@ -149,17 +151,6 @@ func marketPrice(secmap map[string]*portfoliov1.Security, name string, netPrice } } -// TODO(oxisto): remove once maps.Keys is in the stdlib in Go 1.22 -func keys[M ~map[K]V, K comparable, V any](m M) (keys []K) { - keys = make([]K, 0, len(m)) - - for k := range m { - keys = append(keys, k) - } - - return keys -} - // forwardAuth uses the authorization header of [authenticatedReq] to // authenticate [req]. This is a little workaround, until we have proper // service-to-service authentication. From b741b3e7d19fe349268421ca0e0615132bc167aa Mon Sep 17 00:00:00 2001 From: Christian Banse Date: Sat, 28 Dec 2024 10:21:55 +0100 Subject: [PATCH 11/35] ++ --- finance/snapshot.go | 127 ++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 127 insertions(+) create mode 100644 finance/snapshot.go diff --git a/finance/snapshot.go b/finance/snapshot.go new file mode 100644 index 00000000..a9239153 --- /dev/null +++ b/finance/snapshot.go @@ -0,0 +1,127 @@ +package finance + +import ( + "context" + "fmt" + "maps" + "slices" + "time" + + "connectrpc.com/connect" + moneygopher "github.com/oxisto/money-gopher" + portfoliov1 "github.com/oxisto/money-gopher/gen" + "github.com/oxisto/money-gopher/graph" + "github.com/oxisto/money-gopher/persistence" + "google.golang.org/protobuf/types/known/timestamppb" +) + +func BuildSnapshot(time time.Time, db *persistence.DB) (*graph.PortfolioSnapshot, error) { + var ( + snap *portfoliov1.PortfolioSnapshot + p portfoliov1.Portfolio + m map[string][]*portfoliov1.PortfolioEvent + names []string + secres *connect.Response[portfoliov1.ListSecuritiesResponse] + secmap map[string]*portfoliov1.Security + ) + + // Retrieve transactions + p.Events, err = svc.events.List(req.Msg.PortfolioId) + if err != nil { + return nil, connect.NewError(connect.CodeInternal, err) + } + + // If no time is specified, we assume it to be now + if req.Msg.Time == nil { + req.Msg.Time = timestamppb.Now() + } + + // Set up the snapshot + snap = &portfoliov1.PortfolioSnapshot{ + Time: req.Msg.Time, + Positions: make(map[string]*portfoliov1.PortfolioPosition), + TotalPurchaseValue: portfoliov1.Zero(), + TotalMarketValue: portfoliov1.Zero(), + TotalProfitOrLoss: portfoliov1.Zero(), + Cash: portfoliov1.Zero(), + } + + // Record the first transaction time + if len(p.Events) > 0 { + snap.FirstTransactionTime = p.Events[0].Time + } + + // Retrieve the event map; a map of events indexed by their security ID + m = p.EventMap() + names = slices.Collect(maps.Keys(m)) + + // Retrieve market value of filtered securities + secres, err = svc.securities.ListSecurities( + context.Background(), + forwardAuth(connect.NewRequest(&portfoliov1.ListSecuritiesRequest{ + Filter: &portfoliov1.ListSecuritiesRequest_Filter{ + SecurityIds: names, + }, + }), req), + ) + if err != nil { + return nil, connect.NewError(connect.CodeInternal, + fmt.Errorf("internal error while calling ListSecurities on securities service: %w", err), + ) + } + + // Make a map out of the securities list so we can access it easier + secmap = moneygopher.Map(secres.Msg.Securities, func(s *portfoliov1.Security) string { + return s.Id + }) + + // We need to look at the portfolio events up to the time of the snapshot + // and calculate the current positions. + for name, txs := range m { + txs = portfoliov1.EventsBefore(txs, snap.Time.AsTime()) + + c := finance.NewCalculation(txs) + + if name == "cash" { + // Add deposited/withdrawn cash directly + snap.Cash.PlusAssign(c.Cash) + continue + } + + if c.Amount == 0 { + continue + } + + // Also add cash that is part of a securities' transaction (e.g., sell/buy) + snap.Cash.PlusAssign(c.Cash) + + pos := &portfoliov1.PortfolioPosition{ + Security: secmap[name], + Amount: c.Amount, + PurchaseValue: c.NetValue(), + PurchasePrice: c.NetPrice(), + MarketValue: portfoliov1.Times(marketPrice(secmap, name, c.NetPrice()), c.Amount), + MarketPrice: marketPrice(secmap, name, c.NetPrice()), + } + + // Calculate loss and gains + pos.ProfitOrLoss = portfoliov1.Minus(pos.MarketValue, pos.PurchaseValue) + pos.Gains = float64(portfoliov1.Minus(pos.MarketValue, pos.PurchaseValue).Value) / float64(pos.PurchaseValue.Value) + + // Add to total value(s) + snap.TotalPurchaseValue.PlusAssign(pos.PurchaseValue) + snap.TotalMarketValue.PlusAssign(pos.MarketValue) + snap.TotalProfitOrLoss.PlusAssign(pos.ProfitOrLoss) + + // Store position in map + snap.Positions[name] = pos + } + + // Calculate total gains + snap.TotalGains = float64(portfoliov1.Minus(snap.TotalMarketValue, snap.TotalPurchaseValue).Value) / float64(snap.TotalPurchaseValue.Value) + + // Calculate total portfolio value + snap.TotalPortfolioValue = snap.TotalMarketValue.Plus(snap.Cash) + + return connect.NewResponse(snap), nil +} From 8e0d2238992ab04dd40a1a8d88ba71ad5aa6fa4c Mon Sep 17 00:00:00 2001 From: Christian Banse Date: Sun, 29 Dec 2024 11:05:05 +0100 Subject: [PATCH 12/35] Converting++ --- finance/snapshot.go | 89 +-- gqlgen.yml | 4 +- graph/generated.go | 733 ++++++++++++++++++++++--- graph/schema.graphqls | 18 +- graph/schema.resolvers.go | 45 +- {graph => models}/models_gen.go | 55 +- persistence/portfolios.sql.go | 45 ++ persistence/securities.sql.go | 45 ++ persistence/sql/queries/portfolios.sql | 8 + persistence/sql/queries/securities.sql | 10 + 10 files changed, 942 insertions(+), 110 deletions(-) rename {graph => models}/models_gen.go (57%) diff --git a/finance/snapshot.go b/finance/snapshot.go index a9239153..0c6618a8 100644 --- a/finance/snapshot.go +++ b/finance/snapshot.go @@ -10,60 +10,68 @@ import ( "connectrpc.com/connect" moneygopher "github.com/oxisto/money-gopher" portfoliov1 "github.com/oxisto/money-gopher/gen" - "github.com/oxisto/money-gopher/graph" + "github.com/oxisto/money-gopher/models" "github.com/oxisto/money-gopher/persistence" - "google.golang.org/protobuf/types/known/timestamppb" ) -func BuildSnapshot(time time.Time, db *persistence.DB) (*graph.PortfolioSnapshot, error) { +// SnapshotDataProvider is an interface that provides the necessary data for +// building a snapshot. It includes methods for retrieving portfolio events and +// securities by their IDs. +type SnapshotDataProvider interface { + ListPortfolioEventsByPortfolioID(ctx context.Context, portfolioID string) ([]*persistence.PortfolioEvent, error) + ListSecuritiesByIDs(ctx context.Context, ids []string) ([]*persistence.Security, error) +} + +// BuildSnapshot creates a snapshot of the portfolio at a given time. It +// calculates the performance and market value of the current positions and the +// total value of the portfolio. +// +// The snapshot is built by retrieving all events and security information from +// a [SnapshotDataProvider]. The snapshot is built by iterating over the events +// and calculating the positions at the specified timestamp. +func BuildSnapshot( + ctx context.Context, + timestamp *time.Time, + portfolioID string, + provider SnapshotDataProvider, +) (snap *models.PortfolioSnapshot, err error) { var ( - snap *portfoliov1.PortfolioSnapshot + events []*persistence.PortfolioEvent p portfoliov1.Portfolio m map[string][]*portfoliov1.PortfolioEvent - names []string - secres *connect.Response[portfoliov1.ListSecuritiesResponse] - secmap map[string]*portfoliov1.Security + ids []string + secs []*persistence.Security + secmap map[string]*persistence.Security ) - // Retrieve transactions - p.Events, err = svc.events.List(req.Msg.PortfolioId) + // Retrieve events + events, err = provider.ListPortfolioEventsByPortfolioID(ctx, portfolioID) if err != nil { return nil, connect.NewError(connect.CodeInternal, err) } - // If no time is specified, we assume it to be now - if req.Msg.Time == nil { - req.Msg.Time = timestamppb.Now() - } - // Set up the snapshot - snap = &portfoliov1.PortfolioSnapshot{ - Time: req.Msg.Time, - Positions: make(map[string]*portfoliov1.PortfolioPosition), - TotalPurchaseValue: portfoliov1.Zero(), + snap = &models.PortfolioSnapshot{ + Time: timestamp.Format(time.RFC3339), + Positions: make([]*models.PortfolioPosition, 0), + /*TotalPurchaseValue: portfoliov1.Zero(), TotalMarketValue: portfoliov1.Zero(), TotalProfitOrLoss: portfoliov1.Zero(), - Cash: portfoliov1.Zero(), + Cash: portfoliov1.Zero(),*/ } // Record the first transaction time if len(p.Events) > 0 { - snap.FirstTransactionTime = p.Events[0].Time + snap.FirstTransactionTime = events[0].Time.Format(time.RFC3339) } // Retrieve the event map; a map of events indexed by their security ID m = p.EventMap() - names = slices.Collect(maps.Keys(m)) + ids = slices.Collect(maps.Keys(m)) // Retrieve market value of filtered securities - secres, err = svc.securities.ListSecurities( - context.Background(), - forwardAuth(connect.NewRequest(&portfoliov1.ListSecuritiesRequest{ - Filter: &portfoliov1.ListSecuritiesRequest_Filter{ - SecurityIds: names, - }, - }), req), - ) + secs, err = provider.ListSecuritiesByIDs(context.Background(), ids) + if err != nil { return nil, connect.NewError(connect.CodeInternal, fmt.Errorf("internal error while calling ListSecurities on securities service: %w", err), @@ -71,16 +79,16 @@ func BuildSnapshot(time time.Time, db *persistence.DB) (*graph.PortfolioSnapshot } // Make a map out of the securities list so we can access it easier - secmap = moneygopher.Map(secres.Msg.Securities, func(s *portfoliov1.Security) string { - return s.Id + secmap = moneygopher.Map(secs, func(s *persistence.Security) string { + return s.ID }) // We need to look at the portfolio events up to the time of the snapshot // and calculate the current positions. for name, txs := range m { - txs = portfoliov1.EventsBefore(txs, snap.Time.AsTime()) + txs = eventsBefore(txs, timestamp) - c := finance.NewCalculation(txs) + c := NewCalculation(txs) if name == "cash" { // Add deposited/withdrawn cash directly @@ -125,3 +133,18 @@ func BuildSnapshot(time time.Time, db *persistence.DB) (*graph.PortfolioSnapshot return connect.NewResponse(snap), nil } + +// TODO: move to SQL query +func eventsBefore(events []*persistence.PortfolioEvent, t time.Time) (out []*persistence.PortfolioEvent) { + out = make([]*persistence.PortfolioEvent, 0, len(events)) + + for _, event := range events { + if event.Time.After(t) { + continue + } + + out = append(out, event) + } + + return +} diff --git a/gqlgen.yml b/gqlgen.yml index 6063bda4..a69e038a 100644 --- a/gqlgen.yml +++ b/gqlgen.yml @@ -27,8 +27,8 @@ exec: # Where should any generated models go? model: - filename: graph/models_gen.go - package: graph + filename: models/models_gen.go + package: models # Optional: Pass in a path to a new gotpl template to use for generating the models # model_template: [your/path/model.gotpl] diff --git a/graph/generated.go b/graph/generated.go index c6780f6a..a21849e4 100644 --- a/graph/generated.go +++ b/graph/generated.go @@ -14,6 +14,7 @@ import ( "github.com/99designs/gqlgen/graphql" "github.com/99designs/gqlgen/graphql/introspection" + "github.com/oxisto/money-gopher/models" "github.com/oxisto/money-gopher/persistence" gqlparser "github.com/vektah/gqlparser/v2" "github.com/vektah/gqlparser/v2/ast" @@ -42,6 +43,7 @@ type ResolverRoot interface { ListedSecurity() ListedSecurityResolver Mutation() MutationResolver Portfolio() PortfolioResolver + PortfolioEvent() PortfolioEventResolver Query() QueryResolver Security() SecurityResolver } @@ -69,16 +71,23 @@ type ComplexityRoot struct { } Mutation struct { - CreateSecurity func(childComplexity int, input SecurityInput) int + CreateSecurity func(childComplexity int, input models.SecurityInput) int TriggerQuoteUpdate func(childComplexity int, securityIDs []string) int - UpdateSecurity func(childComplexity int, id string, input SecurityInput) int + UpdateSecurity func(childComplexity int, id string, input models.SecurityInput) int } Portfolio struct { BankAccount func(childComplexity int) int DisplayName func(childComplexity int) int + Events func(childComplexity int) int ID func(childComplexity int) int - Snapshot func(childComplexity int, time string) int + Snapshot func(childComplexity int, when string) int + } + + PortfolioEvent struct { + Security func(childComplexity int) int + Time func(childComplexity int) int + Type func(childComplexity int) int } PortfolioPosition struct { @@ -94,8 +103,10 @@ type ComplexityRoot struct { } PortfolioSnapshot struct { - Position func(childComplexity int) int - Time func(childComplexity int) int + Cash func(childComplexity int) int + FirstTransactionTime func(childComplexity int) int + Positions func(childComplexity int) int + Time func(childComplexity int) int } Query struct { @@ -119,13 +130,19 @@ type ListedSecurityResolver interface { LatestQuoteTimestamp(ctx context.Context, obj *persistence.ListedSecurity) (*string, error) } type MutationResolver interface { - CreateSecurity(ctx context.Context, input SecurityInput) (*persistence.Security, error) - UpdateSecurity(ctx context.Context, id string, input SecurityInput) (*persistence.Security, error) + CreateSecurity(ctx context.Context, input models.SecurityInput) (*persistence.Security, error) + UpdateSecurity(ctx context.Context, id string, input models.SecurityInput) (*persistence.Security, error) TriggerQuoteUpdate(ctx context.Context, securityIDs []string) (bool, error) } type PortfolioResolver interface { BankAccount(ctx context.Context, obj *persistence.Portfolio) (*persistence.BankAccount, error) - Snapshot(ctx context.Context, obj *persistence.Portfolio, time string) (*PortfolioSnapshot, error) + Snapshot(ctx context.Context, obj *persistence.Portfolio, when string) (*models.PortfolioSnapshot, error) + Events(ctx context.Context, obj *persistence.Portfolio) ([]*persistence.PortfolioEvent, error) +} +type PortfolioEventResolver interface { + Time(ctx context.Context, obj *persistence.PortfolioEvent) (string, error) + Type(ctx context.Context, obj *persistence.PortfolioEvent) (models.PortfolioEventType, error) + Security(ctx context.Context, obj *persistence.PortfolioEvent) (*persistence.Security, error) } type QueryResolver interface { Security(ctx context.Context, id string) (*persistence.Security, error) @@ -230,7 +247,7 @@ func (e *executableSchema) Complexity(typeName, field string, childComplexity in return 0, false } - return e.complexity.Mutation.CreateSecurity(childComplexity, args["input"].(SecurityInput)), true + return e.complexity.Mutation.CreateSecurity(childComplexity, args["input"].(models.SecurityInput)), true case "Mutation.triggerQuoteUpdate": if e.complexity.Mutation.TriggerQuoteUpdate == nil { @@ -254,7 +271,7 @@ func (e *executableSchema) Complexity(typeName, field string, childComplexity in return 0, false } - return e.complexity.Mutation.UpdateSecurity(childComplexity, args["id"].(string), args["input"].(SecurityInput)), true + return e.complexity.Mutation.UpdateSecurity(childComplexity, args["id"].(string), args["input"].(models.SecurityInput)), true case "Portfolio.bankAccount": if e.complexity.Portfolio.BankAccount == nil { @@ -270,6 +287,13 @@ func (e *executableSchema) Complexity(typeName, field string, childComplexity in return e.complexity.Portfolio.DisplayName(childComplexity), true + case "Portfolio.events": + if e.complexity.Portfolio.Events == nil { + break + } + + return e.complexity.Portfolio.Events(childComplexity), true + case "Portfolio.id": if e.complexity.Portfolio.ID == nil { break @@ -287,7 +311,28 @@ func (e *executableSchema) Complexity(typeName, field string, childComplexity in return 0, false } - return e.complexity.Portfolio.Snapshot(childComplexity, args["time"].(string)), true + return e.complexity.Portfolio.Snapshot(childComplexity, args["when"].(string)), true + + case "PortfolioEvent.security": + if e.complexity.PortfolioEvent.Security == nil { + break + } + + return e.complexity.PortfolioEvent.Security(childComplexity), true + + case "PortfolioEvent.time": + if e.complexity.PortfolioEvent.Time == nil { + break + } + + return e.complexity.PortfolioEvent.Time(childComplexity), true + + case "PortfolioEvent.type": + if e.complexity.PortfolioEvent.Type == nil { + break + } + + return e.complexity.PortfolioEvent.Type(childComplexity), true case "PortfolioPosition.gains": if e.complexity.PortfolioPosition.Gains == nil { @@ -352,12 +397,26 @@ func (e *executableSchema) Complexity(typeName, field string, childComplexity in return e.complexity.PortfolioPosition.TotalFees(childComplexity), true - case "PortfolioSnapshot.position": - if e.complexity.PortfolioSnapshot.Position == nil { + case "PortfolioSnapshot.cash": + if e.complexity.PortfolioSnapshot.Cash == nil { + break + } + + return e.complexity.PortfolioSnapshot.Cash(childComplexity), true + + case "PortfolioSnapshot.firstTransactionTime": + if e.complexity.PortfolioSnapshot.FirstTransactionTime == nil { + break + } + + return e.complexity.PortfolioSnapshot.FirstTransactionTime(childComplexity), true + + case "PortfolioSnapshot.positions": + if e.complexity.PortfolioSnapshot.Positions == nil { break } - return e.complexity.PortfolioSnapshot.Position(childComplexity), true + return e.complexity.PortfolioSnapshot.Positions(childComplexity), true case "PortfolioSnapshot.time": if e.complexity.PortfolioSnapshot.Time == nil { @@ -571,13 +630,13 @@ func (ec *executionContext) field_Mutation_createSecurity_args(ctx context.Conte func (ec *executionContext) field_Mutation_createSecurity_argsInput( ctx context.Context, rawArgs map[string]any, -) (SecurityInput, error) { +) (models.SecurityInput, error) { ctx = graphql.WithPathContext(ctx, graphql.NewPathWithField("input")) if tmp, ok := rawArgs["input"]; ok { - return ec.unmarshalNSecurityInput2githubᚗcomᚋoxistoᚋmoneyᚑgopherᚋgraphᚐSecurityInput(ctx, tmp) + return ec.unmarshalNSecurityInput2githubᚗcomᚋoxistoᚋmoneyᚑgopherᚋmodelsᚐSecurityInput(ctx, tmp) } - var zeroVal SecurityInput + var zeroVal models.SecurityInput return zeroVal, nil } @@ -635,32 +694,32 @@ func (ec *executionContext) field_Mutation_updateSecurity_argsID( func (ec *executionContext) field_Mutation_updateSecurity_argsInput( ctx context.Context, rawArgs map[string]any, -) (SecurityInput, error) { +) (models.SecurityInput, error) { ctx = graphql.WithPathContext(ctx, graphql.NewPathWithField("input")) if tmp, ok := rawArgs["input"]; ok { - return ec.unmarshalNSecurityInput2githubᚗcomᚋoxistoᚋmoneyᚑgopherᚋgraphᚐSecurityInput(ctx, tmp) + return ec.unmarshalNSecurityInput2githubᚗcomᚋoxistoᚋmoneyᚑgopherᚋmodelsᚐSecurityInput(ctx, tmp) } - var zeroVal SecurityInput + var zeroVal models.SecurityInput return zeroVal, nil } func (ec *executionContext) field_Portfolio_snapshot_args(ctx context.Context, rawArgs map[string]any) (map[string]any, error) { var err error args := map[string]any{} - arg0, err := ec.field_Portfolio_snapshot_argsTime(ctx, rawArgs) + arg0, err := ec.field_Portfolio_snapshot_argsWhen(ctx, rawArgs) if err != nil { return nil, err } - args["time"] = arg0 + args["when"] = arg0 return args, nil } -func (ec *executionContext) field_Portfolio_snapshot_argsTime( +func (ec *executionContext) field_Portfolio_snapshot_argsWhen( ctx context.Context, rawArgs map[string]any, ) (string, error) { - ctx = graphql.WithPathContext(ctx, graphql.NewPathWithField("time")) - if tmp, ok := rawArgs["time"]; ok { + ctx = graphql.WithPathContext(ctx, graphql.NewPathWithField("when")) + if tmp, ok := rawArgs["when"]; ok { return ec.unmarshalNDate2string(ctx, tmp) } @@ -1211,7 +1270,7 @@ func (ec *executionContext) _Mutation_createSecurity(ctx context.Context, field }() resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (any, error) { ctx = rctx // use context from middleware stack in children - return ec.resolvers.Mutation().CreateSecurity(rctx, fc.Args["input"].(SecurityInput)) + return ec.resolvers.Mutation().CreateSecurity(rctx, fc.Args["input"].(models.SecurityInput)) }) if err != nil { ec.Error(ctx, err) @@ -1276,7 +1335,7 @@ func (ec *executionContext) _Mutation_updateSecurity(ctx context.Context, field }() resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (any, error) { ctx = rctx // use context from middleware stack in children - return ec.resolvers.Mutation().UpdateSecurity(rctx, fc.Args["id"].(string), fc.Args["input"].(SecurityInput)) + return ec.resolvers.Mutation().UpdateSecurity(rctx, fc.Args["id"].(string), fc.Args["input"].(models.SecurityInput)) }) if err != nil { ec.Error(ctx, err) @@ -1534,7 +1593,7 @@ func (ec *executionContext) _Portfolio_snapshot(ctx context.Context, field graph }() resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (any, error) { ctx = rctx // use context from middleware stack in children - return ec.resolvers.Portfolio().Snapshot(rctx, obj, fc.Args["time"].(string)) + return ec.resolvers.Portfolio().Snapshot(rctx, obj, fc.Args["when"].(string)) }) if err != nil { ec.Error(ctx, err) @@ -1543,9 +1602,9 @@ func (ec *executionContext) _Portfolio_snapshot(ctx context.Context, field graph if resTmp == nil { return graphql.Null } - res := resTmp.(*PortfolioSnapshot) + res := resTmp.(*models.PortfolioSnapshot) fc.Result = res - return ec.marshalOPortfolioSnapshot2ᚖgithubᚗcomᚋoxistoᚋmoneyᚑgopherᚋgraphᚐPortfolioSnapshot(ctx, field.Selections, res) + return ec.marshalOPortfolioSnapshot2ᚖgithubᚗcomᚋoxistoᚋmoneyᚑgopherᚋmodelsᚐPortfolioSnapshot(ctx, field.Selections, res) } func (ec *executionContext) fieldContext_Portfolio_snapshot(ctx context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { @@ -1558,8 +1617,12 @@ func (ec *executionContext) fieldContext_Portfolio_snapshot(ctx context.Context, switch field.Name { case "time": return ec.fieldContext_PortfolioSnapshot_time(ctx, field) - case "position": - return ec.fieldContext_PortfolioSnapshot_position(ctx, field) + case "positions": + return ec.fieldContext_PortfolioSnapshot_positions(ctx, field) + case "firstTransactionTime": + return ec.fieldContext_PortfolioSnapshot_firstTransactionTime(ctx, field) + case "cash": + return ec.fieldContext_PortfolioSnapshot_cash(ctx, field) } return nil, fmt.Errorf("no field named %q was found under type PortfolioSnapshot", field.Name) }, @@ -1578,7 +1641,198 @@ func (ec *executionContext) fieldContext_Portfolio_snapshot(ctx context.Context, return fc, nil } -func (ec *executionContext) _PortfolioPosition_security(ctx context.Context, field graphql.CollectedField, obj *PortfolioPosition) (ret graphql.Marshaler) { +func (ec *executionContext) _Portfolio_events(ctx context.Context, field graphql.CollectedField, obj *persistence.Portfolio) (ret graphql.Marshaler) { + fc, err := ec.fieldContext_Portfolio_events(ctx, field) + if err != nil { + return graphql.Null + } + ctx = graphql.WithFieldContext(ctx, fc) + defer func() { + if r := recover(); r != nil { + ec.Error(ctx, ec.Recover(ctx, r)) + ret = graphql.Null + } + }() + resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (any, error) { + ctx = rctx // use context from middleware stack in children + return ec.resolvers.Portfolio().Events(rctx, obj) + }) + if err != nil { + ec.Error(ctx, err) + return graphql.Null + } + if resTmp == nil { + if !graphql.HasFieldError(ctx, fc) { + ec.Errorf(ctx, "must not be null") + } + return graphql.Null + } + res := resTmp.([]*persistence.PortfolioEvent) + fc.Result = res + return ec.marshalNPortfolioEvent2ᚕᚖgithubᚗcomᚋoxistoᚋmoneyᚑgopherᚋpersistenceᚐPortfolioEventᚄ(ctx, field.Selections, res) +} + +func (ec *executionContext) fieldContext_Portfolio_events(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { + fc = &graphql.FieldContext{ + Object: "Portfolio", + Field: field, + IsMethod: true, + IsResolver: true, + Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + switch field.Name { + case "time": + return ec.fieldContext_PortfolioEvent_time(ctx, field) + case "type": + return ec.fieldContext_PortfolioEvent_type(ctx, field) + case "security": + return ec.fieldContext_PortfolioEvent_security(ctx, field) + } + return nil, fmt.Errorf("no field named %q was found under type PortfolioEvent", field.Name) + }, + } + return fc, nil +} + +func (ec *executionContext) _PortfolioEvent_time(ctx context.Context, field graphql.CollectedField, obj *persistence.PortfolioEvent) (ret graphql.Marshaler) { + fc, err := ec.fieldContext_PortfolioEvent_time(ctx, field) + if err != nil { + return graphql.Null + } + ctx = graphql.WithFieldContext(ctx, fc) + defer func() { + if r := recover(); r != nil { + ec.Error(ctx, ec.Recover(ctx, r)) + ret = graphql.Null + } + }() + resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (any, error) { + ctx = rctx // use context from middleware stack in children + return ec.resolvers.PortfolioEvent().Time(rctx, obj) + }) + if err != nil { + ec.Error(ctx, err) + return graphql.Null + } + if resTmp == nil { + if !graphql.HasFieldError(ctx, fc) { + ec.Errorf(ctx, "must not be null") + } + return graphql.Null + } + res := resTmp.(string) + fc.Result = res + return ec.marshalNDate2string(ctx, field.Selections, res) +} + +func (ec *executionContext) fieldContext_PortfolioEvent_time(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { + fc = &graphql.FieldContext{ + Object: "PortfolioEvent", + Field: field, + IsMethod: true, + IsResolver: true, + Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + return nil, errors.New("field of type Date does not have child fields") + }, + } + return fc, nil +} + +func (ec *executionContext) _PortfolioEvent_type(ctx context.Context, field graphql.CollectedField, obj *persistence.PortfolioEvent) (ret graphql.Marshaler) { + fc, err := ec.fieldContext_PortfolioEvent_type(ctx, field) + if err != nil { + return graphql.Null + } + ctx = graphql.WithFieldContext(ctx, fc) + defer func() { + if r := recover(); r != nil { + ec.Error(ctx, ec.Recover(ctx, r)) + ret = graphql.Null + } + }() + resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (any, error) { + ctx = rctx // use context from middleware stack in children + return ec.resolvers.PortfolioEvent().Type(rctx, obj) + }) + if err != nil { + ec.Error(ctx, err) + return graphql.Null + } + if resTmp == nil { + if !graphql.HasFieldError(ctx, fc) { + ec.Errorf(ctx, "must not be null") + } + return graphql.Null + } + res := resTmp.(models.PortfolioEventType) + fc.Result = res + return ec.marshalNPortfolioEventType2githubᚗcomᚋoxistoᚋmoneyᚑgopherᚋmodelsᚐPortfolioEventType(ctx, field.Selections, res) +} + +func (ec *executionContext) fieldContext_PortfolioEvent_type(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { + fc = &graphql.FieldContext{ + Object: "PortfolioEvent", + Field: field, + IsMethod: true, + IsResolver: true, + Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + return nil, errors.New("field of type PortfolioEventType does not have child fields") + }, + } + return fc, nil +} + +func (ec *executionContext) _PortfolioEvent_security(ctx context.Context, field graphql.CollectedField, obj *persistence.PortfolioEvent) (ret graphql.Marshaler) { + fc, err := ec.fieldContext_PortfolioEvent_security(ctx, field) + if err != nil { + return graphql.Null + } + ctx = graphql.WithFieldContext(ctx, fc) + defer func() { + if r := recover(); r != nil { + ec.Error(ctx, ec.Recover(ctx, r)) + ret = graphql.Null + } + }() + resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (any, error) { + ctx = rctx // use context from middleware stack in children + return ec.resolvers.PortfolioEvent().Security(rctx, obj) + }) + if err != nil { + ec.Error(ctx, err) + return graphql.Null + } + if resTmp == nil { + return graphql.Null + } + res := resTmp.(*persistence.Security) + fc.Result = res + return ec.marshalOSecurity2ᚖgithubᚗcomᚋoxistoᚋmoneyᚑgopherᚋpersistenceᚐSecurity(ctx, field.Selections, res) +} + +func (ec *executionContext) fieldContext_PortfolioEvent_security(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { + fc = &graphql.FieldContext{ + Object: "PortfolioEvent", + Field: field, + IsMethod: true, + IsResolver: true, + Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + switch field.Name { + case "id": + return ec.fieldContext_Security_id(ctx, field) + case "displayName": + return ec.fieldContext_Security_displayName(ctx, field) + case "quoteProvider": + return ec.fieldContext_Security_quoteProvider(ctx, field) + case "listedAs": + return ec.fieldContext_Security_listedAs(ctx, field) + } + return nil, fmt.Errorf("no field named %q was found under type Security", field.Name) + }, + } + return fc, nil +} + +func (ec *executionContext) _PortfolioPosition_security(ctx context.Context, field graphql.CollectedField, obj *models.PortfolioPosition) (ret graphql.Marshaler) { fc, err := ec.fieldContext_PortfolioPosition_security(ctx, field) if err != nil { return graphql.Null @@ -1632,7 +1886,7 @@ func (ec *executionContext) fieldContext_PortfolioPosition_security(_ context.Co return fc, nil } -func (ec *executionContext) _PortfolioPosition_quantity(ctx context.Context, field graphql.CollectedField, obj *PortfolioPosition) (ret graphql.Marshaler) { +func (ec *executionContext) _PortfolioPosition_quantity(ctx context.Context, field graphql.CollectedField, obj *models.PortfolioPosition) (ret graphql.Marshaler) { fc, err := ec.fieldContext_PortfolioPosition_quantity(ctx, field) if err != nil { return graphql.Null @@ -1676,7 +1930,7 @@ func (ec *executionContext) fieldContext_PortfolioPosition_quantity(_ context.Co return fc, nil } -func (ec *executionContext) _PortfolioPosition_purchaseValue(ctx context.Context, field graphql.CollectedField, obj *PortfolioPosition) (ret graphql.Marshaler) { +func (ec *executionContext) _PortfolioPosition_purchaseValue(ctx context.Context, field graphql.CollectedField, obj *models.PortfolioPosition) (ret graphql.Marshaler) { fc, err := ec.fieldContext_PortfolioPosition_purchaseValue(ctx, field) if err != nil { return graphql.Null @@ -1726,7 +1980,7 @@ func (ec *executionContext) fieldContext_PortfolioPosition_purchaseValue(_ conte return fc, nil } -func (ec *executionContext) _PortfolioPosition_purchasePrice(ctx context.Context, field graphql.CollectedField, obj *PortfolioPosition) (ret graphql.Marshaler) { +func (ec *executionContext) _PortfolioPosition_purchasePrice(ctx context.Context, field graphql.CollectedField, obj *models.PortfolioPosition) (ret graphql.Marshaler) { fc, err := ec.fieldContext_PortfolioPosition_purchasePrice(ctx, field) if err != nil { return graphql.Null @@ -1776,7 +2030,7 @@ func (ec *executionContext) fieldContext_PortfolioPosition_purchasePrice(_ conte return fc, nil } -func (ec *executionContext) _PortfolioPosition_marketValue(ctx context.Context, field graphql.CollectedField, obj *PortfolioPosition) (ret graphql.Marshaler) { +func (ec *executionContext) _PortfolioPosition_marketValue(ctx context.Context, field graphql.CollectedField, obj *models.PortfolioPosition) (ret graphql.Marshaler) { fc, err := ec.fieldContext_PortfolioPosition_marketValue(ctx, field) if err != nil { return graphql.Null @@ -1826,7 +2080,7 @@ func (ec *executionContext) fieldContext_PortfolioPosition_marketValue(_ context return fc, nil } -func (ec *executionContext) _PortfolioPosition_marketPrice(ctx context.Context, field graphql.CollectedField, obj *PortfolioPosition) (ret graphql.Marshaler) { +func (ec *executionContext) _PortfolioPosition_marketPrice(ctx context.Context, field graphql.CollectedField, obj *models.PortfolioPosition) (ret graphql.Marshaler) { fc, err := ec.fieldContext_PortfolioPosition_marketPrice(ctx, field) if err != nil { return graphql.Null @@ -1876,7 +2130,7 @@ func (ec *executionContext) fieldContext_PortfolioPosition_marketPrice(_ context return fc, nil } -func (ec *executionContext) _PortfolioPosition_totalFees(ctx context.Context, field graphql.CollectedField, obj *PortfolioPosition) (ret graphql.Marshaler) { +func (ec *executionContext) _PortfolioPosition_totalFees(ctx context.Context, field graphql.CollectedField, obj *models.PortfolioPosition) (ret graphql.Marshaler) { fc, err := ec.fieldContext_PortfolioPosition_totalFees(ctx, field) if err != nil { return graphql.Null @@ -1926,7 +2180,7 @@ func (ec *executionContext) fieldContext_PortfolioPosition_totalFees(_ context.C return fc, nil } -func (ec *executionContext) _PortfolioPosition_profitOrLoss(ctx context.Context, field graphql.CollectedField, obj *PortfolioPosition) (ret graphql.Marshaler) { +func (ec *executionContext) _PortfolioPosition_profitOrLoss(ctx context.Context, field graphql.CollectedField, obj *models.PortfolioPosition) (ret graphql.Marshaler) { fc, err := ec.fieldContext_PortfolioPosition_profitOrLoss(ctx, field) if err != nil { return graphql.Null @@ -1976,7 +2230,7 @@ func (ec *executionContext) fieldContext_PortfolioPosition_profitOrLoss(_ contex return fc, nil } -func (ec *executionContext) _PortfolioPosition_gains(ctx context.Context, field graphql.CollectedField, obj *PortfolioPosition) (ret graphql.Marshaler) { +func (ec *executionContext) _PortfolioPosition_gains(ctx context.Context, field graphql.CollectedField, obj *models.PortfolioPosition) (ret graphql.Marshaler) { fc, err := ec.fieldContext_PortfolioPosition_gains(ctx, field) if err != nil { return graphql.Null @@ -2020,7 +2274,7 @@ func (ec *executionContext) fieldContext_PortfolioPosition_gains(_ context.Conte return fc, nil } -func (ec *executionContext) _PortfolioSnapshot_time(ctx context.Context, field graphql.CollectedField, obj *PortfolioSnapshot) (ret graphql.Marshaler) { +func (ec *executionContext) _PortfolioSnapshot_time(ctx context.Context, field graphql.CollectedField, obj *models.PortfolioSnapshot) (ret graphql.Marshaler) { fc, err := ec.fieldContext_PortfolioSnapshot_time(ctx, field) if err != nil { return graphql.Null @@ -2064,8 +2318,8 @@ func (ec *executionContext) fieldContext_PortfolioSnapshot_time(_ context.Contex return fc, nil } -func (ec *executionContext) _PortfolioSnapshot_position(ctx context.Context, field graphql.CollectedField, obj *PortfolioSnapshot) (ret graphql.Marshaler) { - fc, err := ec.fieldContext_PortfolioSnapshot_position(ctx, field) +func (ec *executionContext) _PortfolioSnapshot_positions(ctx context.Context, field graphql.CollectedField, obj *models.PortfolioSnapshot) (ret graphql.Marshaler) { + fc, err := ec.fieldContext_PortfolioSnapshot_positions(ctx, field) if err != nil { return graphql.Null } @@ -2078,7 +2332,7 @@ func (ec *executionContext) _PortfolioSnapshot_position(ctx context.Context, fie }() resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (any, error) { ctx = rctx // use context from middleware stack in children - return obj.Position, nil + return obj.Positions, nil }) if err != nil { ec.Error(ctx, err) @@ -2090,12 +2344,12 @@ func (ec *executionContext) _PortfolioSnapshot_position(ctx context.Context, fie } return graphql.Null } - res := resTmp.([]*PortfolioPosition) + res := resTmp.([]*models.PortfolioPosition) fc.Result = res - return ec.marshalNPortfolioPosition2ᚕᚖgithubᚗcomᚋoxistoᚋmoneyᚑgopherᚋgraphᚐPortfolioPositionᚄ(ctx, field.Selections, res) + return ec.marshalNPortfolioPosition2ᚕᚖgithubᚗcomᚋoxistoᚋmoneyᚑgopherᚋmodelsᚐPortfolioPositionᚄ(ctx, field.Selections, res) } -func (ec *executionContext) fieldContext_PortfolioSnapshot_position(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { +func (ec *executionContext) fieldContext_PortfolioSnapshot_positions(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { fc = &graphql.FieldContext{ Object: "PortfolioSnapshot", Field: field, @@ -2128,6 +2382,100 @@ func (ec *executionContext) fieldContext_PortfolioSnapshot_position(_ context.Co return fc, nil } +func (ec *executionContext) _PortfolioSnapshot_firstTransactionTime(ctx context.Context, field graphql.CollectedField, obj *models.PortfolioSnapshot) (ret graphql.Marshaler) { + fc, err := ec.fieldContext_PortfolioSnapshot_firstTransactionTime(ctx, field) + if err != nil { + return graphql.Null + } + ctx = graphql.WithFieldContext(ctx, fc) + defer func() { + if r := recover(); r != nil { + ec.Error(ctx, ec.Recover(ctx, r)) + ret = graphql.Null + } + }() + resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (any, error) { + ctx = rctx // use context from middleware stack in children + return obj.FirstTransactionTime, nil + }) + if err != nil { + ec.Error(ctx, err) + return graphql.Null + } + if resTmp == nil { + if !graphql.HasFieldError(ctx, fc) { + ec.Errorf(ctx, "must not be null") + } + return graphql.Null + } + res := resTmp.(string) + fc.Result = res + return ec.marshalNDate2string(ctx, field.Selections, res) +} + +func (ec *executionContext) fieldContext_PortfolioSnapshot_firstTransactionTime(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { + fc = &graphql.FieldContext{ + Object: "PortfolioSnapshot", + Field: field, + IsMethod: false, + IsResolver: false, + Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + return nil, errors.New("field of type Date does not have child fields") + }, + } + return fc, nil +} + +func (ec *executionContext) _PortfolioSnapshot_cash(ctx context.Context, field graphql.CollectedField, obj *models.PortfolioSnapshot) (ret graphql.Marshaler) { + fc, err := ec.fieldContext_PortfolioSnapshot_cash(ctx, field) + if err != nil { + return graphql.Null + } + ctx = graphql.WithFieldContext(ctx, fc) + defer func() { + if r := recover(); r != nil { + ec.Error(ctx, ec.Recover(ctx, r)) + ret = graphql.Null + } + }() + resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (any, error) { + ctx = rctx // use context from middleware stack in children + return obj.Cash, nil + }) + if err != nil { + ec.Error(ctx, err) + return graphql.Null + } + if resTmp == nil { + if !graphql.HasFieldError(ctx, fc) { + ec.Errorf(ctx, "must not be null") + } + return graphql.Null + } + res := resTmp.(*persistence.Currency) + fc.Result = res + return ec.marshalNCurrency2ᚖgithubᚗcomᚋoxistoᚋmoneyᚑgopherᚋpersistenceᚐCurrency(ctx, field.Selections, res) +} + +func (ec *executionContext) fieldContext_PortfolioSnapshot_cash(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { + fc = &graphql.FieldContext{ + Object: "PortfolioSnapshot", + Field: field, + IsMethod: false, + IsResolver: false, + Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + switch field.Name { + case "value": + return ec.fieldContext_Currency_value(ctx, field) + case "symbol": + return ec.fieldContext_Currency_symbol(ctx, field) + } + return nil, fmt.Errorf("no field named %q was found under type Currency", field.Name) + }, + } + return fc, nil +} + func (ec *executionContext) _Query_security(ctx context.Context, field graphql.CollectedField) (ret graphql.Marshaler) { fc, err := ec.fieldContext_Query_security(ctx, field) if err != nil { @@ -2288,6 +2636,8 @@ func (ec *executionContext) fieldContext_Query_portfolio(ctx context.Context, fi return ec.fieldContext_Portfolio_bankAccount(ctx, field) case "snapshot": return ec.fieldContext_Portfolio_snapshot(ctx, field) + case "events": + return ec.fieldContext_Portfolio_events(ctx, field) } return nil, fmt.Errorf("no field named %q was found under type Portfolio", field.Name) }, @@ -2353,6 +2703,8 @@ func (ec *executionContext) fieldContext_Query_portfolios(_ context.Context, fie return ec.fieldContext_Portfolio_bankAccount(ctx, field) case "snapshot": return ec.fieldContext_Portfolio_snapshot(ctx, field) + case "events": + return ec.fieldContext_Portfolio_events(ctx, field) } return nil, fmt.Errorf("no field named %q was found under type Portfolio", field.Name) }, @@ -4444,8 +4796,8 @@ func (ec *executionContext) fieldContext___Type_specifiedByURL(_ context.Context // region **************************** input.gotpl ***************************** -func (ec *executionContext) unmarshalInputListedSecurityInput(ctx context.Context, obj any) (ListedSecurityInput, error) { - var it ListedSecurityInput +func (ec *executionContext) unmarshalInputListedSecurityInput(ctx context.Context, obj any) (models.ListedSecurityInput, error) { + var it models.ListedSecurityInput asMap := map[string]any{} for k, v := range obj.(map[string]any) { asMap[k] = v @@ -4478,8 +4830,8 @@ func (ec *executionContext) unmarshalInputListedSecurityInput(ctx context.Contex return it, nil } -func (ec *executionContext) unmarshalInputSecurityInput(ctx context.Context, obj any) (SecurityInput, error) { - var it SecurityInput +func (ec *executionContext) unmarshalInputSecurityInput(ctx context.Context, obj any) (models.SecurityInput, error) { + var it models.SecurityInput asMap := map[string]any{} for k, v := range obj.(map[string]any) { asMap[k] = v @@ -4508,7 +4860,7 @@ func (ec *executionContext) unmarshalInputSecurityInput(ctx context.Context, obj it.DisplayName = data case "listedAs": ctx := graphql.WithPathContext(ctx, graphql.NewPathWithField("listedAs")) - data, err := ec.unmarshalOListedSecurityInput2ᚕᚖgithubᚗcomᚋoxistoᚋmoneyᚑgopherᚋgraphᚐListedSecurityInputᚄ(ctx, v) + data, err := ec.unmarshalOListedSecurityInput2ᚕᚖgithubᚗcomᚋoxistoᚋmoneyᚑgopherᚋmodelsᚐListedSecurityInputᚄ(ctx, v) if err != nil { return it, err } @@ -4913,6 +5265,181 @@ func (ec *executionContext) _Portfolio(ctx context.Context, sel ast.SelectionSet continue } + out.Concurrently(i, func(ctx context.Context) graphql.Marshaler { return innerFunc(ctx, out) }) + case "events": + field := field + + innerFunc := func(ctx context.Context, fs *graphql.FieldSet) (res graphql.Marshaler) { + defer func() { + if r := recover(); r != nil { + ec.Error(ctx, ec.Recover(ctx, r)) + } + }() + res = ec._Portfolio_events(ctx, field, obj) + if res == graphql.Null { + atomic.AddUint32(&fs.Invalids, 1) + } + return res + } + + if field.Deferrable != nil { + dfs, ok := deferred[field.Deferrable.Label] + di := 0 + if ok { + dfs.AddField(field) + di = len(dfs.Values) - 1 + } else { + dfs = graphql.NewFieldSet([]graphql.CollectedField{field}) + deferred[field.Deferrable.Label] = dfs + } + dfs.Concurrently(di, func(ctx context.Context) graphql.Marshaler { + return innerFunc(ctx, dfs) + }) + + // don't run the out.Concurrently() call below + out.Values[i] = graphql.Null + continue + } + + out.Concurrently(i, func(ctx context.Context) graphql.Marshaler { return innerFunc(ctx, out) }) + default: + panic("unknown field " + strconv.Quote(field.Name)) + } + } + out.Dispatch(ctx) + if out.Invalids > 0 { + return graphql.Null + } + + atomic.AddInt32(&ec.deferred, int32(len(deferred))) + + for label, dfs := range deferred { + ec.processDeferredGroup(graphql.DeferredGroup{ + Label: label, + Path: graphql.GetPath(ctx), + FieldSet: dfs, + Context: ctx, + }) + } + + return out +} + +var portfolioEventImplementors = []string{"PortfolioEvent"} + +func (ec *executionContext) _PortfolioEvent(ctx context.Context, sel ast.SelectionSet, obj *persistence.PortfolioEvent) graphql.Marshaler { + fields := graphql.CollectFields(ec.OperationContext, sel, portfolioEventImplementors) + + out := graphql.NewFieldSet(fields) + deferred := make(map[string]*graphql.FieldSet) + for i, field := range fields { + switch field.Name { + case "__typename": + out.Values[i] = graphql.MarshalString("PortfolioEvent") + case "time": + field := field + + innerFunc := func(ctx context.Context, fs *graphql.FieldSet) (res graphql.Marshaler) { + defer func() { + if r := recover(); r != nil { + ec.Error(ctx, ec.Recover(ctx, r)) + } + }() + res = ec._PortfolioEvent_time(ctx, field, obj) + if res == graphql.Null { + atomic.AddUint32(&fs.Invalids, 1) + } + return res + } + + if field.Deferrable != nil { + dfs, ok := deferred[field.Deferrable.Label] + di := 0 + if ok { + dfs.AddField(field) + di = len(dfs.Values) - 1 + } else { + dfs = graphql.NewFieldSet([]graphql.CollectedField{field}) + deferred[field.Deferrable.Label] = dfs + } + dfs.Concurrently(di, func(ctx context.Context) graphql.Marshaler { + return innerFunc(ctx, dfs) + }) + + // don't run the out.Concurrently() call below + out.Values[i] = graphql.Null + continue + } + + out.Concurrently(i, func(ctx context.Context) graphql.Marshaler { return innerFunc(ctx, out) }) + case "type": + field := field + + innerFunc := func(ctx context.Context, fs *graphql.FieldSet) (res graphql.Marshaler) { + defer func() { + if r := recover(); r != nil { + ec.Error(ctx, ec.Recover(ctx, r)) + } + }() + res = ec._PortfolioEvent_type(ctx, field, obj) + if res == graphql.Null { + atomic.AddUint32(&fs.Invalids, 1) + } + return res + } + + if field.Deferrable != nil { + dfs, ok := deferred[field.Deferrable.Label] + di := 0 + if ok { + dfs.AddField(field) + di = len(dfs.Values) - 1 + } else { + dfs = graphql.NewFieldSet([]graphql.CollectedField{field}) + deferred[field.Deferrable.Label] = dfs + } + dfs.Concurrently(di, func(ctx context.Context) graphql.Marshaler { + return innerFunc(ctx, dfs) + }) + + // don't run the out.Concurrently() call below + out.Values[i] = graphql.Null + continue + } + + out.Concurrently(i, func(ctx context.Context) graphql.Marshaler { return innerFunc(ctx, out) }) + case "security": + field := field + + innerFunc := func(ctx context.Context, _ *graphql.FieldSet) (res graphql.Marshaler) { + defer func() { + if r := recover(); r != nil { + ec.Error(ctx, ec.Recover(ctx, r)) + } + }() + res = ec._PortfolioEvent_security(ctx, field, obj) + return res + } + + if field.Deferrable != nil { + dfs, ok := deferred[field.Deferrable.Label] + di := 0 + if ok { + dfs.AddField(field) + di = len(dfs.Values) - 1 + } else { + dfs = graphql.NewFieldSet([]graphql.CollectedField{field}) + deferred[field.Deferrable.Label] = dfs + } + dfs.Concurrently(di, func(ctx context.Context) graphql.Marshaler { + return innerFunc(ctx, dfs) + }) + + // don't run the out.Concurrently() call below + out.Values[i] = graphql.Null + continue + } + out.Concurrently(i, func(ctx context.Context) graphql.Marshaler { return innerFunc(ctx, out) }) default: panic("unknown field " + strconv.Quote(field.Name)) @@ -4939,7 +5466,7 @@ func (ec *executionContext) _Portfolio(ctx context.Context, sel ast.SelectionSet var portfolioPositionImplementors = []string{"PortfolioPosition"} -func (ec *executionContext) _PortfolioPosition(ctx context.Context, sel ast.SelectionSet, obj *PortfolioPosition) graphql.Marshaler { +func (ec *executionContext) _PortfolioPosition(ctx context.Context, sel ast.SelectionSet, obj *models.PortfolioPosition) graphql.Marshaler { fields := graphql.CollectFields(ec.OperationContext, sel, portfolioPositionImplementors) out := graphql.NewFieldSet(fields) @@ -5018,7 +5545,7 @@ func (ec *executionContext) _PortfolioPosition(ctx context.Context, sel ast.Sele var portfolioSnapshotImplementors = []string{"PortfolioSnapshot"} -func (ec *executionContext) _PortfolioSnapshot(ctx context.Context, sel ast.SelectionSet, obj *PortfolioSnapshot) graphql.Marshaler { +func (ec *executionContext) _PortfolioSnapshot(ctx context.Context, sel ast.SelectionSet, obj *models.PortfolioSnapshot) graphql.Marshaler { fields := graphql.CollectFields(ec.OperationContext, sel, portfolioSnapshotImplementors) out := graphql.NewFieldSet(fields) @@ -5032,8 +5559,18 @@ func (ec *executionContext) _PortfolioSnapshot(ctx context.Context, sel ast.Sele if out.Values[i] == graphql.Null { out.Invalids++ } - case "position": - out.Values[i] = ec._PortfolioSnapshot_position(ctx, field, obj) + case "positions": + out.Values[i] = ec._PortfolioSnapshot_positions(ctx, field, obj) + if out.Values[i] == graphql.Null { + out.Invalids++ + } + case "firstTransactionTime": + out.Values[i] = ec._PortfolioSnapshot_firstTransactionTime(ctx, field, obj) + if out.Values[i] == graphql.Null { + out.Invalids++ + } + case "cash": + out.Values[i] = ec._PortfolioSnapshot_cash(ctx, field, obj) if out.Values[i] == graphql.Null { out.Invalids++ } @@ -5752,7 +6289,7 @@ func (ec *executionContext) marshalNListedSecurity2ᚖgithubᚗcomᚋoxistoᚋmo return ec._ListedSecurity(ctx, sel, v) } -func (ec *executionContext) unmarshalNListedSecurityInput2ᚖgithubᚗcomᚋoxistoᚋmoneyᚑgopherᚋgraphᚐListedSecurityInput(ctx context.Context, v any) (*ListedSecurityInput, error) { +func (ec *executionContext) unmarshalNListedSecurityInput2ᚖgithubᚗcomᚋoxistoᚋmoneyᚑgopherᚋmodelsᚐListedSecurityInput(ctx context.Context, v any) (*models.ListedSecurityInput, error) { res, err := ec.unmarshalInputListedSecurityInput(ctx, v) return &res, graphql.ErrorOnPath(ctx, err) } @@ -5811,7 +6348,71 @@ func (ec *executionContext) marshalNPortfolio2ᚖgithubᚗcomᚋoxistoᚋmoney return ec._Portfolio(ctx, sel, v) } -func (ec *executionContext) marshalNPortfolioPosition2ᚕᚖgithubᚗcomᚋoxistoᚋmoneyᚑgopherᚋgraphᚐPortfolioPositionᚄ(ctx context.Context, sel ast.SelectionSet, v []*PortfolioPosition) graphql.Marshaler { +func (ec *executionContext) marshalNPortfolioEvent2ᚕᚖgithubᚗcomᚋoxistoᚋmoneyᚑgopherᚋpersistenceᚐPortfolioEventᚄ(ctx context.Context, sel ast.SelectionSet, v []*persistence.PortfolioEvent) graphql.Marshaler { + ret := make(graphql.Array, len(v)) + var wg sync.WaitGroup + isLen1 := len(v) == 1 + if !isLen1 { + wg.Add(len(v)) + } + for i := range v { + i := i + fc := &graphql.FieldContext{ + Index: &i, + Result: &v[i], + } + ctx := graphql.WithFieldContext(ctx, fc) + f := func(i int) { + defer func() { + if r := recover(); r != nil { + ec.Error(ctx, ec.Recover(ctx, r)) + ret = nil + } + }() + if !isLen1 { + defer wg.Done() + } + ret[i] = ec.marshalNPortfolioEvent2ᚖgithubᚗcomᚋoxistoᚋmoneyᚑgopherᚋpersistenceᚐPortfolioEvent(ctx, sel, v[i]) + } + if isLen1 { + f(i) + } else { + go f(i) + } + + } + wg.Wait() + + for _, e := range ret { + if e == graphql.Null { + return graphql.Null + } + } + + return ret +} + +func (ec *executionContext) marshalNPortfolioEvent2ᚖgithubᚗcomᚋoxistoᚋmoneyᚑgopherᚋpersistenceᚐPortfolioEvent(ctx context.Context, sel ast.SelectionSet, v *persistence.PortfolioEvent) graphql.Marshaler { + if v == nil { + if !graphql.HasFieldError(ctx, graphql.GetFieldContext(ctx)) { + ec.Errorf(ctx, "the requested element is null which the schema does not allow") + } + return graphql.Null + } + return ec._PortfolioEvent(ctx, sel, v) +} + +func (ec *executionContext) unmarshalNPortfolioEventType2githubᚗcomᚋoxistoᚋmoneyᚑgopherᚋmodelsᚐPortfolioEventType(ctx context.Context, v any) (models.PortfolioEventType, error) { + var res models.PortfolioEventType + err := res.UnmarshalGQL(v) + return res, graphql.ErrorOnPath(ctx, err) +} + +func (ec *executionContext) marshalNPortfolioEventType2githubᚗcomᚋoxistoᚋmoneyᚑgopherᚋmodelsᚐPortfolioEventType(ctx context.Context, sel ast.SelectionSet, v models.PortfolioEventType) graphql.Marshaler { + return v +} + +func (ec *executionContext) marshalNPortfolioPosition2ᚕᚖgithubᚗcomᚋoxistoᚋmoneyᚑgopherᚋmodelsᚐPortfolioPositionᚄ(ctx context.Context, sel ast.SelectionSet, v []*models.PortfolioPosition) graphql.Marshaler { ret := make(graphql.Array, len(v)) var wg sync.WaitGroup isLen1 := len(v) == 1 @@ -5835,7 +6436,7 @@ func (ec *executionContext) marshalNPortfolioPosition2ᚕᚖgithubᚗcomᚋoxist if !isLen1 { defer wg.Done() } - ret[i] = ec.marshalNPortfolioPosition2ᚖgithubᚗcomᚋoxistoᚋmoneyᚑgopherᚋgraphᚐPortfolioPosition(ctx, sel, v[i]) + ret[i] = ec.marshalNPortfolioPosition2ᚖgithubᚗcomᚋoxistoᚋmoneyᚑgopherᚋmodelsᚐPortfolioPosition(ctx, sel, v[i]) } if isLen1 { f(i) @@ -5855,7 +6456,7 @@ func (ec *executionContext) marshalNPortfolioPosition2ᚕᚖgithubᚗcomᚋoxist return ret } -func (ec *executionContext) marshalNPortfolioPosition2ᚖgithubᚗcomᚋoxistoᚋmoneyᚑgopherᚋgraphᚐPortfolioPosition(ctx context.Context, sel ast.SelectionSet, v *PortfolioPosition) graphql.Marshaler { +func (ec *executionContext) marshalNPortfolioPosition2ᚖgithubᚗcomᚋoxistoᚋmoneyᚑgopherᚋmodelsᚐPortfolioPosition(ctx context.Context, sel ast.SelectionSet, v *models.PortfolioPosition) graphql.Marshaler { if v == nil { if !graphql.HasFieldError(ctx, graphql.GetFieldContext(ctx)) { ec.Errorf(ctx, "the requested element is null which the schema does not allow") @@ -5923,7 +6524,7 @@ func (ec *executionContext) marshalNSecurity2ᚖgithubᚗcomᚋoxistoᚋmoneyᚑ return ec._Security(ctx, sel, v) } -func (ec *executionContext) unmarshalNSecurityInput2githubᚗcomᚋoxistoᚋmoneyᚑgopherᚋgraphᚐSecurityInput(ctx context.Context, v any) (SecurityInput, error) { +func (ec *executionContext) unmarshalNSecurityInput2githubᚗcomᚋoxistoᚋmoneyᚑgopherᚋmodelsᚐSecurityInput(ctx context.Context, v any) (models.SecurityInput, error) { res, err := ec.unmarshalInputSecurityInput(ctx, v) return res, graphql.ErrorOnPath(ctx, err) } @@ -6292,7 +6893,7 @@ func (ec *executionContext) marshalOListedSecurity2ᚕᚖgithubᚗcomᚋoxisto return ret } -func (ec *executionContext) unmarshalOListedSecurityInput2ᚕᚖgithubᚗcomᚋoxistoᚋmoneyᚑgopherᚋgraphᚐListedSecurityInputᚄ(ctx context.Context, v any) ([]*ListedSecurityInput, error) { +func (ec *executionContext) unmarshalOListedSecurityInput2ᚕᚖgithubᚗcomᚋoxistoᚋmoneyᚑgopherᚋmodelsᚐListedSecurityInputᚄ(ctx context.Context, v any) ([]*models.ListedSecurityInput, error) { if v == nil { return nil, nil } @@ -6301,10 +6902,10 @@ func (ec *executionContext) unmarshalOListedSecurityInput2ᚕᚖgithubᚗcomᚋo vSlice = graphql.CoerceList(v) } var err error - res := make([]*ListedSecurityInput, len(vSlice)) + res := make([]*models.ListedSecurityInput, len(vSlice)) for i := range vSlice { ctx := graphql.WithPathContext(ctx, graphql.NewPathWithIndex(i)) - res[i], err = ec.unmarshalNListedSecurityInput2ᚖgithubᚗcomᚋoxistoᚋmoneyᚑgopherᚋgraphᚐListedSecurityInput(ctx, vSlice[i]) + res[i], err = ec.unmarshalNListedSecurityInput2ᚖgithubᚗcomᚋoxistoᚋmoneyᚑgopherᚋmodelsᚐListedSecurityInput(ctx, vSlice[i]) if err != nil { return nil, err } @@ -6319,7 +6920,7 @@ func (ec *executionContext) marshalOPortfolio2ᚖgithubᚗcomᚋoxistoᚋmoney return ec._Portfolio(ctx, sel, v) } -func (ec *executionContext) marshalOPortfolioSnapshot2ᚖgithubᚗcomᚋoxistoᚋmoneyᚑgopherᚋgraphᚐPortfolioSnapshot(ctx context.Context, sel ast.SelectionSet, v *PortfolioSnapshot) graphql.Marshaler { +func (ec *executionContext) marshalOPortfolioSnapshot2ᚖgithubᚗcomᚋoxistoᚋmoneyᚑgopherᚋmodelsᚐPortfolioSnapshot(ctx context.Context, sel ast.SelectionSet, v *models.PortfolioSnapshot) graphql.Marshaler { if v == nil { return graphql.Null } diff --git a/graph/schema.graphqls b/graph/schema.graphqls index e30ae9cd..b8d440dc 100644 --- a/graph/schema.graphqls +++ b/graph/schema.graphqls @@ -24,13 +24,27 @@ type Portfolio { id: String! displayName: String! bankAccount: BankAccount! - snapshot(time: Date!): PortfolioSnapshot + snapshot(when: Date!): PortfolioSnapshot events: [PortfolioEvent!]! } +enum PortfolioEventType { + BUY + SELL + DIVIDEND +} + +type PortfolioEvent { + time: Date! + type: PortfolioEventType! + security: Security +} + type PortfolioSnapshot { time: Date! - position: [PortfolioPosition!]! + positions: [PortfolioPosition!]! + firstTransactionTime: Date! + cash: Currency! } type PortfolioPosition { diff --git a/graph/schema.resolvers.go b/graph/schema.resolvers.go index 46445cc9..86ece88d 100644 --- a/graph/schema.resolvers.go +++ b/graph/schema.resolvers.go @@ -10,6 +10,8 @@ import ( "slices" "time" + "github.com/oxisto/money-gopher/finance" + "github.com/oxisto/money-gopher/models" "github.com/oxisto/money-gopher/persistence" ) @@ -43,7 +45,7 @@ func (r *listedSecurityResolver) LatestQuoteTimestamp(ctx context.Context, obj * } // CreateSecurity is the resolver for the createSecurity field. -func (r *mutationResolver) CreateSecurity(ctx context.Context, input SecurityInput) (*persistence.Security, error) { +func (r *mutationResolver) CreateSecurity(ctx context.Context, input models.SecurityInput) (*persistence.Security, error) { return withTx(r.Resolver, func(qtx *persistence.Queries) (*persistence.Security, error) { sec, err := qtx.CreateSecurity(ctx, persistence.CreateSecurityParams{ ID: input.ID, @@ -69,7 +71,7 @@ func (r *mutationResolver) CreateSecurity(ctx context.Context, input SecurityInp } // UpdateSecurity is the resolver for the updateSecurity field. -func (r *mutationResolver) UpdateSecurity(ctx context.Context, id string, input SecurityInput) (*persistence.Security, error) { +func (r *mutationResolver) UpdateSecurity(ctx context.Context, id string, input models.SecurityInput) (*persistence.Security, error) { return withTx(r.Resolver, func(qtx *persistence.Queries) (*persistence.Security, error) { sec, err := qtx.UpdateSecurity(ctx, persistence.UpdateSecurityParams{ ID: id, @@ -133,8 +135,39 @@ func (r *portfolioResolver) BankAccount(ctx context.Context, obj *persistence.Po } // Snapshot is the resolver for the snapshot field. -func (r *portfolioResolver) Snapshot(ctx context.Context, obj *persistence.Portfolio, time string) (*PortfolioSnapshot, error) { - panic(fmt.Errorf("not implemented: Snapshot - snapshot")) +func (r *portfolioResolver) Snapshot(ctx context.Context, obj *persistence.Portfolio, when string) (snap *models.PortfolioSnapshot, err error) { + var t time.Time + + if when == "" { + t = time.Now() + } else { + t, err = time.Parse(time.RFC3339, when) + if err != nil { + return nil, err + } + } + + return finance.BuildSnapshot(ctx, &t, obj.ID, r.DB) +} + +// Events is the resolver for the events field. +func (r *portfolioResolver) Events(ctx context.Context, obj *persistence.Portfolio) ([]*persistence.PortfolioEvent, error) { + panic(fmt.Errorf("not implemented: Events - events")) +} + +// Time is the resolver for the time field. +func (r *portfolioEventResolver) Time(ctx context.Context, obj *persistence.PortfolioEvent) (string, error) { + panic(fmt.Errorf("not implemented: Time - time")) +} + +// Type is the resolver for the type field. +func (r *portfolioEventResolver) Type(ctx context.Context, obj *persistence.PortfolioEvent) (models.PortfolioEventType, error) { + panic(fmt.Errorf("not implemented: Type - type")) +} + +// Security is the resolver for the security field. +func (r *portfolioEventResolver) Security(ctx context.Context, obj *persistence.PortfolioEvent) (*persistence.Security, error) { + panic(fmt.Errorf("not implemented: Security - security")) } // Security is the resolver for the security field. @@ -180,6 +213,9 @@ func (r *Resolver) Mutation() MutationResolver { return &mutationResolver{r} } // Portfolio returns PortfolioResolver implementation. func (r *Resolver) Portfolio() PortfolioResolver { return &portfolioResolver{r} } +// PortfolioEvent returns PortfolioEventResolver implementation. +func (r *Resolver) PortfolioEvent() PortfolioEventResolver { return &portfolioEventResolver{r} } + // Query returns QueryResolver implementation. func (r *Resolver) Query() QueryResolver { return &queryResolver{r} } @@ -189,5 +225,6 @@ func (r *Resolver) Security() SecurityResolver { return &securityResolver{r} } type listedSecurityResolver struct{ *Resolver } type mutationResolver struct{ *Resolver } type portfolioResolver struct{ *Resolver } +type portfolioEventResolver struct{ *Resolver } type queryResolver struct{ *Resolver } type securityResolver struct{ *Resolver } diff --git a/graph/models_gen.go b/models/models_gen.go similarity index 57% rename from graph/models_gen.go rename to models/models_gen.go index e25ef230..e2c4c0a2 100644 --- a/graph/models_gen.go +++ b/models/models_gen.go @@ -1,8 +1,12 @@ // Code generated by github.com/99designs/gqlgen, DO NOT EDIT. -package graph +package models import ( + "fmt" + "io" + "strconv" + "github.com/oxisto/money-gopher/persistence" ) @@ -39,8 +43,10 @@ type PortfolioPosition struct { } type PortfolioSnapshot struct { - Time string `json:"time"` - Position []*PortfolioPosition `json:"position"` + Time string `json:"time"` + Positions []*PortfolioPosition `json:"positions"` + FirstTransactionTime string `json:"firstTransactionTime"` + Cash *persistence.Currency `json:"cash"` } type Query struct { @@ -51,3 +57,46 @@ type SecurityInput struct { DisplayName string `json:"displayName"` ListedAs []*ListedSecurityInput `json:"listedAs,omitempty"` } + +type PortfolioEventType string + +const ( + PortfolioEventTypeBuy PortfolioEventType = "BUY" + PortfolioEventTypeSell PortfolioEventType = "SELL" + PortfolioEventTypeDividend PortfolioEventType = "DIVIDEND" +) + +var AllPortfolioEventType = []PortfolioEventType{ + PortfolioEventTypeBuy, + PortfolioEventTypeSell, + PortfolioEventTypeDividend, +} + +func (e PortfolioEventType) IsValid() bool { + switch e { + case PortfolioEventTypeBuy, PortfolioEventTypeSell, PortfolioEventTypeDividend: + return true + } + return false +} + +func (e PortfolioEventType) String() string { + return string(e) +} + +func (e *PortfolioEventType) UnmarshalGQL(v any) error { + str, ok := v.(string) + if !ok { + return fmt.Errorf("enums must be strings") + } + + *e = PortfolioEventType(str) + if !e.IsValid() { + return fmt.Errorf("%s is not a valid PortfolioEventType", str) + } + return nil +} + +func (e PortfolioEventType) MarshalGQL(w io.Writer) { + fmt.Fprint(w, strconv.Quote(e.String())) +} diff --git a/persistence/portfolios.sql.go b/persistence/portfolios.sql.go index f8cd096b..3a5abcd8 100644 --- a/persistence/portfolios.sql.go +++ b/persistence/portfolios.sql.go @@ -60,6 +60,51 @@ func (q *Queries) GetPortfolio(ctx context.Context, id string) (*Portfolio, erro return &i, err } +const listPortfolioEventsByPortfolioID = `-- name: ListPortfolioEventsByPortfolioID :many +SELECT + id, type, time, portfolio_id, security_id, amount, price, price_currency, fees, fees_currency, taxes, taxes_currency +FROM + portfolio_events +WHERE + portfolio_id = ? +` + +func (q *Queries) ListPortfolioEventsByPortfolioID(ctx context.Context, portfolioID string) ([]*PortfolioEvent, error) { + rows, err := q.db.QueryContext(ctx, listPortfolioEventsByPortfolioID, portfolioID) + if err != nil { + return nil, err + } + defer rows.Close() + var items []*PortfolioEvent + for rows.Next() { + var i PortfolioEvent + if err := rows.Scan( + &i.ID, + &i.Type, + &i.Time, + &i.PortfolioID, + &i.SecurityID, + &i.Amount, + &i.Price, + &i.PriceCurrency, + &i.Fees, + &i.FeesCurrency, + &i.Taxes, + &i.TaxesCurrency, + ); err != nil { + return nil, err + } + items = append(items, &i) + } + if err := rows.Close(); err != nil { + return nil, err + } + if err := rows.Err(); err != nil { + return nil, err + } + return items, nil +} + const listPortfolios = `-- name: ListPortfolios :many SELECT id, display_name, bank_account_id diff --git a/persistence/securities.sql.go b/persistence/securities.sql.go index 60ddd916..49d85ad2 100644 --- a/persistence/securities.sql.go +++ b/persistence/securities.sql.go @@ -8,6 +8,7 @@ package persistence import ( "context" "database/sql" + "strings" ) const createSecurity = `-- name: CreateSecurity :one @@ -143,6 +144,50 @@ func (q *Queries) ListSecurities(ctx context.Context) ([]*Security, error) { return items, nil } +const listSecuritiesByIDs = `-- name: ListSecuritiesByIDs :many +SELECT + id, display_name, quote_provider +FROM + securities +WHERE + id IN (/*SLICE:ids*/?) +ORDER BY + id +` + +func (q *Queries) ListSecuritiesByIDs(ctx context.Context, ids []string) ([]*Security, error) { + query := listSecuritiesByIDs + var queryParams []interface{} + if len(ids) > 0 { + for _, v := range ids { + queryParams = append(queryParams, v) + } + query = strings.Replace(query, "/*SLICE:ids*/?", strings.Repeat(",?", len(ids))[1:], 1) + } else { + query = strings.Replace(query, "/*SLICE:ids*/?", "NULL", 1) + } + rows, err := q.db.QueryContext(ctx, query, queryParams...) + if err != nil { + return nil, err + } + defer rows.Close() + var items []*Security + for rows.Next() { + var i Security + if err := rows.Scan(&i.ID, &i.DisplayName, &i.QuoteProvider); err != nil { + return nil, err + } + items = append(items, &i) + } + if err := rows.Close(); err != nil { + return nil, err + } + if err := rows.Err(); err != nil { + return nil, err + } + return items, nil +} + const updateSecurity = `-- name: UpdateSecurity :one UPDATE securities SET diff --git a/persistence/sql/queries/portfolios.sql b/persistence/sql/queries/portfolios.sql index 4d23617f..4fd0dd99 100644 --- a/persistence/sql/queries/portfolios.sql +++ b/persistence/sql/queries/portfolios.sql @@ -14,6 +14,14 @@ FROM ORDER BY id; +-- name: ListPortfolioEventsByPortfolioID :many +SELECT + * +FROM + portfolio_events +WHERE + portfolio_id = ?; + -- name: GetBankAccount :one SELECT * diff --git a/persistence/sql/queries/securities.sql b/persistence/sql/queries/securities.sql index e7bda499..efd90a14 100644 --- a/persistence/sql/queries/securities.sql +++ b/persistence/sql/queries/securities.sql @@ -14,6 +14,16 @@ FROM ORDER BY id; +-- name: ListSecuritiesByIDs :many +SELECT + * +FROM + securities +WHERE + id IN (sqlc.slice ('ids')) +ORDER BY + id; + -- name: CreateSecurity :one INSERT INTO securities (id, display_name, quote_provider) From fd87d22905aea45252746cf8b08155fcfbe952fa Mon Sep 17 00:00:00 2001 From: Christian Banse Date: Wed, 1 Jan 2025 14:45:45 +0100 Subject: [PATCH 13/35] First test in persistence works --- cli/cli.go | 37 +- cli/commands/account.go | 1 - cli/commands/portfolio.go | 1 - cli/commands/securities.go | 7 +- cli/commands/securities_test.go | 1 - currency/currency.go | 114 + finance/calculation.go | 97 +- finance/calculation_test.go | 56 +- finance/snapshot.go | 74 +- gen/account_sql.go | 98 - gen/currency.go | 94 - gen/mgo.pb.go | 2646 ----------------- gen/portfolio.go | 90 - gen/portfolio_sql.go | 231 -- gen/portfolio_test.go | 75 - gen/portfoliov1connect/mgo.connect.go | 773 ----- gen/securities_sql.go | 223 -- go.mod | 6 +- gqlgen.yml | 2 + graph/generated.go | 490 ++- graph/resolver.go | 4 +- graph/schema.graphqls | 28 +- graph/schema.resolvers.go | 33 +- import/csv/csv_importer.go | 58 +- import/csv/csv_importer_test.go | 9 +- internal/persistencetest/queries.go | 24 + internal/testing/servertest/servertest.go | 15 +- models/models_gen.go | 75 +- money-gopher.code-workspace | 2 + persistence/currency.go | 20 - persistence/{relationships.go => extra.go} | 4 +- persistence/models.go | 28 +- persistence/portfolios.sql.go | 5 +- persistence/securities.sql.go | 4 +- persistence/securities.sql_test.go | 70 + .../sql/migrations/0001_create_securities.sql | 2 +- .../sql/migrations/0002_create_portfolio.sql | 9 +- portfolio/events/type.go | 23 + .../securities => securities/quote}/quote.go | 80 +- .../quote}/quote_provider.go | 5 +- .../quote}/quote_provider_ing.go | 9 +- .../quote}/quote_provider_ing_test.go | 7 +- .../quote}/quote_provider_yf.go | 7 +- .../quote}/quote_provider_yf_test.go | 7 +- .../quote}/quote_test.go | 82 +- securities/securities.go | 13 + server/server.go | 3 - service/internal/crud/crud_requests.go | 98 - service/internal/crud/crud_requests_test.go | 219 -- service/portfolio/account.go | 55 - service/portfolio/account_test.go | 197 -- service/portfolio/portfolio.go | 88 - service/portfolio/portfolio_test.go | 369 --- service/portfolio/service.go | 71 - service/portfolio/service_test.go | 54 - service/portfolio/snapshot.go | 160 - service/portfolio/snapshot_test.go | 211 -- service/portfolio/transactions.go | 156 - service/portfolio/transactions_test.go | 396 --- service/securities/securities.go | 114 - service/securities/securities_test.go | 229 -- service/securities/service.go | 88 - service/securities/service_test.go | 45 - sqlc.yaml | 27 + tools/tools.go | 1 + 65 files changed, 988 insertions(+), 7332 deletions(-) create mode 100644 currency/currency.go delete mode 100644 gen/account_sql.go delete mode 100644 gen/currency.go delete mode 100644 gen/mgo.pb.go delete mode 100644 gen/portfolio.go delete mode 100644 gen/portfolio_sql.go delete mode 100644 gen/portfolio_test.go delete mode 100644 gen/portfoliov1connect/mgo.connect.go delete mode 100644 gen/securities_sql.go create mode 100644 internal/persistencetest/queries.go delete mode 100644 persistence/currency.go rename persistence/{relationships.go => extra.go} (91%) create mode 100644 persistence/securities.sql_test.go create mode 100644 portfolio/events/type.go rename {service/securities => securities/quote}/quote.go (57%) rename {service/securities => securities/quote}/quote_provider.go (92%) rename {service/securities => securities/quote}/quote_provider_ing.go (87%) rename {service/securities => securities/quote}/quote_provider_ing_test.go (96%) rename {service/securities => securities/quote}/quote_provider_yf.go (89%) rename {service/securities => securities/quote}/quote_provider_yf_test.go (96%) rename {service/securities => securities/quote}/quote_test.go (51%) create mode 100644 securities/securities.go delete mode 100644 service/internal/crud/crud_requests.go delete mode 100644 service/internal/crud/crud_requests_test.go delete mode 100644 service/portfolio/account.go delete mode 100644 service/portfolio/account_test.go delete mode 100644 service/portfolio/portfolio.go delete mode 100644 service/portfolio/portfolio_test.go delete mode 100644 service/portfolio/service.go delete mode 100644 service/portfolio/service_test.go delete mode 100644 service/portfolio/snapshot.go delete mode 100644 service/portfolio/snapshot_test.go delete mode 100644 service/portfolio/transactions.go delete mode 100644 service/portfolio/transactions_test.go delete mode 100644 service/securities/securities.go delete mode 100644 service/securities/securities_test.go delete mode 100644 service/securities/service.go delete mode 100644 service/securities/service_test.go diff --git a/cli/cli.go b/cli/cli.go index e9b58531..88553e13 100644 --- a/cli/cli.go +++ b/cli/cli.go @@ -22,15 +22,11 @@ import ( "encoding/json" "fmt" "io" - "log/slog" "net/http" "os" - "github.com/oxisto/money-gopher/gen/portfoliov1connect" "github.com/shurcooL/graphql" - "connectrpc.com/connect" - "github.com/lmittmann/tint" oauth2 "github.com/oxisto/oauth2go" "github.com/urfave/cli/v3" ) @@ -42,9 +38,7 @@ var SessionKey sessionKeyType // Session holds all necessary information about the current CLI session. type Session struct { - PortfolioClient portfoliov1connect.PortfolioServiceClient `json:"-"` - SecuritiesClient portfoliov1connect.SecuritiesServiceClient `json:"-"` - GraphQL *graphql.Client + GraphQL *graphql.Client opts *SessionOptions } @@ -144,39 +138,10 @@ func (s *Session) Save() (err error) { // initClients initializes the clients for the session. func (s *Session) initClients() { - interceptor := func(next connect.UnaryFunc) connect.UnaryFunc { - return connect.UnaryFunc(func( - ctx context.Context, - req connect.AnyRequest, - ) (connect.AnyResponse, error) { - if req.Spec().IsClient { - var t, err = s.opts.OAuth2Config.TokenSource(context.Background(), s.opts.Token).Token() - if err != nil { - slog.Error("Could not retrieve token", tint.Err(err)) - } else { - req.Header().Set("Authorization", "Bearer "+t.AccessToken) - } - } - return next(ctx, req) - }) - } - if s.opts.HttpClient == nil { s.opts.HttpClient = http.DefaultClient } - s.PortfolioClient = portfoliov1connect.NewPortfolioServiceClient( - s.opts.HttpClient, s.opts.BaseURL, - connect.WithHTTPGet(), - connect.WithInterceptors(connect.UnaryInterceptorFunc(interceptor)), - ) - - s.SecuritiesClient = portfoliov1connect.NewSecuritiesServiceClient( - s.opts.HttpClient, s.opts.BaseURL, - connect.WithHTTPGet(), - connect.WithInterceptors(connect.UnaryInterceptorFunc(interceptor)), - ) - s.GraphQL = graphql.NewClient(s.opts.BaseURL+"/graphql/query", s.opts.HttpClient) } diff --git a/cli/commands/account.go b/cli/commands/account.go index ad43b01e..c0e704c7 100644 --- a/cli/commands/account.go +++ b/cli/commands/account.go @@ -21,7 +21,6 @@ import ( "fmt" mcli "github.com/oxisto/money-gopher/cli" - portfoliov1 "github.com/oxisto/money-gopher/gen" "connectrpc.com/connect" "github.com/urfave/cli/v3" diff --git a/cli/commands/portfolio.go b/cli/commands/portfolio.go index c630eda2..11504b16 100644 --- a/cli/commands/portfolio.go +++ b/cli/commands/portfolio.go @@ -25,7 +25,6 @@ import ( "time" mcli "github.com/oxisto/money-gopher/cli" - portfoliov1 "github.com/oxisto/money-gopher/gen" "connectrpc.com/connect" "github.com/fatih/color" diff --git a/cli/commands/securities.go b/cli/commands/securities.go index 1824a93e..0f612479 100644 --- a/cli/commands/securities.go +++ b/cli/commands/securities.go @@ -19,12 +19,9 @@ package commands import ( "context" - "fmt" mcli "github.com/oxisto/money-gopher/cli" - portfoliov1 "github.com/oxisto/money-gopher/gen" - "connectrpc.com/connect" "github.com/shurcooL/graphql" "github.com/urfave/cli/v3" ) @@ -156,7 +153,7 @@ func UpdateAllQuotes(ctx context.Context, cmd *cli.Command) (err error) { // PredictSecurities predicts the securities for shell completion. func PredictSecurities(ctx context.Context, cmd *cli.Command) { - s := mcli.FromContext(ctx) + /*s := mcli.FromContext(ctx) res, err := s.SecuritiesClient.ListSecurities( context.Background(), connect.NewRequest(&portfoliov1.ListSecuritiesRequest{}), @@ -167,5 +164,5 @@ func PredictSecurities(ctx context.Context, cmd *cli.Command) { for _, p := range res.Msg.Securities { fmt.Fprintf(cmd.Root().Writer, "%s:%s\n", p.Id, p.DisplayName) - } + }*/ } diff --git a/cli/commands/securities_test.go b/cli/commands/securities_test.go index 07660466..b0d8073b 100644 --- a/cli/commands/securities_test.go +++ b/cli/commands/securities_test.go @@ -22,7 +22,6 @@ import ( "testing" moneygopher "github.com/oxisto/money-gopher" - portfoliov1 "github.com/oxisto/money-gopher/gen" "github.com/oxisto/money-gopher/internal" "github.com/oxisto/money-gopher/internal/testing/clitest" "github.com/oxisto/money-gopher/internal/testing/servertest" diff --git a/currency/currency.go b/currency/currency.go new file mode 100644 index 00000000..df2848ed --- /dev/null +++ b/currency/currency.go @@ -0,0 +1,114 @@ +package currency + +import ( + "database/sql/driver" + "encoding/json" + "fmt" + "math" +) + +// Currency represents a currency with a value and a symbol. +type Currency struct { + // Amount is the amount of the currency. + Amount int32 `json:"value"` + + // Symbol is the symbol of the currency. + Symbol string `json:"symbol"` +} + +func Zero() *Currency { + // TODO(oxisto): Somehow make it possible to change default currency + return &Currency{Symbol: "EUR"} +} + +func Value(v int32) *Currency { + // TODO(oxisto): Somehow make it possible to change default currency + return &Currency{Symbol: "EUR", Amount: v} +} + +func (c *Currency) PlusAssign(o *Currency) { + if o != nil { + c.Amount += o.Amount + } +} + +func (c *Currency) MinusAssign(o *Currency) { + if o != nil { + c.Amount -= o.Amount + } +} + +func Plus(a *Currency, b *Currency) *Currency { + return &Currency{ + Amount: a.Amount + b.Amount, + Symbol: a.Symbol, + } +} + +func (a *Currency) Plus(b *Currency) *Currency { + if b == nil { + return &Currency{ + Amount: a.Amount, + Symbol: a.Symbol, + } + } + + return &Currency{ + Amount: a.Amount + b.Amount, + Symbol: a.Symbol, + } +} + +func Minus(a *Currency, b *Currency) *Currency { + return &Currency{ + Amount: a.Amount - b.Amount, + Symbol: a.Symbol, + } +} + +func Divide(a *Currency, b float64) *Currency { + return &Currency{ + Amount: int32(math.Round((float64(a.Amount) / b))), + Symbol: a.Symbol, + } +} + +func Times(a *Currency, b float64) *Currency { + return &Currency{ + Amount: int32(math.Round((float64(a.Amount) * b))), + Symbol: a.Symbol, + } +} + +func (c *Currency) Pretty() string { + return fmt.Sprintf("%.0f %s", float32(c.Amount)/100, c.Symbol) +} + +func (c *Currency) IsZero() bool { + return c == nil || c.Amount == 0 +} + +// Value implements the driver.Valuer interface. +func (c *Currency) Value() (driver.Value, error) { + if c == nil { + return nil, nil + } + + return json.Marshal(c) +} + +// Scan implements the sql.Scanner interface. +func (c *Currency) Scan(src interface{}) error { + if src == nil { + return nil + } + + switch v := src.(type) { + case string: + return json.Unmarshal([]byte(v), c) + case []byte: + return json.Unmarshal(v, c) + default: + return fmt.Errorf("unsupported type: %T", src) + } +} diff --git a/finance/calculation.go b/finance/calculation.go index a7699c10..20174860 100644 --- a/finance/calculation.go +++ b/finance/calculation.go @@ -20,57 +20,62 @@ package finance import ( "math" - portfoliov1 "github.com/oxisto/money-gopher/gen" + "github.com/oxisto/money-gopher/currency" + "github.com/oxisto/money-gopher/persistence" + "github.com/oxisto/money-gopher/portfolio/events" ) // fifoTx is a helper struct to store transaction-related information in a FIFO // list. We basically need to copy the values from the original transaction, // since we need to modify it. type fifoTx struct { - amount float64 // amount of shares in this transaction - value *portfoliov1.Currency // value contains the net value of this transaction, i.e., without taxes and fees - fees *portfoliov1.Currency // fees contain any fees associated to this transaction - ppu *portfoliov1.Currency // ppu is the price per unit (amount) + amount float64 // amount of shares in this transaction + value *currency.Currency // value contains the net value of this transaction, i.e., without taxes and fees + fees *currency.Currency // fees contain any fees associated to this transaction + ppu *currency.Currency // ppu is the price per unit (amount) } +// calculation is a helper struct to calculate the net and gross value of a +// portfolio (snapshot). type calculation struct { Amount float64 - Fees *portfoliov1.Currency - Taxes *portfoliov1.Currency + Fees *currency.Currency + Taxes *currency.Currency - Cash *portfoliov1.Currency + Cash *currency.Currency fifo []*fifoTx } -func NewCalculation(txs []*portfoliov1.PortfolioEvent) *calculation { +// NewCalculation creates a new calculation struct and applies all events +func NewCalculation(events []*persistence.PortfolioEvent) *calculation { var c calculation - c.Fees = portfoliov1.Zero() - c.Taxes = portfoliov1.Zero() - c.Cash = portfoliov1.Zero() + c.Fees = currency.Zero() + c.Taxes = currency.Zero() + c.Cash = currency.Zero() - for _, tx := range txs { + for _, tx := range events { c.Apply(tx) } return &c } -func (c *calculation) Apply(tx *portfoliov1.PortfolioEvent) { +func (c *calculation) Apply(tx *persistence.PortfolioEvent) { switch tx.Type { - case portfoliov1.PortfolioEventType_PORTFOLIO_EVENT_TYPE_DELIVERY_INBOUND: + case events.PortfolioEventTypeDeliveryInbound: fallthrough - case portfoliov1.PortfolioEventType_PORTFOLIO_EVENT_TYPE_BUY: + case events.PortfolioEventTypeBuy: var ( - total *portfoliov1.Currency + total *currency.Currency ) // Increase the amount of shares and the fees by the value stored in the // transaction - c.Fees.PlusAssign(tx.Fees) - c.Amount += tx.Amount + c.Fees.PlusAssign(tx.Fees()) + c.Amount += tx.Amount.Float64 - total = portfoliov1.Times(tx.Price, tx.Amount).Plus(tx.Fees).Plus(tx.Taxes) + total = currency.Times(tx.Price(), tx.Amount.Float64).Plus(tx.Fees()).Plus(tx.Taxes()) // Decrease our cash c.Cash.MinusAssign(total) @@ -80,32 +85,32 @@ func (c *calculation) Apply(tx *portfoliov1.PortfolioEvent) { // need to store this information to reduce the amount in the items // later when a sell transaction occurs. c.fifo = append(c.fifo, &fifoTx{ - amount: tx.Amount, - ppu: tx.Price, - value: portfoliov1.Times(tx.Price, tx.Amount), - fees: tx.Fees, + amount: tx.Amount.Float64, + ppu: tx.Price(), + value: currency.Times(tx.Price(), tx.Amount.Float64), + fees: tx.Fees(), }) - case portfoliov1.PortfolioEventType_PORTFOLIO_EVENT_TYPE_DELIVERY_OUTBOUND: + case events.PortfolioEventTypeDeliveryOutbound: fallthrough - case portfoliov1.PortfolioEventType_PORTFOLIO_EVENT_TYPE_SELL: + case events.PortfolioEventTypeSell: var ( sold float64 - total *portfoliov1.Currency + total *currency.Currency ) // Increase the fees and taxes by the value stored in the // transaction - c.Fees.PlusAssign(tx.Fees) - c.Taxes.PlusAssign(tx.Taxes) + c.Fees.PlusAssign(tx.Fees()) + c.Taxes.PlusAssign(tx.Taxes()) - total = portfoliov1.Times(tx.Price, tx.Amount).Plus(tx.Fees).Plus(tx.Taxes) + total = currency.Times(tx.Price(), tx.Amount.Float64).Plus(tx.Fees()).Plus(tx.Taxes()) // Increase our cash c.Cash.PlusAssign(total) // Store the amount of shares sold in this variable, since we later need // to decrease it while looping through the FIFO list - sold = tx.Amount + sold = tx.Amount.Float64 // Calculate the remaining shares (if any) c.Amount -= sold @@ -135,28 +140,28 @@ func (c *calculation) Apply(tx *portfoliov1.PortfolioEvent) { item.amount -= n // Adjust the value with the new amount - item.value = portfoliov1.Times(item.ppu, item.amount) + item.value = currency.Times(item.ppu, item.amount) // If no shares are left in this FIFO transaction, also remove the // fees, because they are now associated to the sale and not part of // the price calculation anymore. if item.amount <= 0 { - item.fees = portfoliov1.Zero() + item.fees = currency.Zero() } sold -= n } - case portfoliov1.PortfolioEventType_PORTFOLIO_EVENT_TYPE_DEPOSIT_CASH: + case events.PortfolioEventTypeDepositCash: // Add to the cash - c.Cash.PlusAssign(tx.Price) - case portfoliov1.PortfolioEventType_PORTFOLIO_EVENT_TYPE_WITHDRAW_CASH: + c.Cash.PlusAssign(tx.Price()) + case events.PortfolioEventTypeWithdrawCash: // Remove from the cash - c.Cash.MinusAssign(tx.Price) + c.Cash.MinusAssign(tx.Price()) } } -func (c *calculation) NetValue() (f *portfoliov1.Currency) { - f = portfoliov1.Zero() +func (c *calculation) NetValue() (f *currency.Currency) { + f = currency.Zero() for _, item := range c.fifo { f.PlusAssign(item.value) @@ -165,20 +170,20 @@ func (c *calculation) NetValue() (f *portfoliov1.Currency) { return } -func (c *calculation) GrossValue() (f *portfoliov1.Currency) { - f = portfoliov1.Zero() +func (c *calculation) GrossValue() (f *currency.Currency) { + f = currency.Zero() for _, item := range c.fifo { - f.PlusAssign(portfoliov1.Plus(item.value, item.fees)) + f.PlusAssign(currency.Plus(item.value, item.fees)) } return } -func (c *calculation) NetPrice() (f *portfoliov1.Currency) { - return portfoliov1.Divide(c.NetValue(), c.Amount) +func (c *calculation) NetPrice() (f *currency.Currency) { + return currency.Divide(c.NetValue(), c.Amount) } -func (c *calculation) GrossPrice() (f *portfoliov1.Currency) { - return portfoliov1.Divide(c.GrossValue(), c.Amount) +func (c *calculation) GrossPrice() (f *currency.Currency) { + return currency.Divide(c.GrossValue(), c.Amount) } diff --git a/finance/calculation_test.go b/finance/calculation_test.go index 811d8eea..f71d26c8 100644 --- a/finance/calculation_test.go +++ b/finance/calculation_test.go @@ -20,14 +20,14 @@ package finance import ( "testing" - portfoliov1 "github.com/oxisto/money-gopher/gen" - "github.com/oxisto/assert" + "github.com/oxisto/money-gopher/persistence" + "github.com/oxisto/money-gopher/portfolio/events" ) func TestNewCalculation(t *testing.T) { type args struct { - txs []*portfoliov1.PortfolioEvent + txs []*persistence.PortfolioEvent } tests := []struct { name string @@ -37,53 +37,53 @@ func TestNewCalculation(t *testing.T) { { name: "buy and sell", args: args{ - txs: []*portfoliov1.PortfolioEvent{ + txs: []*persistence.PortfolioEvent{ { - Type: portfoliov1.PortfolioEventType_PORTFOLIO_EVENT_TYPE_DEPOSIT_CASH, - Price: portfoliov1.Value(500000), + Type: events.PortfolioEventTypeDepositCash, + Price: persistence.Value(500000), }, { - Type: portfoliov1.PortfolioEventType_PORTFOLIO_EVENT_TYPE_BUY, + Type: events.PortfolioEventTypeBuy, Amount: 5, - Price: portfoliov1.Value(18110), - Fees: portfoliov1.Value(716), + Price: persistence.Value(18110), + Fees: persistence.Value(716), }, { - Type: portfoliov1.PortfolioEventType_PORTFOLIO_EVENT_TYPE_SELL, + Type: events.PortfolioEventTypeSell, Amount: 2, - Price: portfoliov1.Value(30430), - Fees: portfoliov1.Value(642), - Taxes: portfoliov1.Value(1632), + Price: persistence.Value(30430), + Fees: persistence.Value(642), + Taxes: persistence.Value(1632), }, { - Type: portfoliov1.PortfolioEventType_PORTFOLIO_EVENT_TYPE_BUY, + Type: events.PortfolioEventTypeBuy, Amount: 5, - Price: portfoliov1.Value(29000), - Fees: portfoliov1.Value(853), + Price: persistence.Value(29000), + Fees: persistence.Value(853), }, { - Type: portfoliov1.PortfolioEventType_PORTFOLIO_EVENT_TYPE_SELL, + Type: events.PortfolioEventTypeSell, Amount: 3, - Price: portfoliov1.Value(22000), - Fees: portfoliov1.Value(845), + Price: persistence.Value(22000), + Fees: persistence.Value(845), }, { - Type: portfoliov1.PortfolioEventType_PORTFOLIO_EVENT_TYPE_BUY, + Type: events.PortfolioEventTypeBuy, Amount: 5, - Price: portfoliov1.Value(20330), - Fees: portfoliov1.Value(744), + Price: persistence.Value(20330), + Fees: persistence.Value(744), }, { - Type: portfoliov1.PortfolioEventType_PORTFOLIO_EVENT_TYPE_BUY, + Type: events.PortfolioEventTypeBuy, Amount: 5, - Price: portfoliov1.Value(19645), - Fees: portfoliov1.Value(736), + Price: persistence.Value(19645), + Fees: persistence.Value(736), }, { - Type: portfoliov1.PortfolioEventType_PORTFOLIO_EVENT_TYPE_BUY, + Type: events.PortfolioEventTypeBuy, Amount: 10, - Price: portfoliov1.Value(14655), - Fees: portfoliov1.Value(856), + Price: persistence.Value(14655), + Fees: persistence.Value(856), }, }, }, diff --git a/finance/snapshot.go b/finance/snapshot.go index 0c6618a8..dda23095 100644 --- a/finance/snapshot.go +++ b/finance/snapshot.go @@ -9,7 +9,7 @@ import ( "connectrpc.com/connect" moneygopher "github.com/oxisto/money-gopher" - portfoliov1 "github.com/oxisto/money-gopher/gen" + "github.com/oxisto/money-gopher/currency" "github.com/oxisto/money-gopher/models" "github.com/oxisto/money-gopher/persistence" ) @@ -18,6 +18,7 @@ import ( // building a snapshot. It includes methods for retrieving portfolio events and // securities by their IDs. type SnapshotDataProvider interface { + ListListedSecuritiesBySecurityID(ctx context.Context, securityID string) ([]*persistence.ListedSecurity, error) ListPortfolioEventsByPortfolioID(ctx context.Context, portfolioID string) ([]*persistence.PortfolioEvent, error) ListSecuritiesByIDs(ctx context.Context, ids []string) ([]*persistence.Security, error) } @@ -31,14 +32,13 @@ type SnapshotDataProvider interface { // and calculating the positions at the specified timestamp. func BuildSnapshot( ctx context.Context, - timestamp *time.Time, + timestamp time.Time, portfolioID string, provider SnapshotDataProvider, ) (snap *models.PortfolioSnapshot, err error) { var ( events []*persistence.PortfolioEvent - p portfoliov1.Portfolio - m map[string][]*portfoliov1.PortfolioEvent + m map[string][]*persistence.PortfolioEvent ids []string secs []*persistence.Security secmap map[string]*persistence.Security @@ -52,21 +52,21 @@ func BuildSnapshot( // Set up the snapshot snap = &models.PortfolioSnapshot{ - Time: timestamp.Format(time.RFC3339), - Positions: make([]*models.PortfolioPosition, 0), - /*TotalPurchaseValue: portfoliov1.Zero(), - TotalMarketValue: portfoliov1.Zero(), - TotalProfitOrLoss: portfoliov1.Zero(), - Cash: portfoliov1.Zero(),*/ + Time: timestamp.Format(time.RFC3339), + Positions: make([]*models.PortfolioPosition, 0), + TotalPurchaseValue: currency.Zero(), + TotalMarketValue: currency.Zero(), + TotalProfitOrLoss: currency.Zero(), + Cash: currency.Zero(), } // Record the first transaction time - if len(p.Events) > 0 { + if len(events) > 0 { snap.FirstTransactionTime = events[0].Time.Format(time.RFC3339) } // Retrieve the event map; a map of events indexed by their security ID - m = p.EventMap() + m = groupByPortfolio(events) ids = slices.Collect(maps.Keys(m)) // Retrieve market value of filtered securities @@ -103,18 +103,18 @@ func BuildSnapshot( // Also add cash that is part of a securities' transaction (e.g., sell/buy) snap.Cash.PlusAssign(c.Cash) - pos := &portfoliov1.PortfolioPosition{ + pos := &models.PortfolioPosition{ Security: secmap[name], Amount: c.Amount, PurchaseValue: c.NetValue(), PurchasePrice: c.NetPrice(), - MarketValue: portfoliov1.Times(marketPrice(secmap, name, c.NetPrice()), c.Amount), - MarketPrice: marketPrice(secmap, name, c.NetPrice()), + MarketValue: currency.Times(marketPrice(secmap, name, c.NetPrice(), provider), c.Amount), + MarketPrice: marketPrice(secmap, name, c.NetPrice(), provider), } // Calculate loss and gains - pos.ProfitOrLoss = portfoliov1.Minus(pos.MarketValue, pos.PurchaseValue) - pos.Gains = float64(portfoliov1.Minus(pos.MarketValue, pos.PurchaseValue).Value) / float64(pos.PurchaseValue.Value) + pos.ProfitOrLoss = currency.Minus(pos.MarketValue, pos.PurchaseValue) + pos.Gains = float64(currency.Minus(pos.MarketValue, pos.PurchaseValue).Value) / float64(pos.PurchaseValue.Value) // Add to total value(s) snap.TotalPurchaseValue.PlusAssign(pos.PurchaseValue) @@ -122,18 +122,19 @@ func BuildSnapshot( snap.TotalProfitOrLoss.PlusAssign(pos.ProfitOrLoss) // Store position in map - snap.Positions[name] = pos + snap.Positions = append(snap.Positions, pos) } // Calculate total gains - snap.TotalGains = float64(portfoliov1.Minus(snap.TotalMarketValue, snap.TotalPurchaseValue).Value) / float64(snap.TotalPurchaseValue.Value) + snap.TotalGains = float64(currency.Minus(snap.TotalMarketValue, snap.TotalPurchaseValue).Value) / float64(snap.TotalPurchaseValue.Value) // Calculate total portfolio value snap.TotalPortfolioValue = snap.TotalMarketValue.Plus(snap.Cash) - return connect.NewResponse(snap), nil + return snap, nil } +// eventsBefore returns all events that occurred before a given time. // TODO: move to SQL query func eventsBefore(events []*persistence.PortfolioEvent, t time.Time) (out []*persistence.PortfolioEvent) { out = make([]*persistence.PortfolioEvent, 0, len(events)) @@ -148,3 +149,36 @@ func eventsBefore(events []*persistence.PortfolioEvent, t time.Time) (out []*per return } + +// groupByPortfolio groups the events by their security ID. +func groupByPortfolio(events []*persistence.PortfolioEvent) (m map[string][]*persistence.PortfolioEvent) { + for _, event := range events { + name := event.SecurityID + if name != "" { + m[name] = append(m[name], event) + } else { + // a little bit of a hack + m["cash"] = append(m["cash"], event) + } + } + + return +} + +func marketPrice( + secmap map[string]*persistence.Security, + name string, + netPrice *currency.Currency, + provider SnapshotDataProvider, +) *currency.Currency { + ls, _ := provider.ListListedSecuritiesBySecurityID(context.Background(), name) + + if ls == nil || !ls[0].LatestQuote.Valid { + return netPrice + } else { + return ¤cy.Currency{ + Value: int32(ls[0].LatestQuote.Int64), + Symbol: ls[0].Currency, + } + } +} diff --git a/gen/account_sql.go b/gen/account_sql.go deleted file mode 100644 index e6ec5509..00000000 --- a/gen/account_sql.go +++ /dev/null @@ -1,98 +0,0 @@ -// Copyright 2023 Christian Banse -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. -// -// This file is part of The Money Gopher. - -package portfoliov1 - -import ( - "database/sql" - "errors" - "strings" - - "github.com/oxisto/money-gopher/persistence" -) - -func (*BankAccount) InitTables(db *persistence.DB) (err error) { - _, err1 := db.Exec(`CREATE TABLE IF NOT EXISTS bank_accounts ( -id TEXT PRIMARY KEY, -display_name TEXT NOT NULL -);`) - err2 := (&PortfolioEvent{}).InitTables(db) - - return errors.Join(err1, err2) -} - -func (*BankAccount) PrepareReplace(db *persistence.DB) (stmt *sql.Stmt, err error) { - return db.Prepare(`REPLACE INTO bank_accounts (id, display_name) VALUES (?,?);`) -} - -func (*BankAccount) PrepareList(db *persistence.DB) (stmt *sql.Stmt, err error) { - return db.Prepare(`SELECT id, display_name FROM bank_accounts`) -} - -func (*BankAccount) PrepareGet(db *persistence.DB) (stmt *sql.Stmt, err error) { - return db.Prepare(`SELECT id, display_name FROM bank_accounts WHERE id = ?`) -} - -func (*BankAccount) PrepareUpdate(db *persistence.DB, columns []string) (stmt *sql.Stmt, err error) { - // We need to make sure to quote columns here because they are potentially evil user input - var ( - query string - set []string - ) - - set = make([]string, len(columns)) - for i, col := range columns { - set[i] = persistence.Quote(col) + " = ?" - } - - query += "UPDATE bank_accounts SET " + strings.Join(set, ", ") + " WHERE id = ?;" - - return db.Prepare(query) -} - -func (*BankAccount) PrepareDelete(db *persistence.DB) (stmt *sql.Stmt, err error) { - return db.Prepare(`DELETE FROM bank_accounts WHERE id = ?`) -} - -func (p *BankAccount) ReplaceIntoArgs() []any { - return []any{p.Id, p.DisplayName} -} - -func (p *BankAccount) UpdateArgs(columns []string) (args []any) { - for _, col := range columns { - switch col { - case "id": - args = append(args, p.Id) - case "display_name": - args = append(args, p.DisplayName) - } - } - - return args -} - -func (*BankAccount) Scan(sc persistence.Scanner) (obj persistence.StorageObject, err error) { - var ( - acc BankAccount - ) - - err = sc.Scan(&acc.Id, &acc.DisplayName) - if err != nil { - return nil, err - } - - return &acc, nil -} diff --git a/gen/currency.go b/gen/currency.go deleted file mode 100644 index 62e2230a..00000000 --- a/gen/currency.go +++ /dev/null @@ -1,94 +0,0 @@ -// Copyright 2023 Christian Banse -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. -// -// This file is part of The Money Gopher. - -package portfoliov1 - -import ( - "fmt" - "math" -) - -func Zero() *Currency { - // TODO(oxisto): Somehow make it possible to change default currency - return &Currency{Symbol: "EUR"} -} - -func Value(v int32) *Currency { - // TODO(oxisto): Somehow make it possible to change default currency - return &Currency{Symbol: "EUR", Value: v} -} - -func (c *Currency) PlusAssign(o *Currency) { - if o != nil { - c.Value += o.Value - } -} - -func (c *Currency) MinusAssign(o *Currency) { - if o != nil { - c.Value -= o.Value - } -} - -func Plus(a *Currency, b *Currency) *Currency { - return &Currency{ - Value: a.Value + b.Value, - Symbol: a.Symbol, - } -} - -func (a *Currency) Plus(b *Currency) *Currency { - if b == nil { - return &Currency{ - Value: a.Value, - Symbol: a.Symbol, - } - } - - return &Currency{ - Value: a.Value + b.Value, - Symbol: a.Symbol, - } -} - -func Minus(a *Currency, b *Currency) *Currency { - return &Currency{ - Value: a.Value - b.Value, - Symbol: a.Symbol, - } -} - -func Divide(a *Currency, b float64) *Currency { - return &Currency{ - Value: int32(math.Round((float64(a.Value) / b))), - Symbol: a.Symbol, - } -} - -func Times(a *Currency, b float64) *Currency { - return &Currency{ - Value: int32(math.Round((float64(a.Value) * b))), - Symbol: a.Symbol, - } -} - -func (c *Currency) Pretty() string { - return fmt.Sprintf("%.0f %s", float32(c.Value)/100, c.Symbol) -} - -func (c *Currency) IsZero() bool { - return c == nil || c.Value == 0 -} diff --git a/gen/mgo.pb.go b/gen/mgo.pb.go deleted file mode 100644 index d86f324d..00000000 --- a/gen/mgo.pb.go +++ /dev/null @@ -1,2646 +0,0 @@ -// Code generated by protoc-gen-go. DO NOT EDIT. -// versions: -// protoc-gen-go v1.36.1 -// protoc (unknown) -// source: mgo.proto - -package portfoliov1 - -import ( - _ "google.golang.org/genproto/googleapis/api/annotations" - protoreflect "google.golang.org/protobuf/reflect/protoreflect" - protoimpl "google.golang.org/protobuf/runtime/protoimpl" - emptypb "google.golang.org/protobuf/types/known/emptypb" - fieldmaskpb "google.golang.org/protobuf/types/known/fieldmaskpb" - timestamppb "google.golang.org/protobuf/types/known/timestamppb" - reflect "reflect" - sync "sync" -) - -const ( - // Verify that this generated code is sufficiently up-to-date. - _ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion) - // Verify that runtime/protoimpl is sufficiently up-to-date. - _ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20) -) - -type PortfolioEventType int32 - -const ( - PortfolioEventType_PORTFOLIO_EVENT_TYPE_UNSPECIFIED PortfolioEventType = 0 - PortfolioEventType_PORTFOLIO_EVENT_TYPE_BUY PortfolioEventType = 1 - PortfolioEventType_PORTFOLIO_EVENT_TYPE_SELL PortfolioEventType = 2 - PortfolioEventType_PORTFOLIO_EVENT_TYPE_DELIVERY_INBOUND PortfolioEventType = 3 - PortfolioEventType_PORTFOLIO_EVENT_TYPE_DELIVERY_OUTBOUND PortfolioEventType = 4 - PortfolioEventType_PORTFOLIO_EVENT_TYPE_DIVIDEND PortfolioEventType = 10 - PortfolioEventType_PORTFOLIO_EVENT_TYPE_INTEREST PortfolioEventType = 11 - PortfolioEventType_PORTFOLIO_EVENT_TYPE_DEPOSIT_CASH PortfolioEventType = 20 - PortfolioEventType_PORTFOLIO_EVENT_TYPE_WITHDRAW_CASH PortfolioEventType = 21 - PortfolioEventType_PORTFOLIO_EVENT_TYPE_ACCOUNT_FEES PortfolioEventType = 30 - PortfolioEventType_PORTFOLIO_EVENT_TYPE_TAX_REFUND PortfolioEventType = 31 -) - -// Enum value maps for PortfolioEventType. -var ( - PortfolioEventType_name = map[int32]string{ - 0: "PORTFOLIO_EVENT_TYPE_UNSPECIFIED", - 1: "PORTFOLIO_EVENT_TYPE_BUY", - 2: "PORTFOLIO_EVENT_TYPE_SELL", - 3: "PORTFOLIO_EVENT_TYPE_DELIVERY_INBOUND", - 4: "PORTFOLIO_EVENT_TYPE_DELIVERY_OUTBOUND", - 10: "PORTFOLIO_EVENT_TYPE_DIVIDEND", - 11: "PORTFOLIO_EVENT_TYPE_INTEREST", - 20: "PORTFOLIO_EVENT_TYPE_DEPOSIT_CASH", - 21: "PORTFOLIO_EVENT_TYPE_WITHDRAW_CASH", - 30: "PORTFOLIO_EVENT_TYPE_ACCOUNT_FEES", - 31: "PORTFOLIO_EVENT_TYPE_TAX_REFUND", - } - PortfolioEventType_value = map[string]int32{ - "PORTFOLIO_EVENT_TYPE_UNSPECIFIED": 0, - "PORTFOLIO_EVENT_TYPE_BUY": 1, - "PORTFOLIO_EVENT_TYPE_SELL": 2, - "PORTFOLIO_EVENT_TYPE_DELIVERY_INBOUND": 3, - "PORTFOLIO_EVENT_TYPE_DELIVERY_OUTBOUND": 4, - "PORTFOLIO_EVENT_TYPE_DIVIDEND": 10, - "PORTFOLIO_EVENT_TYPE_INTEREST": 11, - "PORTFOLIO_EVENT_TYPE_DEPOSIT_CASH": 20, - "PORTFOLIO_EVENT_TYPE_WITHDRAW_CASH": 21, - "PORTFOLIO_EVENT_TYPE_ACCOUNT_FEES": 30, - "PORTFOLIO_EVENT_TYPE_TAX_REFUND": 31, - } -) - -func (x PortfolioEventType) Enum() *PortfolioEventType { - p := new(PortfolioEventType) - *p = x - return p -} - -func (x PortfolioEventType) String() string { - return protoimpl.X.EnumStringOf(x.Descriptor(), protoreflect.EnumNumber(x)) -} - -func (PortfolioEventType) Descriptor() protoreflect.EnumDescriptor { - return file_mgo_proto_enumTypes[0].Descriptor() -} - -func (PortfolioEventType) Type() protoreflect.EnumType { - return &file_mgo_proto_enumTypes[0] -} - -func (x PortfolioEventType) Number() protoreflect.EnumNumber { - return protoreflect.EnumNumber(x) -} - -// Deprecated: Use PortfolioEventType.Descriptor instead. -func (PortfolioEventType) EnumDescriptor() ([]byte, []int) { - return file_mgo_proto_rawDescGZIP(), []int{0} -} - -// Currency is a currency value in the lowest unit of the selected currency -// (e.g., cents for EUR/USD). -type Currency struct { - state protoimpl.MessageState `protogen:"open.v1"` - Value int32 `protobuf:"varint,1,opt,name=value,proto3" json:"value,omitempty"` - Symbol string `protobuf:"bytes,2,opt,name=symbol,proto3" json:"symbol,omitempty"` - unknownFields protoimpl.UnknownFields - sizeCache protoimpl.SizeCache -} - -func (x *Currency) Reset() { - *x = Currency{} - mi := &file_mgo_proto_msgTypes[0] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) -} - -func (x *Currency) String() string { - return protoimpl.X.MessageStringOf(x) -} - -func (*Currency) ProtoMessage() {} - -func (x *Currency) ProtoReflect() protoreflect.Message { - mi := &file_mgo_proto_msgTypes[0] - if x != nil { - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - if ms.LoadMessageInfo() == nil { - ms.StoreMessageInfo(mi) - } - return ms - } - return mi.MessageOf(x) -} - -// Deprecated: Use Currency.ProtoReflect.Descriptor instead. -func (*Currency) Descriptor() ([]byte, []int) { - return file_mgo_proto_rawDescGZIP(), []int{0} -} - -func (x *Currency) GetValue() int32 { - if x != nil { - return x.Value - } - return 0 -} - -func (x *Currency) GetSymbol() string { - if x != nil { - return x.Symbol - } - return "" -} - -type CreatePortfolioRequest struct { - state protoimpl.MessageState `protogen:"open.v1"` - Portfolio *Portfolio `protobuf:"bytes,1,opt,name=portfolio,proto3" json:"portfolio,omitempty"` - unknownFields protoimpl.UnknownFields - sizeCache protoimpl.SizeCache -} - -func (x *CreatePortfolioRequest) Reset() { - *x = CreatePortfolioRequest{} - mi := &file_mgo_proto_msgTypes[1] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) -} - -func (x *CreatePortfolioRequest) String() string { - return protoimpl.X.MessageStringOf(x) -} - -func (*CreatePortfolioRequest) ProtoMessage() {} - -func (x *CreatePortfolioRequest) ProtoReflect() protoreflect.Message { - mi := &file_mgo_proto_msgTypes[1] - if x != nil { - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - if ms.LoadMessageInfo() == nil { - ms.StoreMessageInfo(mi) - } - return ms - } - return mi.MessageOf(x) -} - -// Deprecated: Use CreatePortfolioRequest.ProtoReflect.Descriptor instead. -func (*CreatePortfolioRequest) Descriptor() ([]byte, []int) { - return file_mgo_proto_rawDescGZIP(), []int{1} -} - -func (x *CreatePortfolioRequest) GetPortfolio() *Portfolio { - if x != nil { - return x.Portfolio - } - return nil -} - -type ListPortfoliosRequest struct { - state protoimpl.MessageState `protogen:"open.v1"` - unknownFields protoimpl.UnknownFields - sizeCache protoimpl.SizeCache -} - -func (x *ListPortfoliosRequest) Reset() { - *x = ListPortfoliosRequest{} - mi := &file_mgo_proto_msgTypes[2] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) -} - -func (x *ListPortfoliosRequest) String() string { - return protoimpl.X.MessageStringOf(x) -} - -func (*ListPortfoliosRequest) ProtoMessage() {} - -func (x *ListPortfoliosRequest) ProtoReflect() protoreflect.Message { - mi := &file_mgo_proto_msgTypes[2] - if x != nil { - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - if ms.LoadMessageInfo() == nil { - ms.StoreMessageInfo(mi) - } - return ms - } - return mi.MessageOf(x) -} - -// Deprecated: Use ListPortfoliosRequest.ProtoReflect.Descriptor instead. -func (*ListPortfoliosRequest) Descriptor() ([]byte, []int) { - return file_mgo_proto_rawDescGZIP(), []int{2} -} - -type ListPortfoliosResponse struct { - state protoimpl.MessageState `protogen:"open.v1"` - Portfolios []*Portfolio `protobuf:"bytes,1,rep,name=portfolios,proto3" json:"portfolios,omitempty"` - unknownFields protoimpl.UnknownFields - sizeCache protoimpl.SizeCache -} - -func (x *ListPortfoliosResponse) Reset() { - *x = ListPortfoliosResponse{} - mi := &file_mgo_proto_msgTypes[3] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) -} - -func (x *ListPortfoliosResponse) String() string { - return protoimpl.X.MessageStringOf(x) -} - -func (*ListPortfoliosResponse) ProtoMessage() {} - -func (x *ListPortfoliosResponse) ProtoReflect() protoreflect.Message { - mi := &file_mgo_proto_msgTypes[3] - if x != nil { - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - if ms.LoadMessageInfo() == nil { - ms.StoreMessageInfo(mi) - } - return ms - } - return mi.MessageOf(x) -} - -// Deprecated: Use ListPortfoliosResponse.ProtoReflect.Descriptor instead. -func (*ListPortfoliosResponse) Descriptor() ([]byte, []int) { - return file_mgo_proto_rawDescGZIP(), []int{3} -} - -func (x *ListPortfoliosResponse) GetPortfolios() []*Portfolio { - if x != nil { - return x.Portfolios - } - return nil -} - -type GetPortfolioRequest struct { - state protoimpl.MessageState `protogen:"open.v1"` - Id string `protobuf:"bytes,1,opt,name=id,proto3" json:"id,omitempty"` - unknownFields protoimpl.UnknownFields - sizeCache protoimpl.SizeCache -} - -func (x *GetPortfolioRequest) Reset() { - *x = GetPortfolioRequest{} - mi := &file_mgo_proto_msgTypes[4] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) -} - -func (x *GetPortfolioRequest) String() string { - return protoimpl.X.MessageStringOf(x) -} - -func (*GetPortfolioRequest) ProtoMessage() {} - -func (x *GetPortfolioRequest) ProtoReflect() protoreflect.Message { - mi := &file_mgo_proto_msgTypes[4] - if x != nil { - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - if ms.LoadMessageInfo() == nil { - ms.StoreMessageInfo(mi) - } - return ms - } - return mi.MessageOf(x) -} - -// Deprecated: Use GetPortfolioRequest.ProtoReflect.Descriptor instead. -func (*GetPortfolioRequest) Descriptor() ([]byte, []int) { - return file_mgo_proto_rawDescGZIP(), []int{4} -} - -func (x *GetPortfolioRequest) GetId() string { - if x != nil { - return x.Id - } - return "" -} - -type UpdatePortfolioRequest struct { - state protoimpl.MessageState `protogen:"open.v1"` - Portfolio *Portfolio `protobuf:"bytes,1,opt,name=portfolio,proto3" json:"portfolio,omitempty"` - UpdateMask *fieldmaskpb.FieldMask `protobuf:"bytes,2,opt,name=updateMask,proto3" json:"updateMask,omitempty"` - unknownFields protoimpl.UnknownFields - sizeCache protoimpl.SizeCache -} - -func (x *UpdatePortfolioRequest) Reset() { - *x = UpdatePortfolioRequest{} - mi := &file_mgo_proto_msgTypes[5] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) -} - -func (x *UpdatePortfolioRequest) String() string { - return protoimpl.X.MessageStringOf(x) -} - -func (*UpdatePortfolioRequest) ProtoMessage() {} - -func (x *UpdatePortfolioRequest) ProtoReflect() protoreflect.Message { - mi := &file_mgo_proto_msgTypes[5] - if x != nil { - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - if ms.LoadMessageInfo() == nil { - ms.StoreMessageInfo(mi) - } - return ms - } - return mi.MessageOf(x) -} - -// Deprecated: Use UpdatePortfolioRequest.ProtoReflect.Descriptor instead. -func (*UpdatePortfolioRequest) Descriptor() ([]byte, []int) { - return file_mgo_proto_rawDescGZIP(), []int{5} -} - -func (x *UpdatePortfolioRequest) GetPortfolio() *Portfolio { - if x != nil { - return x.Portfolio - } - return nil -} - -func (x *UpdatePortfolioRequest) GetUpdateMask() *fieldmaskpb.FieldMask { - if x != nil { - return x.UpdateMask - } - return nil -} - -type DeletePortfolioRequest struct { - state protoimpl.MessageState `protogen:"open.v1"` - Id string `protobuf:"bytes,1,opt,name=id,proto3" json:"id,omitempty"` - unknownFields protoimpl.UnknownFields - sizeCache protoimpl.SizeCache -} - -func (x *DeletePortfolioRequest) Reset() { - *x = DeletePortfolioRequest{} - mi := &file_mgo_proto_msgTypes[6] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) -} - -func (x *DeletePortfolioRequest) String() string { - return protoimpl.X.MessageStringOf(x) -} - -func (*DeletePortfolioRequest) ProtoMessage() {} - -func (x *DeletePortfolioRequest) ProtoReflect() protoreflect.Message { - mi := &file_mgo_proto_msgTypes[6] - if x != nil { - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - if ms.LoadMessageInfo() == nil { - ms.StoreMessageInfo(mi) - } - return ms - } - return mi.MessageOf(x) -} - -// Deprecated: Use DeletePortfolioRequest.ProtoReflect.Descriptor instead. -func (*DeletePortfolioRequest) Descriptor() ([]byte, []int) { - return file_mgo_proto_rawDescGZIP(), []int{6} -} - -func (x *DeletePortfolioRequest) GetId() string { - if x != nil { - return x.Id - } - return "" -} - -type GetPortfolioSnapshotRequest struct { - state protoimpl.MessageState `protogen:"open.v1"` - // PortfolioId is the identifier of the portfolio we want to - // "snapshot". - PortfolioId string `protobuf:"bytes,1,opt,name=portfolio_id,json=portfolioId,proto3" json:"portfolio_id,omitempty"` - // Time is the point in time of the requested snapshot. - Time *timestamppb.Timestamp `protobuf:"bytes,2,opt,name=time,proto3" json:"time,omitempty"` - unknownFields protoimpl.UnknownFields - sizeCache protoimpl.SizeCache -} - -func (x *GetPortfolioSnapshotRequest) Reset() { - *x = GetPortfolioSnapshotRequest{} - mi := &file_mgo_proto_msgTypes[7] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) -} - -func (x *GetPortfolioSnapshotRequest) String() string { - return protoimpl.X.MessageStringOf(x) -} - -func (*GetPortfolioSnapshotRequest) ProtoMessage() {} - -func (x *GetPortfolioSnapshotRequest) ProtoReflect() protoreflect.Message { - mi := &file_mgo_proto_msgTypes[7] - if x != nil { - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - if ms.LoadMessageInfo() == nil { - ms.StoreMessageInfo(mi) - } - return ms - } - return mi.MessageOf(x) -} - -// Deprecated: Use GetPortfolioSnapshotRequest.ProtoReflect.Descriptor instead. -func (*GetPortfolioSnapshotRequest) Descriptor() ([]byte, []int) { - return file_mgo_proto_rawDescGZIP(), []int{7} -} - -func (x *GetPortfolioSnapshotRequest) GetPortfolioId() string { - if x != nil { - return x.PortfolioId - } - return "" -} - -func (x *GetPortfolioSnapshotRequest) GetTime() *timestamppb.Timestamp { - if x != nil { - return x.Time - } - return nil -} - -type CreatePortfolioTransactionRequest struct { - state protoimpl.MessageState `protogen:"open.v1"` - Transaction *PortfolioEvent `protobuf:"bytes,1,opt,name=transaction,proto3" json:"transaction,omitempty"` - unknownFields protoimpl.UnknownFields - sizeCache protoimpl.SizeCache -} - -func (x *CreatePortfolioTransactionRequest) Reset() { - *x = CreatePortfolioTransactionRequest{} - mi := &file_mgo_proto_msgTypes[8] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) -} - -func (x *CreatePortfolioTransactionRequest) String() string { - return protoimpl.X.MessageStringOf(x) -} - -func (*CreatePortfolioTransactionRequest) ProtoMessage() {} - -func (x *CreatePortfolioTransactionRequest) ProtoReflect() protoreflect.Message { - mi := &file_mgo_proto_msgTypes[8] - if x != nil { - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - if ms.LoadMessageInfo() == nil { - ms.StoreMessageInfo(mi) - } - return ms - } - return mi.MessageOf(x) -} - -// Deprecated: Use CreatePortfolioTransactionRequest.ProtoReflect.Descriptor instead. -func (*CreatePortfolioTransactionRequest) Descriptor() ([]byte, []int) { - return file_mgo_proto_rawDescGZIP(), []int{8} -} - -func (x *CreatePortfolioTransactionRequest) GetTransaction() *PortfolioEvent { - if x != nil { - return x.Transaction - } - return nil -} - -type GetPortfolioTransactionRequest struct { - state protoimpl.MessageState `protogen:"open.v1"` - Id string `protobuf:"bytes,1,opt,name=id,proto3" json:"id,omitempty"` - unknownFields protoimpl.UnknownFields - sizeCache protoimpl.SizeCache -} - -func (x *GetPortfolioTransactionRequest) Reset() { - *x = GetPortfolioTransactionRequest{} - mi := &file_mgo_proto_msgTypes[9] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) -} - -func (x *GetPortfolioTransactionRequest) String() string { - return protoimpl.X.MessageStringOf(x) -} - -func (*GetPortfolioTransactionRequest) ProtoMessage() {} - -func (x *GetPortfolioTransactionRequest) ProtoReflect() protoreflect.Message { - mi := &file_mgo_proto_msgTypes[9] - if x != nil { - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - if ms.LoadMessageInfo() == nil { - ms.StoreMessageInfo(mi) - } - return ms - } - return mi.MessageOf(x) -} - -// Deprecated: Use GetPortfolioTransactionRequest.ProtoReflect.Descriptor instead. -func (*GetPortfolioTransactionRequest) Descriptor() ([]byte, []int) { - return file_mgo_proto_rawDescGZIP(), []int{9} -} - -func (x *GetPortfolioTransactionRequest) GetId() string { - if x != nil { - return x.Id - } - return "" -} - -type ListPortfolioTransactionsRequest struct { - state protoimpl.MessageState `protogen:"open.v1"` - PortfolioId string `protobuf:"bytes,1,opt,name=portfolio_id,json=portfolioId,proto3" json:"portfolio_id,omitempty"` - unknownFields protoimpl.UnknownFields - sizeCache protoimpl.SizeCache -} - -func (x *ListPortfolioTransactionsRequest) Reset() { - *x = ListPortfolioTransactionsRequest{} - mi := &file_mgo_proto_msgTypes[10] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) -} - -func (x *ListPortfolioTransactionsRequest) String() string { - return protoimpl.X.MessageStringOf(x) -} - -func (*ListPortfolioTransactionsRequest) ProtoMessage() {} - -func (x *ListPortfolioTransactionsRequest) ProtoReflect() protoreflect.Message { - mi := &file_mgo_proto_msgTypes[10] - if x != nil { - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - if ms.LoadMessageInfo() == nil { - ms.StoreMessageInfo(mi) - } - return ms - } - return mi.MessageOf(x) -} - -// Deprecated: Use ListPortfolioTransactionsRequest.ProtoReflect.Descriptor instead. -func (*ListPortfolioTransactionsRequest) Descriptor() ([]byte, []int) { - return file_mgo_proto_rawDescGZIP(), []int{10} -} - -func (x *ListPortfolioTransactionsRequest) GetPortfolioId() string { - if x != nil { - return x.PortfolioId - } - return "" -} - -type ListPortfolioTransactionsResponse struct { - state protoimpl.MessageState `protogen:"open.v1"` - Transactions []*PortfolioEvent `protobuf:"bytes,1,rep,name=transactions,proto3" json:"transactions,omitempty"` - unknownFields protoimpl.UnknownFields - sizeCache protoimpl.SizeCache -} - -func (x *ListPortfolioTransactionsResponse) Reset() { - *x = ListPortfolioTransactionsResponse{} - mi := &file_mgo_proto_msgTypes[11] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) -} - -func (x *ListPortfolioTransactionsResponse) String() string { - return protoimpl.X.MessageStringOf(x) -} - -func (*ListPortfolioTransactionsResponse) ProtoMessage() {} - -func (x *ListPortfolioTransactionsResponse) ProtoReflect() protoreflect.Message { - mi := &file_mgo_proto_msgTypes[11] - if x != nil { - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - if ms.LoadMessageInfo() == nil { - ms.StoreMessageInfo(mi) - } - return ms - } - return mi.MessageOf(x) -} - -// Deprecated: Use ListPortfolioTransactionsResponse.ProtoReflect.Descriptor instead. -func (*ListPortfolioTransactionsResponse) Descriptor() ([]byte, []int) { - return file_mgo_proto_rawDescGZIP(), []int{11} -} - -func (x *ListPortfolioTransactionsResponse) GetTransactions() []*PortfolioEvent { - if x != nil { - return x.Transactions - } - return nil -} - -type UpdatePortfolioTransactionRequest struct { - state protoimpl.MessageState `protogen:"open.v1"` - Transaction *PortfolioEvent `protobuf:"bytes,1,opt,name=transaction,proto3" json:"transaction,omitempty"` - UpdateMask *fieldmaskpb.FieldMask `protobuf:"bytes,2,opt,name=updateMask,proto3" json:"updateMask,omitempty"` - unknownFields protoimpl.UnknownFields - sizeCache protoimpl.SizeCache -} - -func (x *UpdatePortfolioTransactionRequest) Reset() { - *x = UpdatePortfolioTransactionRequest{} - mi := &file_mgo_proto_msgTypes[12] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) -} - -func (x *UpdatePortfolioTransactionRequest) String() string { - return protoimpl.X.MessageStringOf(x) -} - -func (*UpdatePortfolioTransactionRequest) ProtoMessage() {} - -func (x *UpdatePortfolioTransactionRequest) ProtoReflect() protoreflect.Message { - mi := &file_mgo_proto_msgTypes[12] - if x != nil { - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - if ms.LoadMessageInfo() == nil { - ms.StoreMessageInfo(mi) - } - return ms - } - return mi.MessageOf(x) -} - -// Deprecated: Use UpdatePortfolioTransactionRequest.ProtoReflect.Descriptor instead. -func (*UpdatePortfolioTransactionRequest) Descriptor() ([]byte, []int) { - return file_mgo_proto_rawDescGZIP(), []int{12} -} - -func (x *UpdatePortfolioTransactionRequest) GetTransaction() *PortfolioEvent { - if x != nil { - return x.Transaction - } - return nil -} - -func (x *UpdatePortfolioTransactionRequest) GetUpdateMask() *fieldmaskpb.FieldMask { - if x != nil { - return x.UpdateMask - } - return nil -} - -type DeletePortfolioTransactionRequest struct { - state protoimpl.MessageState `protogen:"open.v1"` - TransactionId int32 `protobuf:"varint,1,opt,name=transaction_id,json=transactionId,proto3" json:"transaction_id,omitempty"` - unknownFields protoimpl.UnknownFields - sizeCache protoimpl.SizeCache -} - -func (x *DeletePortfolioTransactionRequest) Reset() { - *x = DeletePortfolioTransactionRequest{} - mi := &file_mgo_proto_msgTypes[13] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) -} - -func (x *DeletePortfolioTransactionRequest) String() string { - return protoimpl.X.MessageStringOf(x) -} - -func (*DeletePortfolioTransactionRequest) ProtoMessage() {} - -func (x *DeletePortfolioTransactionRequest) ProtoReflect() protoreflect.Message { - mi := &file_mgo_proto_msgTypes[13] - if x != nil { - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - if ms.LoadMessageInfo() == nil { - ms.StoreMessageInfo(mi) - } - return ms - } - return mi.MessageOf(x) -} - -// Deprecated: Use DeletePortfolioTransactionRequest.ProtoReflect.Descriptor instead. -func (*DeletePortfolioTransactionRequest) Descriptor() ([]byte, []int) { - return file_mgo_proto_rawDescGZIP(), []int{13} -} - -func (x *DeletePortfolioTransactionRequest) GetTransactionId() int32 { - if x != nil { - return x.TransactionId - } - return 0 -} - -type ImportTransactionsRequest struct { - state protoimpl.MessageState `protogen:"open.v1"` - PortfolioId string `protobuf:"bytes,1,opt,name=portfolio_id,json=portfolioId,proto3" json:"portfolio_id,omitempty"` - FromCsv string `protobuf:"bytes,2,opt,name=from_csv,json=fromCsv,proto3" json:"from_csv,omitempty"` - unknownFields protoimpl.UnknownFields - sizeCache protoimpl.SizeCache -} - -func (x *ImportTransactionsRequest) Reset() { - *x = ImportTransactionsRequest{} - mi := &file_mgo_proto_msgTypes[14] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) -} - -func (x *ImportTransactionsRequest) String() string { - return protoimpl.X.MessageStringOf(x) -} - -func (*ImportTransactionsRequest) ProtoMessage() {} - -func (x *ImportTransactionsRequest) ProtoReflect() protoreflect.Message { - mi := &file_mgo_proto_msgTypes[14] - if x != nil { - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - if ms.LoadMessageInfo() == nil { - ms.StoreMessageInfo(mi) - } - return ms - } - return mi.MessageOf(x) -} - -// Deprecated: Use ImportTransactionsRequest.ProtoReflect.Descriptor instead. -func (*ImportTransactionsRequest) Descriptor() ([]byte, []int) { - return file_mgo_proto_rawDescGZIP(), []int{14} -} - -func (x *ImportTransactionsRequest) GetPortfolioId() string { - if x != nil { - return x.PortfolioId - } - return "" -} - -func (x *ImportTransactionsRequest) GetFromCsv() string { - if x != nil { - return x.FromCsv - } - return "" -} - -type CreateBankAccountRequest struct { - state protoimpl.MessageState `protogen:"open.v1"` - BankAccount *BankAccount `protobuf:"bytes,1,opt,name=bank_account,json=bankAccount,proto3" json:"bank_account,omitempty"` - unknownFields protoimpl.UnknownFields - sizeCache protoimpl.SizeCache -} - -func (x *CreateBankAccountRequest) Reset() { - *x = CreateBankAccountRequest{} - mi := &file_mgo_proto_msgTypes[15] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) -} - -func (x *CreateBankAccountRequest) String() string { - return protoimpl.X.MessageStringOf(x) -} - -func (*CreateBankAccountRequest) ProtoMessage() {} - -func (x *CreateBankAccountRequest) ProtoReflect() protoreflect.Message { - mi := &file_mgo_proto_msgTypes[15] - if x != nil { - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - if ms.LoadMessageInfo() == nil { - ms.StoreMessageInfo(mi) - } - return ms - } - return mi.MessageOf(x) -} - -// Deprecated: Use CreateBankAccountRequest.ProtoReflect.Descriptor instead. -func (*CreateBankAccountRequest) Descriptor() ([]byte, []int) { - return file_mgo_proto_rawDescGZIP(), []int{15} -} - -func (x *CreateBankAccountRequest) GetBankAccount() *BankAccount { - if x != nil { - return x.BankAccount - } - return nil -} - -type UpdateBankAccountRequest struct { - state protoimpl.MessageState `protogen:"open.v1"` - Account *BankAccount `protobuf:"bytes,1,opt,name=account,proto3" json:"account,omitempty"` - UpdateMask *fieldmaskpb.FieldMask `protobuf:"bytes,2,opt,name=updateMask,proto3" json:"updateMask,omitempty"` - unknownFields protoimpl.UnknownFields - sizeCache protoimpl.SizeCache -} - -func (x *UpdateBankAccountRequest) Reset() { - *x = UpdateBankAccountRequest{} - mi := &file_mgo_proto_msgTypes[16] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) -} - -func (x *UpdateBankAccountRequest) String() string { - return protoimpl.X.MessageStringOf(x) -} - -func (*UpdateBankAccountRequest) ProtoMessage() {} - -func (x *UpdateBankAccountRequest) ProtoReflect() protoreflect.Message { - mi := &file_mgo_proto_msgTypes[16] - if x != nil { - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - if ms.LoadMessageInfo() == nil { - ms.StoreMessageInfo(mi) - } - return ms - } - return mi.MessageOf(x) -} - -// Deprecated: Use UpdateBankAccountRequest.ProtoReflect.Descriptor instead. -func (*UpdateBankAccountRequest) Descriptor() ([]byte, []int) { - return file_mgo_proto_rawDescGZIP(), []int{16} -} - -func (x *UpdateBankAccountRequest) GetAccount() *BankAccount { - if x != nil { - return x.Account - } - return nil -} - -func (x *UpdateBankAccountRequest) GetUpdateMask() *fieldmaskpb.FieldMask { - if x != nil { - return x.UpdateMask - } - return nil -} - -type DeleteBankAccountRequest struct { - state protoimpl.MessageState `protogen:"open.v1"` - Id string `protobuf:"bytes,1,opt,name=id,proto3" json:"id,omitempty"` - unknownFields protoimpl.UnknownFields - sizeCache protoimpl.SizeCache -} - -func (x *DeleteBankAccountRequest) Reset() { - *x = DeleteBankAccountRequest{} - mi := &file_mgo_proto_msgTypes[17] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) -} - -func (x *DeleteBankAccountRequest) String() string { - return protoimpl.X.MessageStringOf(x) -} - -func (*DeleteBankAccountRequest) ProtoMessage() {} - -func (x *DeleteBankAccountRequest) ProtoReflect() protoreflect.Message { - mi := &file_mgo_proto_msgTypes[17] - if x != nil { - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - if ms.LoadMessageInfo() == nil { - ms.StoreMessageInfo(mi) - } - return ms - } - return mi.MessageOf(x) -} - -// Deprecated: Use DeleteBankAccountRequest.ProtoReflect.Descriptor instead. -func (*DeleteBankAccountRequest) Descriptor() ([]byte, []int) { - return file_mgo_proto_rawDescGZIP(), []int{17} -} - -func (x *DeleteBankAccountRequest) GetId() string { - if x != nil { - return x.Id - } - return "" -} - -type Portfolio struct { - state protoimpl.MessageState `protogen:"open.v1"` - Id string `protobuf:"bytes,1,opt,name=id,proto3" json:"id,omitempty"` - DisplayName string `protobuf:"bytes,2,opt,name=display_name,json=displayName,proto3" json:"display_name,omitempty"` - // BankAccountId contains the id/identifier of the underlying bank - // account. - BankAccountId string `protobuf:"bytes,3,opt,name=bank_account_id,json=bankAccountId,proto3" json:"bank_account_id,omitempty"` - // Events contains all portfolio events, such as buy/sell transactions, - // dividends or other. They need to be ordered by time (ascending). - Events []*PortfolioEvent `protobuf:"bytes,5,rep,name=events,proto3" json:"events,omitempty"` - unknownFields protoimpl.UnknownFields - sizeCache protoimpl.SizeCache -} - -func (x *Portfolio) Reset() { - *x = Portfolio{} - mi := &file_mgo_proto_msgTypes[18] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) -} - -func (x *Portfolio) String() string { - return protoimpl.X.MessageStringOf(x) -} - -func (*Portfolio) ProtoMessage() {} - -func (x *Portfolio) ProtoReflect() protoreflect.Message { - mi := &file_mgo_proto_msgTypes[18] - if x != nil { - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - if ms.LoadMessageInfo() == nil { - ms.StoreMessageInfo(mi) - } - return ms - } - return mi.MessageOf(x) -} - -// Deprecated: Use Portfolio.ProtoReflect.Descriptor instead. -func (*Portfolio) Descriptor() ([]byte, []int) { - return file_mgo_proto_rawDescGZIP(), []int{18} -} - -func (x *Portfolio) GetId() string { - if x != nil { - return x.Id - } - return "" -} - -func (x *Portfolio) GetDisplayName() string { - if x != nil { - return x.DisplayName - } - return "" -} - -func (x *Portfolio) GetBankAccountId() string { - if x != nil { - return x.BankAccountId - } - return "" -} - -func (x *Portfolio) GetEvents() []*PortfolioEvent { - if x != nil { - return x.Events - } - return nil -} - -type BankAccount struct { - state protoimpl.MessageState `protogen:"open.v1"` - Id string `protobuf:"bytes,1,opt,name=id,proto3" json:"id,omitempty"` - DisplayName string `protobuf:"bytes,2,opt,name=display_name,json=displayName,proto3" json:"display_name,omitempty"` - unknownFields protoimpl.UnknownFields - sizeCache protoimpl.SizeCache -} - -func (x *BankAccount) Reset() { - *x = BankAccount{} - mi := &file_mgo_proto_msgTypes[19] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) -} - -func (x *BankAccount) String() string { - return protoimpl.X.MessageStringOf(x) -} - -func (*BankAccount) ProtoMessage() {} - -func (x *BankAccount) ProtoReflect() protoreflect.Message { - mi := &file_mgo_proto_msgTypes[19] - if x != nil { - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - if ms.LoadMessageInfo() == nil { - ms.StoreMessageInfo(mi) - } - return ms - } - return mi.MessageOf(x) -} - -// Deprecated: Use BankAccount.ProtoReflect.Descriptor instead. -func (*BankAccount) Descriptor() ([]byte, []int) { - return file_mgo_proto_rawDescGZIP(), []int{19} -} - -func (x *BankAccount) GetId() string { - if x != nil { - return x.Id - } - return "" -} - -func (x *BankAccount) GetDisplayName() string { - if x != nil { - return x.DisplayName - } - return "" -} - -// PortfolioSnapshot represents a snapshot in time of the portfolio. It can for -// example be the current state of the portfolio but also represent the state of -// the portfolio at a certain time in the past. -type PortfolioSnapshot struct { - state protoimpl.MessageState `protogen:"open.v1"` - // Time is the time when this snapshot was taken. - Time *timestamppb.Timestamp `protobuf:"bytes,1,opt,name=time,proto3" json:"time,omitempty"` - // Positions holds the current positions within the snapshot and their value. - Positions map[string]*PortfolioPosition `protobuf:"bytes,2,rep,name=positions,proto3" json:"positions,omitempty" protobuf_key:"bytes,1,opt,name=key" protobuf_val:"bytes,2,opt,name=value"` - // FirstTransactionTime is the time of the first transaction with the - // snapshot. - FirstTransactionTime *timestamppb.Timestamp `protobuf:"bytes,3,opt,name=first_transaction_time,json=firstTransactionTime,proto3,oneof" json:"first_transaction_time,omitempty"` - // TotalPurchaseValue contains the total purchase value of all asset positions - TotalPurchaseValue *Currency `protobuf:"bytes,10,opt,name=total_purchase_value,json=totalPurchaseValue,proto3" json:"total_purchase_value,omitempty"` - // TotalMarketValue contains the total market value of all asset positions - TotalMarketValue *Currency `protobuf:"bytes,11,opt,name=total_market_value,json=totalMarketValue,proto3" json:"total_market_value,omitempty"` - // TotalProfitOrLoss contains the total absolute amount of profit or loss in - // this snapshot, based on asset value. - TotalProfitOrLoss *Currency `protobuf:"bytes,20,opt,name=total_profit_or_loss,json=totalProfitOrLoss,proto3" json:"total_profit_or_loss,omitempty"` - // TotalGains contains the total relative amount of profit or loss in this - // snapshot, based on asset value. - TotalGains float64 `protobuf:"fixed64,21,opt,name=total_gains,json=totalGains,proto3" json:"total_gains,omitempty"` - // Cash contains the current amount of cash in the portfolio's bank - // account(s). - Cash *Currency `protobuf:"bytes,22,opt,name=cash,proto3" json:"cash,omitempty"` - // TotalPortfolioValue contains the amount of cash plus the total market value - // of all assets. - TotalPortfolioValue *Currency `protobuf:"bytes,23,opt,name=total_portfolio_value,json=totalPortfolioValue,proto3" json:"total_portfolio_value,omitempty"` - unknownFields protoimpl.UnknownFields - sizeCache protoimpl.SizeCache -} - -func (x *PortfolioSnapshot) Reset() { - *x = PortfolioSnapshot{} - mi := &file_mgo_proto_msgTypes[20] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) -} - -func (x *PortfolioSnapshot) String() string { - return protoimpl.X.MessageStringOf(x) -} - -func (*PortfolioSnapshot) ProtoMessage() {} - -func (x *PortfolioSnapshot) ProtoReflect() protoreflect.Message { - mi := &file_mgo_proto_msgTypes[20] - if x != nil { - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - if ms.LoadMessageInfo() == nil { - ms.StoreMessageInfo(mi) - } - return ms - } - return mi.MessageOf(x) -} - -// Deprecated: Use PortfolioSnapshot.ProtoReflect.Descriptor instead. -func (*PortfolioSnapshot) Descriptor() ([]byte, []int) { - return file_mgo_proto_rawDescGZIP(), []int{20} -} - -func (x *PortfolioSnapshot) GetTime() *timestamppb.Timestamp { - if x != nil { - return x.Time - } - return nil -} - -func (x *PortfolioSnapshot) GetPositions() map[string]*PortfolioPosition { - if x != nil { - return x.Positions - } - return nil -} - -func (x *PortfolioSnapshot) GetFirstTransactionTime() *timestamppb.Timestamp { - if x != nil { - return x.FirstTransactionTime - } - return nil -} - -func (x *PortfolioSnapshot) GetTotalPurchaseValue() *Currency { - if x != nil { - return x.TotalPurchaseValue - } - return nil -} - -func (x *PortfolioSnapshot) GetTotalMarketValue() *Currency { - if x != nil { - return x.TotalMarketValue - } - return nil -} - -func (x *PortfolioSnapshot) GetTotalProfitOrLoss() *Currency { - if x != nil { - return x.TotalProfitOrLoss - } - return nil -} - -func (x *PortfolioSnapshot) GetTotalGains() float64 { - if x != nil { - return x.TotalGains - } - return 0 -} - -func (x *PortfolioSnapshot) GetCash() *Currency { - if x != nil { - return x.Cash - } - return nil -} - -func (x *PortfolioSnapshot) GetTotalPortfolioValue() *Currency { - if x != nil { - return x.TotalPortfolioValue - } - return nil -} - -type PortfolioPosition struct { - state protoimpl.MessageState `protogen:"open.v1"` - Security *Security `protobuf:"bytes,1,opt,name=security,proto3" json:"security,omitempty"` - Amount float64 `protobuf:"fixed64,2,opt,name=amount,proto3" json:"amount,omitempty"` - // PurchaseValue was the market value of this position when it was bought - // (net; exclusive of any fees). - PurchaseValue *Currency `protobuf:"bytes,5,opt,name=purchase_value,json=purchaseValue,proto3" json:"purchase_value,omitempty"` - // PurchasePrice was the market price of this position when it was bought - // (net; exclusive of any fees). - PurchasePrice *Currency `protobuf:"bytes,6,opt,name=purchase_price,json=purchasePrice,proto3" json:"purchase_price,omitempty"` - // MarketValue is the current market value of this position, as retrieved from - // the securities service. - MarketValue *Currency `protobuf:"bytes,10,opt,name=market_value,json=marketValue,proto3" json:"market_value,omitempty"` - // MarketPrice is the current market price of this position, as retrieved from - // the securities service. - MarketPrice *Currency `protobuf:"bytes,11,opt,name=market_price,json=marketPrice,proto3" json:"market_price,omitempty"` - // TotalFees is the total amount of fees accumulating in this position through - // various transactions. - TotalFees *Currency `protobuf:"bytes,15,opt,name=total_fees,json=totalFees,proto3" json:"total_fees,omitempty"` - // ProfitOrLoss contains the absolute amount of profit or loss in this - // position. - ProfitOrLoss *Currency `protobuf:"bytes,20,opt,name=profit_or_loss,json=profitOrLoss,proto3" json:"profit_or_loss,omitempty"` - // Gains contains the relative amount of profit or loss in this position. - Gains float64 `protobuf:"fixed64,21,opt,name=gains,proto3" json:"gains,omitempty"` - unknownFields protoimpl.UnknownFields - sizeCache protoimpl.SizeCache -} - -func (x *PortfolioPosition) Reset() { - *x = PortfolioPosition{} - mi := &file_mgo_proto_msgTypes[21] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) -} - -func (x *PortfolioPosition) String() string { - return protoimpl.X.MessageStringOf(x) -} - -func (*PortfolioPosition) ProtoMessage() {} - -func (x *PortfolioPosition) ProtoReflect() protoreflect.Message { - mi := &file_mgo_proto_msgTypes[21] - if x != nil { - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - if ms.LoadMessageInfo() == nil { - ms.StoreMessageInfo(mi) - } - return ms - } - return mi.MessageOf(x) -} - -// Deprecated: Use PortfolioPosition.ProtoReflect.Descriptor instead. -func (*PortfolioPosition) Descriptor() ([]byte, []int) { - return file_mgo_proto_rawDescGZIP(), []int{21} -} - -func (x *PortfolioPosition) GetSecurity() *Security { - if x != nil { - return x.Security - } - return nil -} - -func (x *PortfolioPosition) GetAmount() float64 { - if x != nil { - return x.Amount - } - return 0 -} - -func (x *PortfolioPosition) GetPurchaseValue() *Currency { - if x != nil { - return x.PurchaseValue - } - return nil -} - -func (x *PortfolioPosition) GetPurchasePrice() *Currency { - if x != nil { - return x.PurchasePrice - } - return nil -} - -func (x *PortfolioPosition) GetMarketValue() *Currency { - if x != nil { - return x.MarketValue - } - return nil -} - -func (x *PortfolioPosition) GetMarketPrice() *Currency { - if x != nil { - return x.MarketPrice - } - return nil -} - -func (x *PortfolioPosition) GetTotalFees() *Currency { - if x != nil { - return x.TotalFees - } - return nil -} - -func (x *PortfolioPosition) GetProfitOrLoss() *Currency { - if x != nil { - return x.ProfitOrLoss - } - return nil -} - -func (x *PortfolioPosition) GetGains() float64 { - if x != nil { - return x.Gains - } - return 0 -} - -type PortfolioEvent struct { - state protoimpl.MessageState `protogen:"open.v1"` - Id string `protobuf:"bytes,1,opt,name=id,proto3" json:"id,omitempty"` - Type PortfolioEventType `protobuf:"varint,2,opt,name=type,proto3,enum=mgo.portfolio.v1.PortfolioEventType" json:"type,omitempty"` - Time *timestamppb.Timestamp `protobuf:"bytes,3,opt,name=time,proto3" json:"time,omitempty"` - PortfolioId string `protobuf:"bytes,4,opt,name=portfolio_id,json=portfolioId,proto3" json:"portfolio_id,omitempty"` - SecurityId string `protobuf:"bytes,5,opt,name=security_id,json=securityId,proto3" json:"security_id,omitempty"` - Amount float64 `protobuf:"fixed64,10,opt,name=amount,proto3" json:"amount,omitempty"` - Price *Currency `protobuf:"bytes,11,opt,name=price,proto3" json:"price,omitempty"` - Fees *Currency `protobuf:"bytes,12,opt,name=fees,proto3" json:"fees,omitempty"` - Taxes *Currency `protobuf:"bytes,13,opt,name=taxes,proto3" json:"taxes,omitempty"` - unknownFields protoimpl.UnknownFields - sizeCache protoimpl.SizeCache -} - -func (x *PortfolioEvent) Reset() { - *x = PortfolioEvent{} - mi := &file_mgo_proto_msgTypes[22] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) -} - -func (x *PortfolioEvent) String() string { - return protoimpl.X.MessageStringOf(x) -} - -func (*PortfolioEvent) ProtoMessage() {} - -func (x *PortfolioEvent) ProtoReflect() protoreflect.Message { - mi := &file_mgo_proto_msgTypes[22] - if x != nil { - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - if ms.LoadMessageInfo() == nil { - ms.StoreMessageInfo(mi) - } - return ms - } - return mi.MessageOf(x) -} - -// Deprecated: Use PortfolioEvent.ProtoReflect.Descriptor instead. -func (*PortfolioEvent) Descriptor() ([]byte, []int) { - return file_mgo_proto_rawDescGZIP(), []int{22} -} - -func (x *PortfolioEvent) GetId() string { - if x != nil { - return x.Id - } - return "" -} - -func (x *PortfolioEvent) GetType() PortfolioEventType { - if x != nil { - return x.Type - } - return PortfolioEventType_PORTFOLIO_EVENT_TYPE_UNSPECIFIED -} - -func (x *PortfolioEvent) GetTime() *timestamppb.Timestamp { - if x != nil { - return x.Time - } - return nil -} - -func (x *PortfolioEvent) GetPortfolioId() string { - if x != nil { - return x.PortfolioId - } - return "" -} - -func (x *PortfolioEvent) GetSecurityId() string { - if x != nil { - return x.SecurityId - } - return "" -} - -func (x *PortfolioEvent) GetAmount() float64 { - if x != nil { - return x.Amount - } - return 0 -} - -func (x *PortfolioEvent) GetPrice() *Currency { - if x != nil { - return x.Price - } - return nil -} - -func (x *PortfolioEvent) GetFees() *Currency { - if x != nil { - return x.Fees - } - return nil -} - -func (x *PortfolioEvent) GetTaxes() *Currency { - if x != nil { - return x.Taxes - } - return nil -} - -type Security struct { - state protoimpl.MessageState `protogen:"open.v1"` - // Id contains the unique resource ID. For a stock or bond, this should be - // an ISIN. - Id string `protobuf:"bytes,1,opt,name=id,proto3" json:"id,omitempty"` - // DisplayName contains the human readable id. - DisplayName string `protobuf:"bytes,2,opt,name=display_name,json=displayName,proto3" json:"display_name,omitempty"` - ListedOn []*ListedSecurity `protobuf:"bytes,4,rep,name=listed_on,json=listedOn,proto3" json:"listed_on,omitempty"` - QuoteProvider *string `protobuf:"bytes,10,opt,name=quote_provider,json=quoteProvider,proto3,oneof" json:"quote_provider,omitempty"` - unknownFields protoimpl.UnknownFields - sizeCache protoimpl.SizeCache -} - -func (x *Security) Reset() { - *x = Security{} - mi := &file_mgo_proto_msgTypes[23] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) -} - -func (x *Security) String() string { - return protoimpl.X.MessageStringOf(x) -} - -func (*Security) ProtoMessage() {} - -func (x *Security) ProtoReflect() protoreflect.Message { - mi := &file_mgo_proto_msgTypes[23] - if x != nil { - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - if ms.LoadMessageInfo() == nil { - ms.StoreMessageInfo(mi) - } - return ms - } - return mi.MessageOf(x) -} - -// Deprecated: Use Security.ProtoReflect.Descriptor instead. -func (*Security) Descriptor() ([]byte, []int) { - return file_mgo_proto_rawDescGZIP(), []int{23} -} - -func (x *Security) GetId() string { - if x != nil { - return x.Id - } - return "" -} - -func (x *Security) GetDisplayName() string { - if x != nil { - return x.DisplayName - } - return "" -} - -func (x *Security) GetListedOn() []*ListedSecurity { - if x != nil { - return x.ListedOn - } - return nil -} - -func (x *Security) GetQuoteProvider() string { - if x != nil && x.QuoteProvider != nil { - return *x.QuoteProvider - } - return "" -} - -type ListedSecurity struct { - state protoimpl.MessageState `protogen:"open.v1"` - SecurityId string `protobuf:"bytes,1,opt,name=security_id,json=securityId,proto3" json:"security_id,omitempty"` - Ticker string `protobuf:"bytes,3,opt,name=ticker,proto3" json:"ticker,omitempty"` - Currency string `protobuf:"bytes,4,opt,name=currency,proto3" json:"currency,omitempty"` - LatestQuote *Currency `protobuf:"bytes,5,opt,name=latest_quote,json=latestQuote,proto3,oneof" json:"latest_quote,omitempty"` - LatestQuoteTimestamp *timestamppb.Timestamp `protobuf:"bytes,6,opt,name=latest_quote_timestamp,json=latestQuoteTimestamp,proto3,oneof" json:"latest_quote_timestamp,omitempty"` - unknownFields protoimpl.UnknownFields - sizeCache protoimpl.SizeCache -} - -func (x *ListedSecurity) Reset() { - *x = ListedSecurity{} - mi := &file_mgo_proto_msgTypes[24] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) -} - -func (x *ListedSecurity) String() string { - return protoimpl.X.MessageStringOf(x) -} - -func (*ListedSecurity) ProtoMessage() {} - -func (x *ListedSecurity) ProtoReflect() protoreflect.Message { - mi := &file_mgo_proto_msgTypes[24] - if x != nil { - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - if ms.LoadMessageInfo() == nil { - ms.StoreMessageInfo(mi) - } - return ms - } - return mi.MessageOf(x) -} - -// Deprecated: Use ListedSecurity.ProtoReflect.Descriptor instead. -func (*ListedSecurity) Descriptor() ([]byte, []int) { - return file_mgo_proto_rawDescGZIP(), []int{24} -} - -func (x *ListedSecurity) GetSecurityId() string { - if x != nil { - return x.SecurityId - } - return "" -} - -func (x *ListedSecurity) GetTicker() string { - if x != nil { - return x.Ticker - } - return "" -} - -func (x *ListedSecurity) GetCurrency() string { - if x != nil { - return x.Currency - } - return "" -} - -func (x *ListedSecurity) GetLatestQuote() *Currency { - if x != nil { - return x.LatestQuote - } - return nil -} - -func (x *ListedSecurity) GetLatestQuoteTimestamp() *timestamppb.Timestamp { - if x != nil { - return x.LatestQuoteTimestamp - } - return nil -} - -type ListSecuritiesRequest struct { - state protoimpl.MessageState `protogen:"open.v1"` - Filter *ListSecuritiesRequest_Filter `protobuf:"bytes,5,opt,name=filter,proto3,oneof" json:"filter,omitempty"` - unknownFields protoimpl.UnknownFields - sizeCache protoimpl.SizeCache -} - -func (x *ListSecuritiesRequest) Reset() { - *x = ListSecuritiesRequest{} - mi := &file_mgo_proto_msgTypes[25] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) -} - -func (x *ListSecuritiesRequest) String() string { - return protoimpl.X.MessageStringOf(x) -} - -func (*ListSecuritiesRequest) ProtoMessage() {} - -func (x *ListSecuritiesRequest) ProtoReflect() protoreflect.Message { - mi := &file_mgo_proto_msgTypes[25] - if x != nil { - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - if ms.LoadMessageInfo() == nil { - ms.StoreMessageInfo(mi) - } - return ms - } - return mi.MessageOf(x) -} - -// Deprecated: Use ListSecuritiesRequest.ProtoReflect.Descriptor instead. -func (*ListSecuritiesRequest) Descriptor() ([]byte, []int) { - return file_mgo_proto_rawDescGZIP(), []int{25} -} - -func (x *ListSecuritiesRequest) GetFilter() *ListSecuritiesRequest_Filter { - if x != nil { - return x.Filter - } - return nil -} - -type ListSecuritiesResponse struct { - state protoimpl.MessageState `protogen:"open.v1"` - Securities []*Security `protobuf:"bytes,1,rep,name=securities,proto3" json:"securities,omitempty"` - unknownFields protoimpl.UnknownFields - sizeCache protoimpl.SizeCache -} - -func (x *ListSecuritiesResponse) Reset() { - *x = ListSecuritiesResponse{} - mi := &file_mgo_proto_msgTypes[26] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) -} - -func (x *ListSecuritiesResponse) String() string { - return protoimpl.X.MessageStringOf(x) -} - -func (*ListSecuritiesResponse) ProtoMessage() {} - -func (x *ListSecuritiesResponse) ProtoReflect() protoreflect.Message { - mi := &file_mgo_proto_msgTypes[26] - if x != nil { - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - if ms.LoadMessageInfo() == nil { - ms.StoreMessageInfo(mi) - } - return ms - } - return mi.MessageOf(x) -} - -// Deprecated: Use ListSecuritiesResponse.ProtoReflect.Descriptor instead. -func (*ListSecuritiesResponse) Descriptor() ([]byte, []int) { - return file_mgo_proto_rawDescGZIP(), []int{26} -} - -func (x *ListSecuritiesResponse) GetSecurities() []*Security { - if x != nil { - return x.Securities - } - return nil -} - -type GetSecurityRequest struct { - state protoimpl.MessageState `protogen:"open.v1"` - Id string `protobuf:"bytes,1,opt,name=id,proto3" json:"id,omitempty"` - unknownFields protoimpl.UnknownFields - sizeCache protoimpl.SizeCache -} - -func (x *GetSecurityRequest) Reset() { - *x = GetSecurityRequest{} - mi := &file_mgo_proto_msgTypes[27] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) -} - -func (x *GetSecurityRequest) String() string { - return protoimpl.X.MessageStringOf(x) -} - -func (*GetSecurityRequest) ProtoMessage() {} - -func (x *GetSecurityRequest) ProtoReflect() protoreflect.Message { - mi := &file_mgo_proto_msgTypes[27] - if x != nil { - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - if ms.LoadMessageInfo() == nil { - ms.StoreMessageInfo(mi) - } - return ms - } - return mi.MessageOf(x) -} - -// Deprecated: Use GetSecurityRequest.ProtoReflect.Descriptor instead. -func (*GetSecurityRequest) Descriptor() ([]byte, []int) { - return file_mgo_proto_rawDescGZIP(), []int{27} -} - -func (x *GetSecurityRequest) GetId() string { - if x != nil { - return x.Id - } - return "" -} - -type CreateSecurityRequest struct { - state protoimpl.MessageState `protogen:"open.v1"` - Security *Security `protobuf:"bytes,1,opt,name=security,proto3" json:"security,omitempty"` - unknownFields protoimpl.UnknownFields - sizeCache protoimpl.SizeCache -} - -func (x *CreateSecurityRequest) Reset() { - *x = CreateSecurityRequest{} - mi := &file_mgo_proto_msgTypes[28] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) -} - -func (x *CreateSecurityRequest) String() string { - return protoimpl.X.MessageStringOf(x) -} - -func (*CreateSecurityRequest) ProtoMessage() {} - -func (x *CreateSecurityRequest) ProtoReflect() protoreflect.Message { - mi := &file_mgo_proto_msgTypes[28] - if x != nil { - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - if ms.LoadMessageInfo() == nil { - ms.StoreMessageInfo(mi) - } - return ms - } - return mi.MessageOf(x) -} - -// Deprecated: Use CreateSecurityRequest.ProtoReflect.Descriptor instead. -func (*CreateSecurityRequest) Descriptor() ([]byte, []int) { - return file_mgo_proto_rawDescGZIP(), []int{28} -} - -func (x *CreateSecurityRequest) GetSecurity() *Security { - if x != nil { - return x.Security - } - return nil -} - -type UpdateSecurityRequest struct { - state protoimpl.MessageState `protogen:"open.v1"` - Security *Security `protobuf:"bytes,1,opt,name=security,proto3" json:"security,omitempty"` - UpdateMask *fieldmaskpb.FieldMask `protobuf:"bytes,2,opt,name=updateMask,proto3" json:"updateMask,omitempty"` - unknownFields protoimpl.UnknownFields - sizeCache protoimpl.SizeCache -} - -func (x *UpdateSecurityRequest) Reset() { - *x = UpdateSecurityRequest{} - mi := &file_mgo_proto_msgTypes[29] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) -} - -func (x *UpdateSecurityRequest) String() string { - return protoimpl.X.MessageStringOf(x) -} - -func (*UpdateSecurityRequest) ProtoMessage() {} - -func (x *UpdateSecurityRequest) ProtoReflect() protoreflect.Message { - mi := &file_mgo_proto_msgTypes[29] - if x != nil { - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - if ms.LoadMessageInfo() == nil { - ms.StoreMessageInfo(mi) - } - return ms - } - return mi.MessageOf(x) -} - -// Deprecated: Use UpdateSecurityRequest.ProtoReflect.Descriptor instead. -func (*UpdateSecurityRequest) Descriptor() ([]byte, []int) { - return file_mgo_proto_rawDescGZIP(), []int{29} -} - -func (x *UpdateSecurityRequest) GetSecurity() *Security { - if x != nil { - return x.Security - } - return nil -} - -func (x *UpdateSecurityRequest) GetUpdateMask() *fieldmaskpb.FieldMask { - if x != nil { - return x.UpdateMask - } - return nil -} - -type DeleteSecurityRequest struct { - state protoimpl.MessageState `protogen:"open.v1"` - Id string `protobuf:"bytes,1,opt,name=id,proto3" json:"id,omitempty"` - unknownFields protoimpl.UnknownFields - sizeCache protoimpl.SizeCache -} - -func (x *DeleteSecurityRequest) Reset() { - *x = DeleteSecurityRequest{} - mi := &file_mgo_proto_msgTypes[30] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) -} - -func (x *DeleteSecurityRequest) String() string { - return protoimpl.X.MessageStringOf(x) -} - -func (*DeleteSecurityRequest) ProtoMessage() {} - -func (x *DeleteSecurityRequest) ProtoReflect() protoreflect.Message { - mi := &file_mgo_proto_msgTypes[30] - if x != nil { - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - if ms.LoadMessageInfo() == nil { - ms.StoreMessageInfo(mi) - } - return ms - } - return mi.MessageOf(x) -} - -// Deprecated: Use DeleteSecurityRequest.ProtoReflect.Descriptor instead. -func (*DeleteSecurityRequest) Descriptor() ([]byte, []int) { - return file_mgo_proto_rawDescGZIP(), []int{30} -} - -func (x *DeleteSecurityRequest) GetId() string { - if x != nil { - return x.Id - } - return "" -} - -type TriggerQuoteUpdateRequest struct { - state protoimpl.MessageState `protogen:"open.v1"` - SecurityIds []string `protobuf:"bytes,1,rep,name=security_ids,json=securityIds,proto3" json:"security_ids,omitempty"` - unknownFields protoimpl.UnknownFields - sizeCache protoimpl.SizeCache -} - -func (x *TriggerQuoteUpdateRequest) Reset() { - *x = TriggerQuoteUpdateRequest{} - mi := &file_mgo_proto_msgTypes[31] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) -} - -func (x *TriggerQuoteUpdateRequest) String() string { - return protoimpl.X.MessageStringOf(x) -} - -func (*TriggerQuoteUpdateRequest) ProtoMessage() {} - -func (x *TriggerQuoteUpdateRequest) ProtoReflect() protoreflect.Message { - mi := &file_mgo_proto_msgTypes[31] - if x != nil { - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - if ms.LoadMessageInfo() == nil { - ms.StoreMessageInfo(mi) - } - return ms - } - return mi.MessageOf(x) -} - -// Deprecated: Use TriggerQuoteUpdateRequest.ProtoReflect.Descriptor instead. -func (*TriggerQuoteUpdateRequest) Descriptor() ([]byte, []int) { - return file_mgo_proto_rawDescGZIP(), []int{31} -} - -func (x *TriggerQuoteUpdateRequest) GetSecurityIds() []string { - if x != nil { - return x.SecurityIds - } - return nil -} - -type TriggerQuoteUpdateResponse struct { - state protoimpl.MessageState `protogen:"open.v1"` - unknownFields protoimpl.UnknownFields - sizeCache protoimpl.SizeCache -} - -func (x *TriggerQuoteUpdateResponse) Reset() { - *x = TriggerQuoteUpdateResponse{} - mi := &file_mgo_proto_msgTypes[32] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) -} - -func (x *TriggerQuoteUpdateResponse) String() string { - return protoimpl.X.MessageStringOf(x) -} - -func (*TriggerQuoteUpdateResponse) ProtoMessage() {} - -func (x *TriggerQuoteUpdateResponse) ProtoReflect() protoreflect.Message { - mi := &file_mgo_proto_msgTypes[32] - if x != nil { - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - if ms.LoadMessageInfo() == nil { - ms.StoreMessageInfo(mi) - } - return ms - } - return mi.MessageOf(x) -} - -// Deprecated: Use TriggerQuoteUpdateResponse.ProtoReflect.Descriptor instead. -func (*TriggerQuoteUpdateResponse) Descriptor() ([]byte, []int) { - return file_mgo_proto_rawDescGZIP(), []int{32} -} - -type ListSecuritiesRequest_Filter struct { - state protoimpl.MessageState `protogen:"open.v1"` - SecurityIds []string `protobuf:"bytes,1,rep,name=security_ids,json=securityIds,proto3" json:"security_ids,omitempty"` - unknownFields protoimpl.UnknownFields - sizeCache protoimpl.SizeCache -} - -func (x *ListSecuritiesRequest_Filter) Reset() { - *x = ListSecuritiesRequest_Filter{} - mi := &file_mgo_proto_msgTypes[34] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) -} - -func (x *ListSecuritiesRequest_Filter) String() string { - return protoimpl.X.MessageStringOf(x) -} - -func (*ListSecuritiesRequest_Filter) ProtoMessage() {} - -func (x *ListSecuritiesRequest_Filter) ProtoReflect() protoreflect.Message { - mi := &file_mgo_proto_msgTypes[34] - if x != nil { - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - if ms.LoadMessageInfo() == nil { - ms.StoreMessageInfo(mi) - } - return ms - } - return mi.MessageOf(x) -} - -// Deprecated: Use ListSecuritiesRequest_Filter.ProtoReflect.Descriptor instead. -func (*ListSecuritiesRequest_Filter) Descriptor() ([]byte, []int) { - return file_mgo_proto_rawDescGZIP(), []int{25, 0} -} - -func (x *ListSecuritiesRequest_Filter) GetSecurityIds() []string { - if x != nil { - return x.SecurityIds - } - return nil -} - -var File_mgo_proto protoreflect.FileDescriptor - -var file_mgo_proto_rawDesc = []byte{ - 0x0a, 0x09, 0x6d, 0x67, 0x6f, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x12, 0x10, 0x6d, 0x67, 0x6f, - 0x2e, 0x70, 0x6f, 0x72, 0x74, 0x66, 0x6f, 0x6c, 0x69, 0x6f, 0x2e, 0x76, 0x31, 0x1a, 0x1c, 0x67, - 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2f, 0x61, 0x70, 0x69, 0x2f, 0x61, 0x6e, 0x6e, 0x6f, 0x74, 0x61, - 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x1a, 0x1f, 0x67, 0x6f, 0x6f, - 0x67, 0x6c, 0x65, 0x2f, 0x61, 0x70, 0x69, 0x2f, 0x66, 0x69, 0x65, 0x6c, 0x64, 0x5f, 0x62, 0x65, - 0x68, 0x61, 0x76, 0x69, 0x6f, 0x72, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x1a, 0x1b, 0x67, 0x6f, - 0x6f, 0x67, 0x6c, 0x65, 0x2f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2f, 0x65, 0x6d, - 0x70, 0x74, 0x79, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x1a, 0x20, 0x67, 0x6f, 0x6f, 0x67, 0x6c, - 0x65, 0x2f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2f, 0x66, 0x69, 0x65, 0x6c, 0x64, - 0x5f, 0x6d, 0x61, 0x73, 0x6b, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x1a, 0x1f, 0x67, 0x6f, 0x6f, - 0x67, 0x6c, 0x65, 0x2f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2f, 0x74, 0x69, 0x6d, - 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x22, 0x42, 0x0a, 0x08, - 0x43, 0x75, 0x72, 0x72, 0x65, 0x6e, 0x63, 0x79, 0x12, 0x19, 0x0a, 0x05, 0x76, 0x61, 0x6c, 0x75, - 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x05, 0x42, 0x03, 0xe0, 0x41, 0x02, 0x52, 0x05, 0x76, 0x61, - 0x6c, 0x75, 0x65, 0x12, 0x1b, 0x0a, 0x06, 0x73, 0x79, 0x6d, 0x62, 0x6f, 0x6c, 0x18, 0x02, 0x20, - 0x01, 0x28, 0x09, 0x42, 0x03, 0xe0, 0x41, 0x02, 0x52, 0x06, 0x73, 0x79, 0x6d, 0x62, 0x6f, 0x6c, - 0x22, 0x58, 0x0a, 0x16, 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, 0x50, 0x6f, 0x72, 0x74, 0x66, 0x6f, - 0x6c, 0x69, 0x6f, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x3e, 0x0a, 0x09, 0x70, 0x6f, - 0x72, 0x74, 0x66, 0x6f, 0x6c, 0x69, 0x6f, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1b, 0x2e, - 0x6d, 0x67, 0x6f, 0x2e, 0x70, 0x6f, 0x72, 0x74, 0x66, 0x6f, 0x6c, 0x69, 0x6f, 0x2e, 0x76, 0x31, - 0x2e, 0x50, 0x6f, 0x72, 0x74, 0x66, 0x6f, 0x6c, 0x69, 0x6f, 0x42, 0x03, 0xe0, 0x41, 0x02, 0x52, - 0x09, 0x70, 0x6f, 0x72, 0x74, 0x66, 0x6f, 0x6c, 0x69, 0x6f, 0x22, 0x17, 0x0a, 0x15, 0x4c, 0x69, - 0x73, 0x74, 0x50, 0x6f, 0x72, 0x74, 0x66, 0x6f, 0x6c, 0x69, 0x6f, 0x73, 0x52, 0x65, 0x71, 0x75, - 0x65, 0x73, 0x74, 0x22, 0x5a, 0x0a, 0x16, 0x4c, 0x69, 0x73, 0x74, 0x50, 0x6f, 0x72, 0x74, 0x66, - 0x6f, 0x6c, 0x69, 0x6f, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x40, 0x0a, - 0x0a, 0x70, 0x6f, 0x72, 0x74, 0x66, 0x6f, 0x6c, 0x69, 0x6f, 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, - 0x0b, 0x32, 0x1b, 0x2e, 0x6d, 0x67, 0x6f, 0x2e, 0x70, 0x6f, 0x72, 0x74, 0x66, 0x6f, 0x6c, 0x69, - 0x6f, 0x2e, 0x76, 0x31, 0x2e, 0x50, 0x6f, 0x72, 0x74, 0x66, 0x6f, 0x6c, 0x69, 0x6f, 0x42, 0x03, - 0xe0, 0x41, 0x02, 0x52, 0x0a, 0x70, 0x6f, 0x72, 0x74, 0x66, 0x6f, 0x6c, 0x69, 0x6f, 0x73, 0x22, - 0x2a, 0x0a, 0x13, 0x47, 0x65, 0x74, 0x50, 0x6f, 0x72, 0x74, 0x66, 0x6f, 0x6c, 0x69, 0x6f, 0x52, - 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x13, 0x0a, 0x02, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, - 0x28, 0x09, 0x42, 0x03, 0xe0, 0x41, 0x02, 0x52, 0x02, 0x69, 0x64, 0x22, 0x99, 0x01, 0x0a, 0x16, - 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x50, 0x6f, 0x72, 0x74, 0x66, 0x6f, 0x6c, 0x69, 0x6f, 0x52, - 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x3e, 0x0a, 0x09, 0x70, 0x6f, 0x72, 0x74, 0x66, 0x6f, - 0x6c, 0x69, 0x6f, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1b, 0x2e, 0x6d, 0x67, 0x6f, 0x2e, - 0x70, 0x6f, 0x72, 0x74, 0x66, 0x6f, 0x6c, 0x69, 0x6f, 0x2e, 0x76, 0x31, 0x2e, 0x50, 0x6f, 0x72, - 0x74, 0x66, 0x6f, 0x6c, 0x69, 0x6f, 0x42, 0x03, 0xe0, 0x41, 0x02, 0x52, 0x09, 0x70, 0x6f, 0x72, - 0x74, 0x66, 0x6f, 0x6c, 0x69, 0x6f, 0x12, 0x3f, 0x0a, 0x0a, 0x75, 0x70, 0x64, 0x61, 0x74, 0x65, - 0x4d, 0x61, 0x73, 0x6b, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1a, 0x2e, 0x67, 0x6f, 0x6f, - 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x46, 0x69, 0x65, - 0x6c, 0x64, 0x4d, 0x61, 0x73, 0x6b, 0x42, 0x03, 0xe0, 0x41, 0x02, 0x52, 0x0a, 0x75, 0x70, 0x64, - 0x61, 0x74, 0x65, 0x4d, 0x61, 0x73, 0x6b, 0x22, 0x2d, 0x0a, 0x16, 0x44, 0x65, 0x6c, 0x65, 0x74, - 0x65, 0x50, 0x6f, 0x72, 0x74, 0x66, 0x6f, 0x6c, 0x69, 0x6f, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, - 0x74, 0x12, 0x13, 0x0a, 0x02, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x42, 0x03, 0xe0, - 0x41, 0x02, 0x52, 0x02, 0x69, 0x64, 0x22, 0x7a, 0x0a, 0x1b, 0x47, 0x65, 0x74, 0x50, 0x6f, 0x72, - 0x74, 0x66, 0x6f, 0x6c, 0x69, 0x6f, 0x53, 0x6e, 0x61, 0x70, 0x73, 0x68, 0x6f, 0x74, 0x52, 0x65, - 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x26, 0x0a, 0x0c, 0x70, 0x6f, 0x72, 0x74, 0x66, 0x6f, 0x6c, - 0x69, 0x6f, 0x5f, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x42, 0x03, 0xe0, 0x41, 0x02, - 0x52, 0x0b, 0x70, 0x6f, 0x72, 0x74, 0x66, 0x6f, 0x6c, 0x69, 0x6f, 0x49, 0x64, 0x12, 0x33, 0x0a, - 0x04, 0x74, 0x69, 0x6d, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1a, 0x2e, 0x67, 0x6f, - 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x54, 0x69, - 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x42, 0x03, 0xe0, 0x41, 0x02, 0x52, 0x04, 0x74, 0x69, - 0x6d, 0x65, 0x22, 0x6c, 0x0a, 0x21, 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, 0x50, 0x6f, 0x72, 0x74, - 0x66, 0x6f, 0x6c, 0x69, 0x6f, 0x54, 0x72, 0x61, 0x6e, 0x73, 0x61, 0x63, 0x74, 0x69, 0x6f, 0x6e, - 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x47, 0x0a, 0x0b, 0x74, 0x72, 0x61, 0x6e, 0x73, - 0x61, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x20, 0x2e, 0x6d, - 0x67, 0x6f, 0x2e, 0x70, 0x6f, 0x72, 0x74, 0x66, 0x6f, 0x6c, 0x69, 0x6f, 0x2e, 0x76, 0x31, 0x2e, - 0x50, 0x6f, 0x72, 0x74, 0x66, 0x6f, 0x6c, 0x69, 0x6f, 0x45, 0x76, 0x65, 0x6e, 0x74, 0x42, 0x03, - 0xe0, 0x41, 0x02, 0x52, 0x0b, 0x74, 0x72, 0x61, 0x6e, 0x73, 0x61, 0x63, 0x74, 0x69, 0x6f, 0x6e, - 0x22, 0x35, 0x0a, 0x1e, 0x47, 0x65, 0x74, 0x50, 0x6f, 0x72, 0x74, 0x66, 0x6f, 0x6c, 0x69, 0x6f, - 0x54, 0x72, 0x61, 0x6e, 0x73, 0x61, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x52, 0x65, 0x71, 0x75, 0x65, - 0x73, 0x74, 0x12, 0x13, 0x0a, 0x02, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x42, 0x03, - 0xe0, 0x41, 0x02, 0x52, 0x02, 0x69, 0x64, 0x22, 0x4a, 0x0a, 0x20, 0x4c, 0x69, 0x73, 0x74, 0x50, - 0x6f, 0x72, 0x74, 0x66, 0x6f, 0x6c, 0x69, 0x6f, 0x54, 0x72, 0x61, 0x6e, 0x73, 0x61, 0x63, 0x74, - 0x69, 0x6f, 0x6e, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x26, 0x0a, 0x0c, 0x70, - 0x6f, 0x72, 0x74, 0x66, 0x6f, 0x6c, 0x69, 0x6f, 0x5f, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, - 0x09, 0x42, 0x03, 0xe0, 0x41, 0x02, 0x52, 0x0b, 0x70, 0x6f, 0x72, 0x74, 0x66, 0x6f, 0x6c, 0x69, - 0x6f, 0x49, 0x64, 0x22, 0x6e, 0x0a, 0x21, 0x4c, 0x69, 0x73, 0x74, 0x50, 0x6f, 0x72, 0x74, 0x66, - 0x6f, 0x6c, 0x69, 0x6f, 0x54, 0x72, 0x61, 0x6e, 0x73, 0x61, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x73, - 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x49, 0x0a, 0x0c, 0x74, 0x72, 0x61, 0x6e, - 0x73, 0x61, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x20, - 0x2e, 0x6d, 0x67, 0x6f, 0x2e, 0x70, 0x6f, 0x72, 0x74, 0x66, 0x6f, 0x6c, 0x69, 0x6f, 0x2e, 0x76, - 0x31, 0x2e, 0x50, 0x6f, 0x72, 0x74, 0x66, 0x6f, 0x6c, 0x69, 0x6f, 0x45, 0x76, 0x65, 0x6e, 0x74, - 0x42, 0x03, 0xe0, 0x41, 0x02, 0x52, 0x0c, 0x74, 0x72, 0x61, 0x6e, 0x73, 0x61, 0x63, 0x74, 0x69, - 0x6f, 0x6e, 0x73, 0x22, 0xad, 0x01, 0x0a, 0x21, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x50, 0x6f, - 0x72, 0x74, 0x66, 0x6f, 0x6c, 0x69, 0x6f, 0x54, 0x72, 0x61, 0x6e, 0x73, 0x61, 0x63, 0x74, 0x69, - 0x6f, 0x6e, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x47, 0x0a, 0x0b, 0x74, 0x72, 0x61, - 0x6e, 0x73, 0x61, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x20, - 0x2e, 0x6d, 0x67, 0x6f, 0x2e, 0x70, 0x6f, 0x72, 0x74, 0x66, 0x6f, 0x6c, 0x69, 0x6f, 0x2e, 0x76, - 0x31, 0x2e, 0x50, 0x6f, 0x72, 0x74, 0x66, 0x6f, 0x6c, 0x69, 0x6f, 0x45, 0x76, 0x65, 0x6e, 0x74, - 0x42, 0x03, 0xe0, 0x41, 0x02, 0x52, 0x0b, 0x74, 0x72, 0x61, 0x6e, 0x73, 0x61, 0x63, 0x74, 0x69, - 0x6f, 0x6e, 0x12, 0x3f, 0x0a, 0x0a, 0x75, 0x70, 0x64, 0x61, 0x74, 0x65, 0x4d, 0x61, 0x73, 0x6b, - 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1a, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, - 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x46, 0x69, 0x65, 0x6c, 0x64, 0x4d, 0x61, - 0x73, 0x6b, 0x42, 0x03, 0xe0, 0x41, 0x02, 0x52, 0x0a, 0x75, 0x70, 0x64, 0x61, 0x74, 0x65, 0x4d, - 0x61, 0x73, 0x6b, 0x22, 0x4f, 0x0a, 0x21, 0x44, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x50, 0x6f, 0x72, - 0x74, 0x66, 0x6f, 0x6c, 0x69, 0x6f, 0x54, 0x72, 0x61, 0x6e, 0x73, 0x61, 0x63, 0x74, 0x69, 0x6f, - 0x6e, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x2a, 0x0a, 0x0e, 0x74, 0x72, 0x61, 0x6e, - 0x73, 0x61, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x5f, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x05, - 0x42, 0x03, 0xe0, 0x41, 0x02, 0x52, 0x0d, 0x74, 0x72, 0x61, 0x6e, 0x73, 0x61, 0x63, 0x74, 0x69, - 0x6f, 0x6e, 0x49, 0x64, 0x22, 0x63, 0x0a, 0x19, 0x49, 0x6d, 0x70, 0x6f, 0x72, 0x74, 0x54, 0x72, - 0x61, 0x6e, 0x73, 0x61, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, - 0x74, 0x12, 0x26, 0x0a, 0x0c, 0x70, 0x6f, 0x72, 0x74, 0x66, 0x6f, 0x6c, 0x69, 0x6f, 0x5f, 0x69, - 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x42, 0x03, 0xe0, 0x41, 0x02, 0x52, 0x0b, 0x70, 0x6f, - 0x72, 0x74, 0x66, 0x6f, 0x6c, 0x69, 0x6f, 0x49, 0x64, 0x12, 0x1e, 0x0a, 0x08, 0x66, 0x72, 0x6f, - 0x6d, 0x5f, 0x63, 0x73, 0x76, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x42, 0x03, 0xe0, 0x41, 0x02, - 0x52, 0x07, 0x66, 0x72, 0x6f, 0x6d, 0x43, 0x73, 0x76, 0x22, 0x61, 0x0a, 0x18, 0x43, 0x72, 0x65, - 0x61, 0x74, 0x65, 0x42, 0x61, 0x6e, 0x6b, 0x41, 0x63, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x52, 0x65, - 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x45, 0x0a, 0x0c, 0x62, 0x61, 0x6e, 0x6b, 0x5f, 0x61, 0x63, - 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1d, 0x2e, 0x6d, 0x67, - 0x6f, 0x2e, 0x70, 0x6f, 0x72, 0x74, 0x66, 0x6f, 0x6c, 0x69, 0x6f, 0x2e, 0x76, 0x31, 0x2e, 0x42, - 0x61, 0x6e, 0x6b, 0x41, 0x63, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x42, 0x03, 0xe0, 0x41, 0x02, 0x52, - 0x0b, 0x62, 0x61, 0x6e, 0x6b, 0x41, 0x63, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x22, 0x99, 0x01, 0x0a, - 0x18, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x42, 0x61, 0x6e, 0x6b, 0x41, 0x63, 0x63, 0x6f, 0x75, - 0x6e, 0x74, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x3c, 0x0a, 0x07, 0x61, 0x63, 0x63, - 0x6f, 0x75, 0x6e, 0x74, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1d, 0x2e, 0x6d, 0x67, 0x6f, - 0x2e, 0x70, 0x6f, 0x72, 0x74, 0x66, 0x6f, 0x6c, 0x69, 0x6f, 0x2e, 0x76, 0x31, 0x2e, 0x42, 0x61, - 0x6e, 0x6b, 0x41, 0x63, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x42, 0x03, 0xe0, 0x41, 0x02, 0x52, 0x07, - 0x61, 0x63, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x12, 0x3f, 0x0a, 0x0a, 0x75, 0x70, 0x64, 0x61, 0x74, - 0x65, 0x4d, 0x61, 0x73, 0x6b, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1a, 0x2e, 0x67, 0x6f, - 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x46, 0x69, - 0x65, 0x6c, 0x64, 0x4d, 0x61, 0x73, 0x6b, 0x42, 0x03, 0xe0, 0x41, 0x02, 0x52, 0x0a, 0x75, 0x70, - 0x64, 0x61, 0x74, 0x65, 0x4d, 0x61, 0x73, 0x6b, 0x22, 0x2f, 0x0a, 0x18, 0x44, 0x65, 0x6c, 0x65, - 0x74, 0x65, 0x42, 0x61, 0x6e, 0x6b, 0x41, 0x63, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x52, 0x65, 0x71, - 0x75, 0x65, 0x73, 0x74, 0x12, 0x13, 0x0a, 0x02, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, - 0x42, 0x03, 0xe0, 0x41, 0x02, 0x52, 0x02, 0x69, 0x64, 0x22, 0xaf, 0x01, 0x0a, 0x09, 0x50, 0x6f, - 0x72, 0x74, 0x66, 0x6f, 0x6c, 0x69, 0x6f, 0x12, 0x13, 0x0a, 0x02, 0x69, 0x64, 0x18, 0x01, 0x20, - 0x01, 0x28, 0x09, 0x42, 0x03, 0xe0, 0x41, 0x02, 0x52, 0x02, 0x69, 0x64, 0x12, 0x26, 0x0a, 0x0c, - 0x64, 0x69, 0x73, 0x70, 0x6c, 0x61, 0x79, 0x5f, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x02, 0x20, 0x01, - 0x28, 0x09, 0x42, 0x03, 0xe0, 0x41, 0x02, 0x52, 0x0b, 0x64, 0x69, 0x73, 0x70, 0x6c, 0x61, 0x79, - 0x4e, 0x61, 0x6d, 0x65, 0x12, 0x2b, 0x0a, 0x0f, 0x62, 0x61, 0x6e, 0x6b, 0x5f, 0x61, 0x63, 0x63, - 0x6f, 0x75, 0x6e, 0x74, 0x5f, 0x69, 0x64, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x42, 0x03, 0xe0, - 0x41, 0x02, 0x52, 0x0d, 0x62, 0x61, 0x6e, 0x6b, 0x41, 0x63, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x49, - 0x64, 0x12, 0x38, 0x0a, 0x06, 0x65, 0x76, 0x65, 0x6e, 0x74, 0x73, 0x18, 0x05, 0x20, 0x03, 0x28, - 0x0b, 0x32, 0x20, 0x2e, 0x6d, 0x67, 0x6f, 0x2e, 0x70, 0x6f, 0x72, 0x74, 0x66, 0x6f, 0x6c, 0x69, - 0x6f, 0x2e, 0x76, 0x31, 0x2e, 0x50, 0x6f, 0x72, 0x74, 0x66, 0x6f, 0x6c, 0x69, 0x6f, 0x45, 0x76, - 0x65, 0x6e, 0x74, 0x52, 0x06, 0x65, 0x76, 0x65, 0x6e, 0x74, 0x73, 0x22, 0x4a, 0x0a, 0x0b, 0x42, - 0x61, 0x6e, 0x6b, 0x41, 0x63, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x12, 0x13, 0x0a, 0x02, 0x69, 0x64, - 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x42, 0x03, 0xe0, 0x41, 0x02, 0x52, 0x02, 0x69, 0x64, 0x12, - 0x26, 0x0a, 0x0c, 0x64, 0x69, 0x73, 0x70, 0x6c, 0x61, 0x79, 0x5f, 0x6e, 0x61, 0x6d, 0x65, 0x18, - 0x02, 0x20, 0x01, 0x28, 0x09, 0x42, 0x03, 0xe0, 0x41, 0x02, 0x52, 0x0b, 0x64, 0x69, 0x73, 0x70, - 0x6c, 0x61, 0x79, 0x4e, 0x61, 0x6d, 0x65, 0x22, 0x9d, 0x06, 0x0a, 0x11, 0x50, 0x6f, 0x72, 0x74, - 0x66, 0x6f, 0x6c, 0x69, 0x6f, 0x53, 0x6e, 0x61, 0x70, 0x73, 0x68, 0x6f, 0x74, 0x12, 0x33, 0x0a, - 0x04, 0x74, 0x69, 0x6d, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1a, 0x2e, 0x67, 0x6f, - 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x54, 0x69, - 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x42, 0x03, 0xe0, 0x41, 0x02, 0x52, 0x04, 0x74, 0x69, - 0x6d, 0x65, 0x12, 0x55, 0x0a, 0x09, 0x70, 0x6f, 0x73, 0x69, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x18, - 0x02, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x32, 0x2e, 0x6d, 0x67, 0x6f, 0x2e, 0x70, 0x6f, 0x72, 0x74, - 0x66, 0x6f, 0x6c, 0x69, 0x6f, 0x2e, 0x76, 0x31, 0x2e, 0x50, 0x6f, 0x72, 0x74, 0x66, 0x6f, 0x6c, - 0x69, 0x6f, 0x53, 0x6e, 0x61, 0x70, 0x73, 0x68, 0x6f, 0x74, 0x2e, 0x50, 0x6f, 0x73, 0x69, 0x74, - 0x69, 0x6f, 0x6e, 0x73, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x42, 0x03, 0xe0, 0x41, 0x02, 0x52, 0x09, - 0x70, 0x6f, 0x73, 0x69, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x12, 0x5a, 0x0a, 0x16, 0x66, 0x69, 0x72, - 0x73, 0x74, 0x5f, 0x74, 0x72, 0x61, 0x6e, 0x73, 0x61, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x5f, 0x74, - 0x69, 0x6d, 0x65, 0x18, 0x03, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1a, 0x2e, 0x67, 0x6f, 0x6f, 0x67, - 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x54, 0x69, 0x6d, 0x65, - 0x73, 0x74, 0x61, 0x6d, 0x70, 0x42, 0x03, 0xe0, 0x41, 0x02, 0x48, 0x00, 0x52, 0x14, 0x66, 0x69, - 0x72, 0x73, 0x74, 0x54, 0x72, 0x61, 0x6e, 0x73, 0x61, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x54, 0x69, - 0x6d, 0x65, 0x88, 0x01, 0x01, 0x12, 0x51, 0x0a, 0x14, 0x74, 0x6f, 0x74, 0x61, 0x6c, 0x5f, 0x70, - 0x75, 0x72, 0x63, 0x68, 0x61, 0x73, 0x65, 0x5f, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x18, 0x0a, 0x20, - 0x01, 0x28, 0x0b, 0x32, 0x1a, 0x2e, 0x6d, 0x67, 0x6f, 0x2e, 0x70, 0x6f, 0x72, 0x74, 0x66, 0x6f, - 0x6c, 0x69, 0x6f, 0x2e, 0x76, 0x31, 0x2e, 0x43, 0x75, 0x72, 0x72, 0x65, 0x6e, 0x63, 0x79, 0x42, - 0x03, 0xe0, 0x41, 0x02, 0x52, 0x12, 0x74, 0x6f, 0x74, 0x61, 0x6c, 0x50, 0x75, 0x72, 0x63, 0x68, - 0x61, 0x73, 0x65, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x12, 0x4d, 0x0a, 0x12, 0x74, 0x6f, 0x74, 0x61, - 0x6c, 0x5f, 0x6d, 0x61, 0x72, 0x6b, 0x65, 0x74, 0x5f, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x18, 0x0b, - 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1a, 0x2e, 0x6d, 0x67, 0x6f, 0x2e, 0x70, 0x6f, 0x72, 0x74, 0x66, - 0x6f, 0x6c, 0x69, 0x6f, 0x2e, 0x76, 0x31, 0x2e, 0x43, 0x75, 0x72, 0x72, 0x65, 0x6e, 0x63, 0x79, - 0x42, 0x03, 0xe0, 0x41, 0x02, 0x52, 0x10, 0x74, 0x6f, 0x74, 0x61, 0x6c, 0x4d, 0x61, 0x72, 0x6b, - 0x65, 0x74, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x12, 0x50, 0x0a, 0x14, 0x74, 0x6f, 0x74, 0x61, 0x6c, - 0x5f, 0x70, 0x72, 0x6f, 0x66, 0x69, 0x74, 0x5f, 0x6f, 0x72, 0x5f, 0x6c, 0x6f, 0x73, 0x73, 0x18, - 0x14, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1a, 0x2e, 0x6d, 0x67, 0x6f, 0x2e, 0x70, 0x6f, 0x72, 0x74, - 0x66, 0x6f, 0x6c, 0x69, 0x6f, 0x2e, 0x76, 0x31, 0x2e, 0x43, 0x75, 0x72, 0x72, 0x65, 0x6e, 0x63, - 0x79, 0x42, 0x03, 0xe0, 0x41, 0x02, 0x52, 0x11, 0x74, 0x6f, 0x74, 0x61, 0x6c, 0x50, 0x72, 0x6f, - 0x66, 0x69, 0x74, 0x4f, 0x72, 0x4c, 0x6f, 0x73, 0x73, 0x12, 0x24, 0x0a, 0x0b, 0x74, 0x6f, 0x74, - 0x61, 0x6c, 0x5f, 0x67, 0x61, 0x69, 0x6e, 0x73, 0x18, 0x15, 0x20, 0x01, 0x28, 0x01, 0x42, 0x03, - 0xe0, 0x41, 0x02, 0x52, 0x0a, 0x74, 0x6f, 0x74, 0x61, 0x6c, 0x47, 0x61, 0x69, 0x6e, 0x73, 0x12, - 0x33, 0x0a, 0x04, 0x63, 0x61, 0x73, 0x68, 0x18, 0x16, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1a, 0x2e, - 0x6d, 0x67, 0x6f, 0x2e, 0x70, 0x6f, 0x72, 0x74, 0x66, 0x6f, 0x6c, 0x69, 0x6f, 0x2e, 0x76, 0x31, - 0x2e, 0x43, 0x75, 0x72, 0x72, 0x65, 0x6e, 0x63, 0x79, 0x42, 0x03, 0xe0, 0x41, 0x02, 0x52, 0x04, - 0x63, 0x61, 0x73, 0x68, 0x12, 0x53, 0x0a, 0x15, 0x74, 0x6f, 0x74, 0x61, 0x6c, 0x5f, 0x70, 0x6f, - 0x72, 0x74, 0x66, 0x6f, 0x6c, 0x69, 0x6f, 0x5f, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x18, 0x17, 0x20, - 0x01, 0x28, 0x0b, 0x32, 0x1a, 0x2e, 0x6d, 0x67, 0x6f, 0x2e, 0x70, 0x6f, 0x72, 0x74, 0x66, 0x6f, - 0x6c, 0x69, 0x6f, 0x2e, 0x76, 0x31, 0x2e, 0x43, 0x75, 0x72, 0x72, 0x65, 0x6e, 0x63, 0x79, 0x42, - 0x03, 0xe0, 0x41, 0x02, 0x52, 0x13, 0x74, 0x6f, 0x74, 0x61, 0x6c, 0x50, 0x6f, 0x72, 0x74, 0x66, - 0x6f, 0x6c, 0x69, 0x6f, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x1a, 0x61, 0x0a, 0x0e, 0x50, 0x6f, 0x73, - 0x69, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x12, 0x10, 0x0a, 0x03, 0x6b, - 0x65, 0x79, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x03, 0x6b, 0x65, 0x79, 0x12, 0x39, 0x0a, - 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x23, 0x2e, 0x6d, - 0x67, 0x6f, 0x2e, 0x70, 0x6f, 0x72, 0x74, 0x66, 0x6f, 0x6c, 0x69, 0x6f, 0x2e, 0x76, 0x31, 0x2e, - 0x50, 0x6f, 0x72, 0x74, 0x66, 0x6f, 0x6c, 0x69, 0x6f, 0x50, 0x6f, 0x73, 0x69, 0x74, 0x69, 0x6f, - 0x6e, 0x52, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x3a, 0x02, 0x38, 0x01, 0x42, 0x19, 0x0a, 0x17, - 0x5f, 0x66, 0x69, 0x72, 0x73, 0x74, 0x5f, 0x74, 0x72, 0x61, 0x6e, 0x73, 0x61, 0x63, 0x74, 0x69, - 0x6f, 0x6e, 0x5f, 0x74, 0x69, 0x6d, 0x65, 0x22, 0xa7, 0x04, 0x0a, 0x11, 0x50, 0x6f, 0x72, 0x74, - 0x66, 0x6f, 0x6c, 0x69, 0x6f, 0x50, 0x6f, 0x73, 0x69, 0x74, 0x69, 0x6f, 0x6e, 0x12, 0x3b, 0x0a, - 0x08, 0x73, 0x65, 0x63, 0x75, 0x72, 0x69, 0x74, 0x79, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, - 0x1a, 0x2e, 0x6d, 0x67, 0x6f, 0x2e, 0x70, 0x6f, 0x72, 0x74, 0x66, 0x6f, 0x6c, 0x69, 0x6f, 0x2e, - 0x76, 0x31, 0x2e, 0x53, 0x65, 0x63, 0x75, 0x72, 0x69, 0x74, 0x79, 0x42, 0x03, 0xe0, 0x41, 0x02, - 0x52, 0x08, 0x73, 0x65, 0x63, 0x75, 0x72, 0x69, 0x74, 0x79, 0x12, 0x1b, 0x0a, 0x06, 0x61, 0x6d, - 0x6f, 0x75, 0x6e, 0x74, 0x18, 0x02, 0x20, 0x01, 0x28, 0x01, 0x42, 0x03, 0xe0, 0x41, 0x02, 0x52, - 0x06, 0x61, 0x6d, 0x6f, 0x75, 0x6e, 0x74, 0x12, 0x46, 0x0a, 0x0e, 0x70, 0x75, 0x72, 0x63, 0x68, - 0x61, 0x73, 0x65, 0x5f, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x18, 0x05, 0x20, 0x01, 0x28, 0x0b, 0x32, - 0x1a, 0x2e, 0x6d, 0x67, 0x6f, 0x2e, 0x70, 0x6f, 0x72, 0x74, 0x66, 0x6f, 0x6c, 0x69, 0x6f, 0x2e, - 0x76, 0x31, 0x2e, 0x43, 0x75, 0x72, 0x72, 0x65, 0x6e, 0x63, 0x79, 0x42, 0x03, 0xe0, 0x41, 0x02, - 0x52, 0x0d, 0x70, 0x75, 0x72, 0x63, 0x68, 0x61, 0x73, 0x65, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x12, - 0x46, 0x0a, 0x0e, 0x70, 0x75, 0x72, 0x63, 0x68, 0x61, 0x73, 0x65, 0x5f, 0x70, 0x72, 0x69, 0x63, - 0x65, 0x18, 0x06, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1a, 0x2e, 0x6d, 0x67, 0x6f, 0x2e, 0x70, 0x6f, - 0x72, 0x74, 0x66, 0x6f, 0x6c, 0x69, 0x6f, 0x2e, 0x76, 0x31, 0x2e, 0x43, 0x75, 0x72, 0x72, 0x65, - 0x6e, 0x63, 0x79, 0x42, 0x03, 0xe0, 0x41, 0x02, 0x52, 0x0d, 0x70, 0x75, 0x72, 0x63, 0x68, 0x61, - 0x73, 0x65, 0x50, 0x72, 0x69, 0x63, 0x65, 0x12, 0x42, 0x0a, 0x0c, 0x6d, 0x61, 0x72, 0x6b, 0x65, - 0x74, 0x5f, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x18, 0x0a, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1a, 0x2e, - 0x6d, 0x67, 0x6f, 0x2e, 0x70, 0x6f, 0x72, 0x74, 0x66, 0x6f, 0x6c, 0x69, 0x6f, 0x2e, 0x76, 0x31, - 0x2e, 0x43, 0x75, 0x72, 0x72, 0x65, 0x6e, 0x63, 0x79, 0x42, 0x03, 0xe0, 0x41, 0x02, 0x52, 0x0b, - 0x6d, 0x61, 0x72, 0x6b, 0x65, 0x74, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x12, 0x42, 0x0a, 0x0c, 0x6d, - 0x61, 0x72, 0x6b, 0x65, 0x74, 0x5f, 0x70, 0x72, 0x69, 0x63, 0x65, 0x18, 0x0b, 0x20, 0x01, 0x28, - 0x0b, 0x32, 0x1a, 0x2e, 0x6d, 0x67, 0x6f, 0x2e, 0x70, 0x6f, 0x72, 0x74, 0x66, 0x6f, 0x6c, 0x69, - 0x6f, 0x2e, 0x76, 0x31, 0x2e, 0x43, 0x75, 0x72, 0x72, 0x65, 0x6e, 0x63, 0x79, 0x42, 0x03, 0xe0, - 0x41, 0x02, 0x52, 0x0b, 0x6d, 0x61, 0x72, 0x6b, 0x65, 0x74, 0x50, 0x72, 0x69, 0x63, 0x65, 0x12, - 0x3e, 0x0a, 0x0a, 0x74, 0x6f, 0x74, 0x61, 0x6c, 0x5f, 0x66, 0x65, 0x65, 0x73, 0x18, 0x0f, 0x20, - 0x01, 0x28, 0x0b, 0x32, 0x1a, 0x2e, 0x6d, 0x67, 0x6f, 0x2e, 0x70, 0x6f, 0x72, 0x74, 0x66, 0x6f, - 0x6c, 0x69, 0x6f, 0x2e, 0x76, 0x31, 0x2e, 0x43, 0x75, 0x72, 0x72, 0x65, 0x6e, 0x63, 0x79, 0x42, - 0x03, 0xe0, 0x41, 0x02, 0x52, 0x09, 0x74, 0x6f, 0x74, 0x61, 0x6c, 0x46, 0x65, 0x65, 0x73, 0x12, - 0x45, 0x0a, 0x0e, 0x70, 0x72, 0x6f, 0x66, 0x69, 0x74, 0x5f, 0x6f, 0x72, 0x5f, 0x6c, 0x6f, 0x73, - 0x73, 0x18, 0x14, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1a, 0x2e, 0x6d, 0x67, 0x6f, 0x2e, 0x70, 0x6f, - 0x72, 0x74, 0x66, 0x6f, 0x6c, 0x69, 0x6f, 0x2e, 0x76, 0x31, 0x2e, 0x43, 0x75, 0x72, 0x72, 0x65, - 0x6e, 0x63, 0x79, 0x42, 0x03, 0xe0, 0x41, 0x02, 0x52, 0x0c, 0x70, 0x72, 0x6f, 0x66, 0x69, 0x74, - 0x4f, 0x72, 0x4c, 0x6f, 0x73, 0x73, 0x12, 0x19, 0x0a, 0x05, 0x67, 0x61, 0x69, 0x6e, 0x73, 0x18, - 0x15, 0x20, 0x01, 0x28, 0x01, 0x42, 0x03, 0xe0, 0x41, 0x02, 0x52, 0x05, 0x67, 0x61, 0x69, 0x6e, - 0x73, 0x22, 0xa7, 0x03, 0x0a, 0x0e, 0x50, 0x6f, 0x72, 0x74, 0x66, 0x6f, 0x6c, 0x69, 0x6f, 0x45, - 0x76, 0x65, 0x6e, 0x74, 0x12, 0x13, 0x0a, 0x02, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, - 0x42, 0x03, 0xe0, 0x41, 0x02, 0x52, 0x02, 0x69, 0x64, 0x12, 0x3d, 0x0a, 0x04, 0x74, 0x79, 0x70, - 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0e, 0x32, 0x24, 0x2e, 0x6d, 0x67, 0x6f, 0x2e, 0x70, 0x6f, - 0x72, 0x74, 0x66, 0x6f, 0x6c, 0x69, 0x6f, 0x2e, 0x76, 0x31, 0x2e, 0x50, 0x6f, 0x72, 0x74, 0x66, - 0x6f, 0x6c, 0x69, 0x6f, 0x45, 0x76, 0x65, 0x6e, 0x74, 0x54, 0x79, 0x70, 0x65, 0x42, 0x03, 0xe0, - 0x41, 0x02, 0x52, 0x04, 0x74, 0x79, 0x70, 0x65, 0x12, 0x33, 0x0a, 0x04, 0x74, 0x69, 0x6d, 0x65, - 0x18, 0x03, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1a, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, - 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x54, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, - 0x6d, 0x70, 0x42, 0x03, 0xe0, 0x41, 0x02, 0x52, 0x04, 0x74, 0x69, 0x6d, 0x65, 0x12, 0x26, 0x0a, - 0x0c, 0x70, 0x6f, 0x72, 0x74, 0x66, 0x6f, 0x6c, 0x69, 0x6f, 0x5f, 0x69, 0x64, 0x18, 0x04, 0x20, - 0x01, 0x28, 0x09, 0x42, 0x03, 0xe0, 0x41, 0x02, 0x52, 0x0b, 0x70, 0x6f, 0x72, 0x74, 0x66, 0x6f, - 0x6c, 0x69, 0x6f, 0x49, 0x64, 0x12, 0x24, 0x0a, 0x0b, 0x73, 0x65, 0x63, 0x75, 0x72, 0x69, 0x74, - 0x79, 0x5f, 0x69, 0x64, 0x18, 0x05, 0x20, 0x01, 0x28, 0x09, 0x42, 0x03, 0xe0, 0x41, 0x02, 0x52, - 0x0a, 0x73, 0x65, 0x63, 0x75, 0x72, 0x69, 0x74, 0x79, 0x49, 0x64, 0x12, 0x1b, 0x0a, 0x06, 0x61, - 0x6d, 0x6f, 0x75, 0x6e, 0x74, 0x18, 0x0a, 0x20, 0x01, 0x28, 0x01, 0x42, 0x03, 0xe0, 0x41, 0x02, - 0x52, 0x06, 0x61, 0x6d, 0x6f, 0x75, 0x6e, 0x74, 0x12, 0x35, 0x0a, 0x05, 0x70, 0x72, 0x69, 0x63, - 0x65, 0x18, 0x0b, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1a, 0x2e, 0x6d, 0x67, 0x6f, 0x2e, 0x70, 0x6f, - 0x72, 0x74, 0x66, 0x6f, 0x6c, 0x69, 0x6f, 0x2e, 0x76, 0x31, 0x2e, 0x43, 0x75, 0x72, 0x72, 0x65, - 0x6e, 0x63, 0x79, 0x42, 0x03, 0xe0, 0x41, 0x02, 0x52, 0x05, 0x70, 0x72, 0x69, 0x63, 0x65, 0x12, - 0x33, 0x0a, 0x04, 0x66, 0x65, 0x65, 0x73, 0x18, 0x0c, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1a, 0x2e, - 0x6d, 0x67, 0x6f, 0x2e, 0x70, 0x6f, 0x72, 0x74, 0x66, 0x6f, 0x6c, 0x69, 0x6f, 0x2e, 0x76, 0x31, - 0x2e, 0x43, 0x75, 0x72, 0x72, 0x65, 0x6e, 0x63, 0x79, 0x42, 0x03, 0xe0, 0x41, 0x02, 0x52, 0x04, - 0x66, 0x65, 0x65, 0x73, 0x12, 0x35, 0x0a, 0x05, 0x74, 0x61, 0x78, 0x65, 0x73, 0x18, 0x0d, 0x20, - 0x01, 0x28, 0x0b, 0x32, 0x1a, 0x2e, 0x6d, 0x67, 0x6f, 0x2e, 0x70, 0x6f, 0x72, 0x74, 0x66, 0x6f, - 0x6c, 0x69, 0x6f, 0x2e, 0x76, 0x31, 0x2e, 0x43, 0x75, 0x72, 0x72, 0x65, 0x6e, 0x63, 0x79, 0x42, - 0x03, 0xe0, 0x41, 0x02, 0x52, 0x05, 0x74, 0x61, 0x78, 0x65, 0x73, 0x22, 0xcf, 0x01, 0x0a, 0x08, - 0x53, 0x65, 0x63, 0x75, 0x72, 0x69, 0x74, 0x79, 0x12, 0x13, 0x0a, 0x02, 0x69, 0x64, 0x18, 0x01, - 0x20, 0x01, 0x28, 0x09, 0x42, 0x03, 0xe0, 0x41, 0x02, 0x52, 0x02, 0x69, 0x64, 0x12, 0x26, 0x0a, - 0x0c, 0x64, 0x69, 0x73, 0x70, 0x6c, 0x61, 0x79, 0x5f, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x02, 0x20, - 0x01, 0x28, 0x09, 0x42, 0x03, 0xe0, 0x41, 0x02, 0x52, 0x0b, 0x64, 0x69, 0x73, 0x70, 0x6c, 0x61, - 0x79, 0x4e, 0x61, 0x6d, 0x65, 0x12, 0x42, 0x0a, 0x09, 0x6c, 0x69, 0x73, 0x74, 0x65, 0x64, 0x5f, - 0x6f, 0x6e, 0x18, 0x04, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x20, 0x2e, 0x6d, 0x67, 0x6f, 0x2e, 0x70, - 0x6f, 0x72, 0x74, 0x66, 0x6f, 0x6c, 0x69, 0x6f, 0x2e, 0x76, 0x31, 0x2e, 0x4c, 0x69, 0x73, 0x74, - 0x65, 0x64, 0x53, 0x65, 0x63, 0x75, 0x72, 0x69, 0x74, 0x79, 0x42, 0x03, 0xe0, 0x41, 0x02, 0x52, - 0x08, 0x6c, 0x69, 0x73, 0x74, 0x65, 0x64, 0x4f, 0x6e, 0x12, 0x2f, 0x0a, 0x0e, 0x71, 0x75, 0x6f, - 0x74, 0x65, 0x5f, 0x70, 0x72, 0x6f, 0x76, 0x69, 0x64, 0x65, 0x72, 0x18, 0x0a, 0x20, 0x01, 0x28, - 0x09, 0x42, 0x03, 0xe0, 0x41, 0x02, 0x48, 0x00, 0x52, 0x0d, 0x71, 0x75, 0x6f, 0x74, 0x65, 0x50, - 0x72, 0x6f, 0x76, 0x69, 0x64, 0x65, 0x72, 0x88, 0x01, 0x01, 0x42, 0x11, 0x0a, 0x0f, 0x5f, 0x71, - 0x75, 0x6f, 0x74, 0x65, 0x5f, 0x70, 0x72, 0x6f, 0x76, 0x69, 0x64, 0x65, 0x72, 0x22, 0xbb, 0x02, - 0x0a, 0x0e, 0x4c, 0x69, 0x73, 0x74, 0x65, 0x64, 0x53, 0x65, 0x63, 0x75, 0x72, 0x69, 0x74, 0x79, - 0x12, 0x24, 0x0a, 0x0b, 0x73, 0x65, 0x63, 0x75, 0x72, 0x69, 0x74, 0x79, 0x5f, 0x69, 0x64, 0x18, - 0x01, 0x20, 0x01, 0x28, 0x09, 0x42, 0x03, 0xe0, 0x41, 0x02, 0x52, 0x0a, 0x73, 0x65, 0x63, 0x75, - 0x72, 0x69, 0x74, 0x79, 0x49, 0x64, 0x12, 0x1b, 0x0a, 0x06, 0x74, 0x69, 0x63, 0x6b, 0x65, 0x72, - 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x42, 0x03, 0xe0, 0x41, 0x02, 0x52, 0x06, 0x74, 0x69, 0x63, - 0x6b, 0x65, 0x72, 0x12, 0x1f, 0x0a, 0x08, 0x63, 0x75, 0x72, 0x72, 0x65, 0x6e, 0x63, 0x79, 0x18, - 0x04, 0x20, 0x01, 0x28, 0x09, 0x42, 0x03, 0xe0, 0x41, 0x02, 0x52, 0x08, 0x63, 0x75, 0x72, 0x72, - 0x65, 0x6e, 0x63, 0x79, 0x12, 0x42, 0x0a, 0x0c, 0x6c, 0x61, 0x74, 0x65, 0x73, 0x74, 0x5f, 0x71, - 0x75, 0x6f, 0x74, 0x65, 0x18, 0x05, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1a, 0x2e, 0x6d, 0x67, 0x6f, - 0x2e, 0x70, 0x6f, 0x72, 0x74, 0x66, 0x6f, 0x6c, 0x69, 0x6f, 0x2e, 0x76, 0x31, 0x2e, 0x43, 0x75, - 0x72, 0x72, 0x65, 0x6e, 0x63, 0x79, 0x48, 0x00, 0x52, 0x0b, 0x6c, 0x61, 0x74, 0x65, 0x73, 0x74, - 0x51, 0x75, 0x6f, 0x74, 0x65, 0x88, 0x01, 0x01, 0x12, 0x55, 0x0a, 0x16, 0x6c, 0x61, 0x74, 0x65, - 0x73, 0x74, 0x5f, 0x71, 0x75, 0x6f, 0x74, 0x65, 0x5f, 0x74, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, - 0x6d, 0x70, 0x18, 0x06, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1a, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, - 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x54, 0x69, 0x6d, 0x65, 0x73, - 0x74, 0x61, 0x6d, 0x70, 0x48, 0x01, 0x52, 0x14, 0x6c, 0x61, 0x74, 0x65, 0x73, 0x74, 0x51, 0x75, - 0x6f, 0x74, 0x65, 0x54, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x88, 0x01, 0x01, 0x42, - 0x0f, 0x0a, 0x0d, 0x5f, 0x6c, 0x61, 0x74, 0x65, 0x73, 0x74, 0x5f, 0x71, 0x75, 0x6f, 0x74, 0x65, - 0x42, 0x19, 0x0a, 0x17, 0x5f, 0x6c, 0x61, 0x74, 0x65, 0x73, 0x74, 0x5f, 0x71, 0x75, 0x6f, 0x74, - 0x65, 0x5f, 0x74, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x22, 0x9c, 0x01, 0x0a, 0x15, - 0x4c, 0x69, 0x73, 0x74, 0x53, 0x65, 0x63, 0x75, 0x72, 0x69, 0x74, 0x69, 0x65, 0x73, 0x52, 0x65, - 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x4b, 0x0a, 0x06, 0x66, 0x69, 0x6c, 0x74, 0x65, 0x72, 0x18, - 0x05, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x2e, 0x2e, 0x6d, 0x67, 0x6f, 0x2e, 0x70, 0x6f, 0x72, 0x74, - 0x66, 0x6f, 0x6c, 0x69, 0x6f, 0x2e, 0x76, 0x31, 0x2e, 0x4c, 0x69, 0x73, 0x74, 0x53, 0x65, 0x63, - 0x75, 0x72, 0x69, 0x74, 0x69, 0x65, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x2e, 0x46, - 0x69, 0x6c, 0x74, 0x65, 0x72, 0x48, 0x00, 0x52, 0x06, 0x66, 0x69, 0x6c, 0x74, 0x65, 0x72, 0x88, - 0x01, 0x01, 0x1a, 0x2b, 0x0a, 0x06, 0x46, 0x69, 0x6c, 0x74, 0x65, 0x72, 0x12, 0x21, 0x0a, 0x0c, - 0x73, 0x65, 0x63, 0x75, 0x72, 0x69, 0x74, 0x79, 0x5f, 0x69, 0x64, 0x73, 0x18, 0x01, 0x20, 0x03, - 0x28, 0x09, 0x52, 0x0b, 0x73, 0x65, 0x63, 0x75, 0x72, 0x69, 0x74, 0x79, 0x49, 0x64, 0x73, 0x42, - 0x09, 0x0a, 0x07, 0x5f, 0x66, 0x69, 0x6c, 0x74, 0x65, 0x72, 0x22, 0x59, 0x0a, 0x16, 0x4c, 0x69, - 0x73, 0x74, 0x53, 0x65, 0x63, 0x75, 0x72, 0x69, 0x74, 0x69, 0x65, 0x73, 0x52, 0x65, 0x73, 0x70, - 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x3f, 0x0a, 0x0a, 0x73, 0x65, 0x63, 0x75, 0x72, 0x69, 0x74, 0x69, - 0x65, 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x1a, 0x2e, 0x6d, 0x67, 0x6f, 0x2e, 0x70, - 0x6f, 0x72, 0x74, 0x66, 0x6f, 0x6c, 0x69, 0x6f, 0x2e, 0x76, 0x31, 0x2e, 0x53, 0x65, 0x63, 0x75, - 0x72, 0x69, 0x74, 0x79, 0x42, 0x03, 0xe0, 0x41, 0x02, 0x52, 0x0a, 0x73, 0x65, 0x63, 0x75, 0x72, - 0x69, 0x74, 0x69, 0x65, 0x73, 0x22, 0x29, 0x0a, 0x12, 0x47, 0x65, 0x74, 0x53, 0x65, 0x63, 0x75, - 0x72, 0x69, 0x74, 0x79, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x13, 0x0a, 0x02, 0x69, - 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x42, 0x03, 0xe0, 0x41, 0x02, 0x52, 0x02, 0x69, 0x64, - 0x22, 0x54, 0x0a, 0x15, 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, 0x53, 0x65, 0x63, 0x75, 0x72, 0x69, - 0x74, 0x79, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x3b, 0x0a, 0x08, 0x73, 0x65, 0x63, - 0x75, 0x72, 0x69, 0x74, 0x79, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1a, 0x2e, 0x6d, 0x67, - 0x6f, 0x2e, 0x70, 0x6f, 0x72, 0x74, 0x66, 0x6f, 0x6c, 0x69, 0x6f, 0x2e, 0x76, 0x31, 0x2e, 0x53, - 0x65, 0x63, 0x75, 0x72, 0x69, 0x74, 0x79, 0x42, 0x03, 0xe0, 0x41, 0x02, 0x52, 0x08, 0x73, 0x65, - 0x63, 0x75, 0x72, 0x69, 0x74, 0x79, 0x22, 0x95, 0x01, 0x0a, 0x15, 0x55, 0x70, 0x64, 0x61, 0x74, - 0x65, 0x53, 0x65, 0x63, 0x75, 0x72, 0x69, 0x74, 0x79, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, - 0x12, 0x3b, 0x0a, 0x08, 0x73, 0x65, 0x63, 0x75, 0x72, 0x69, 0x74, 0x79, 0x18, 0x01, 0x20, 0x01, - 0x28, 0x0b, 0x32, 0x1a, 0x2e, 0x6d, 0x67, 0x6f, 0x2e, 0x70, 0x6f, 0x72, 0x74, 0x66, 0x6f, 0x6c, - 0x69, 0x6f, 0x2e, 0x76, 0x31, 0x2e, 0x53, 0x65, 0x63, 0x75, 0x72, 0x69, 0x74, 0x79, 0x42, 0x03, - 0xe0, 0x41, 0x02, 0x52, 0x08, 0x73, 0x65, 0x63, 0x75, 0x72, 0x69, 0x74, 0x79, 0x12, 0x3f, 0x0a, - 0x0a, 0x75, 0x70, 0x64, 0x61, 0x74, 0x65, 0x4d, 0x61, 0x73, 0x6b, 0x18, 0x02, 0x20, 0x01, 0x28, - 0x0b, 0x32, 0x1a, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, - 0x62, 0x75, 0x66, 0x2e, 0x46, 0x69, 0x65, 0x6c, 0x64, 0x4d, 0x61, 0x73, 0x6b, 0x42, 0x03, 0xe0, - 0x41, 0x02, 0x52, 0x0a, 0x75, 0x70, 0x64, 0x61, 0x74, 0x65, 0x4d, 0x61, 0x73, 0x6b, 0x22, 0x2c, - 0x0a, 0x15, 0x44, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x53, 0x65, 0x63, 0x75, 0x72, 0x69, 0x74, 0x79, - 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x13, 0x0a, 0x02, 0x69, 0x64, 0x18, 0x01, 0x20, - 0x01, 0x28, 0x09, 0x42, 0x03, 0xe0, 0x41, 0x02, 0x52, 0x02, 0x69, 0x64, 0x22, 0x3e, 0x0a, 0x19, - 0x54, 0x72, 0x69, 0x67, 0x67, 0x65, 0x72, 0x51, 0x75, 0x6f, 0x74, 0x65, 0x55, 0x70, 0x64, 0x61, - 0x74, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x21, 0x0a, 0x0c, 0x73, 0x65, 0x63, - 0x75, 0x72, 0x69, 0x74, 0x79, 0x5f, 0x69, 0x64, 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, 0x09, 0x52, - 0x0b, 0x73, 0x65, 0x63, 0x75, 0x72, 0x69, 0x74, 0x79, 0x49, 0x64, 0x73, 0x22, 0x1c, 0x0a, 0x1a, - 0x54, 0x72, 0x69, 0x67, 0x67, 0x65, 0x72, 0x51, 0x75, 0x6f, 0x74, 0x65, 0x55, 0x70, 0x64, 0x61, - 0x74, 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x2a, 0xaf, 0x03, 0x0a, 0x12, 0x50, - 0x6f, 0x72, 0x74, 0x66, 0x6f, 0x6c, 0x69, 0x6f, 0x45, 0x76, 0x65, 0x6e, 0x74, 0x54, 0x79, 0x70, - 0x65, 0x12, 0x24, 0x0a, 0x20, 0x50, 0x4f, 0x52, 0x54, 0x46, 0x4f, 0x4c, 0x49, 0x4f, 0x5f, 0x45, - 0x56, 0x45, 0x4e, 0x54, 0x5f, 0x54, 0x59, 0x50, 0x45, 0x5f, 0x55, 0x4e, 0x53, 0x50, 0x45, 0x43, - 0x49, 0x46, 0x49, 0x45, 0x44, 0x10, 0x00, 0x12, 0x1c, 0x0a, 0x18, 0x50, 0x4f, 0x52, 0x54, 0x46, - 0x4f, 0x4c, 0x49, 0x4f, 0x5f, 0x45, 0x56, 0x45, 0x4e, 0x54, 0x5f, 0x54, 0x59, 0x50, 0x45, 0x5f, - 0x42, 0x55, 0x59, 0x10, 0x01, 0x12, 0x1d, 0x0a, 0x19, 0x50, 0x4f, 0x52, 0x54, 0x46, 0x4f, 0x4c, - 0x49, 0x4f, 0x5f, 0x45, 0x56, 0x45, 0x4e, 0x54, 0x5f, 0x54, 0x59, 0x50, 0x45, 0x5f, 0x53, 0x45, - 0x4c, 0x4c, 0x10, 0x02, 0x12, 0x29, 0x0a, 0x25, 0x50, 0x4f, 0x52, 0x54, 0x46, 0x4f, 0x4c, 0x49, - 0x4f, 0x5f, 0x45, 0x56, 0x45, 0x4e, 0x54, 0x5f, 0x54, 0x59, 0x50, 0x45, 0x5f, 0x44, 0x45, 0x4c, - 0x49, 0x56, 0x45, 0x52, 0x59, 0x5f, 0x49, 0x4e, 0x42, 0x4f, 0x55, 0x4e, 0x44, 0x10, 0x03, 0x12, - 0x2a, 0x0a, 0x26, 0x50, 0x4f, 0x52, 0x54, 0x46, 0x4f, 0x4c, 0x49, 0x4f, 0x5f, 0x45, 0x56, 0x45, - 0x4e, 0x54, 0x5f, 0x54, 0x59, 0x50, 0x45, 0x5f, 0x44, 0x45, 0x4c, 0x49, 0x56, 0x45, 0x52, 0x59, - 0x5f, 0x4f, 0x55, 0x54, 0x42, 0x4f, 0x55, 0x4e, 0x44, 0x10, 0x04, 0x12, 0x21, 0x0a, 0x1d, 0x50, - 0x4f, 0x52, 0x54, 0x46, 0x4f, 0x4c, 0x49, 0x4f, 0x5f, 0x45, 0x56, 0x45, 0x4e, 0x54, 0x5f, 0x54, - 0x59, 0x50, 0x45, 0x5f, 0x44, 0x49, 0x56, 0x49, 0x44, 0x45, 0x4e, 0x44, 0x10, 0x0a, 0x12, 0x21, - 0x0a, 0x1d, 0x50, 0x4f, 0x52, 0x54, 0x46, 0x4f, 0x4c, 0x49, 0x4f, 0x5f, 0x45, 0x56, 0x45, 0x4e, - 0x54, 0x5f, 0x54, 0x59, 0x50, 0x45, 0x5f, 0x49, 0x4e, 0x54, 0x45, 0x52, 0x45, 0x53, 0x54, 0x10, - 0x0b, 0x12, 0x25, 0x0a, 0x21, 0x50, 0x4f, 0x52, 0x54, 0x46, 0x4f, 0x4c, 0x49, 0x4f, 0x5f, 0x45, - 0x56, 0x45, 0x4e, 0x54, 0x5f, 0x54, 0x59, 0x50, 0x45, 0x5f, 0x44, 0x45, 0x50, 0x4f, 0x53, 0x49, - 0x54, 0x5f, 0x43, 0x41, 0x53, 0x48, 0x10, 0x14, 0x12, 0x26, 0x0a, 0x22, 0x50, 0x4f, 0x52, 0x54, - 0x46, 0x4f, 0x4c, 0x49, 0x4f, 0x5f, 0x45, 0x56, 0x45, 0x4e, 0x54, 0x5f, 0x54, 0x59, 0x50, 0x45, - 0x5f, 0x57, 0x49, 0x54, 0x48, 0x44, 0x52, 0x41, 0x57, 0x5f, 0x43, 0x41, 0x53, 0x48, 0x10, 0x15, - 0x12, 0x25, 0x0a, 0x21, 0x50, 0x4f, 0x52, 0x54, 0x46, 0x4f, 0x4c, 0x49, 0x4f, 0x5f, 0x45, 0x56, - 0x45, 0x4e, 0x54, 0x5f, 0x54, 0x59, 0x50, 0x45, 0x5f, 0x41, 0x43, 0x43, 0x4f, 0x55, 0x4e, 0x54, - 0x5f, 0x46, 0x45, 0x45, 0x53, 0x10, 0x1e, 0x12, 0x23, 0x0a, 0x1f, 0x50, 0x4f, 0x52, 0x54, 0x46, - 0x4f, 0x4c, 0x49, 0x4f, 0x5f, 0x45, 0x56, 0x45, 0x4e, 0x54, 0x5f, 0x54, 0x59, 0x50, 0x45, 0x5f, - 0x54, 0x41, 0x58, 0x5f, 0x52, 0x45, 0x46, 0x55, 0x4e, 0x44, 0x10, 0x1f, 0x32, 0xf2, 0x0e, 0x0a, - 0x10, 0x50, 0x6f, 0x72, 0x74, 0x66, 0x6f, 0x6c, 0x69, 0x6f, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, - 0x65, 0x12, 0x7b, 0x0a, 0x0f, 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, 0x50, 0x6f, 0x72, 0x74, 0x66, - 0x6f, 0x6c, 0x69, 0x6f, 0x12, 0x28, 0x2e, 0x6d, 0x67, 0x6f, 0x2e, 0x70, 0x6f, 0x72, 0x74, 0x66, - 0x6f, 0x6c, 0x69, 0x6f, 0x2e, 0x76, 0x31, 0x2e, 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, 0x50, 0x6f, - 0x72, 0x74, 0x66, 0x6f, 0x6c, 0x69, 0x6f, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x1b, - 0x2e, 0x6d, 0x67, 0x6f, 0x2e, 0x70, 0x6f, 0x72, 0x74, 0x66, 0x6f, 0x6c, 0x69, 0x6f, 0x2e, 0x76, - 0x31, 0x2e, 0x50, 0x6f, 0x72, 0x74, 0x66, 0x6f, 0x6c, 0x69, 0x6f, 0x22, 0x21, 0x82, 0xd3, 0xe4, - 0x93, 0x02, 0x1b, 0x3a, 0x09, 0x70, 0x6f, 0x72, 0x74, 0x66, 0x6f, 0x6c, 0x69, 0x6f, 0x22, 0x0e, - 0x2f, 0x76, 0x31, 0x2f, 0x70, 0x6f, 0x72, 0x74, 0x66, 0x6f, 0x6c, 0x69, 0x6f, 0x73, 0x12, 0x7e, - 0x0a, 0x0e, 0x4c, 0x69, 0x73, 0x74, 0x50, 0x6f, 0x72, 0x74, 0x66, 0x6f, 0x6c, 0x69, 0x6f, 0x73, - 0x12, 0x27, 0x2e, 0x6d, 0x67, 0x6f, 0x2e, 0x70, 0x6f, 0x72, 0x74, 0x66, 0x6f, 0x6c, 0x69, 0x6f, - 0x2e, 0x76, 0x31, 0x2e, 0x4c, 0x69, 0x73, 0x74, 0x50, 0x6f, 0x72, 0x74, 0x66, 0x6f, 0x6c, 0x69, - 0x6f, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x28, 0x2e, 0x6d, 0x67, 0x6f, 0x2e, - 0x70, 0x6f, 0x72, 0x74, 0x66, 0x6f, 0x6c, 0x69, 0x6f, 0x2e, 0x76, 0x31, 0x2e, 0x4c, 0x69, 0x73, - 0x74, 0x50, 0x6f, 0x72, 0x74, 0x66, 0x6f, 0x6c, 0x69, 0x6f, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, - 0x6e, 0x73, 0x65, 0x22, 0x19, 0x82, 0xd3, 0xe4, 0x93, 0x02, 0x10, 0x12, 0x0e, 0x2f, 0x76, 0x31, - 0x2f, 0x70, 0x6f, 0x72, 0x74, 0x66, 0x6f, 0x6c, 0x69, 0x6f, 0x73, 0x90, 0x02, 0x01, 0x12, 0x72, - 0x0a, 0x0c, 0x47, 0x65, 0x74, 0x50, 0x6f, 0x72, 0x74, 0x66, 0x6f, 0x6c, 0x69, 0x6f, 0x12, 0x25, - 0x2e, 0x6d, 0x67, 0x6f, 0x2e, 0x70, 0x6f, 0x72, 0x74, 0x66, 0x6f, 0x6c, 0x69, 0x6f, 0x2e, 0x76, - 0x31, 0x2e, 0x47, 0x65, 0x74, 0x50, 0x6f, 0x72, 0x74, 0x66, 0x6f, 0x6c, 0x69, 0x6f, 0x52, 0x65, - 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x1b, 0x2e, 0x6d, 0x67, 0x6f, 0x2e, 0x70, 0x6f, 0x72, 0x74, - 0x66, 0x6f, 0x6c, 0x69, 0x6f, 0x2e, 0x76, 0x31, 0x2e, 0x50, 0x6f, 0x72, 0x74, 0x66, 0x6f, 0x6c, - 0x69, 0x6f, 0x22, 0x1e, 0x82, 0xd3, 0xe4, 0x93, 0x02, 0x15, 0x12, 0x13, 0x2f, 0x76, 0x31, 0x2f, - 0x70, 0x6f, 0x72, 0x74, 0x66, 0x6f, 0x6c, 0x69, 0x6f, 0x73, 0x2f, 0x7b, 0x69, 0x64, 0x7d, 0x90, - 0x02, 0x01, 0x12, 0x58, 0x0a, 0x0f, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x50, 0x6f, 0x72, 0x74, - 0x66, 0x6f, 0x6c, 0x69, 0x6f, 0x12, 0x28, 0x2e, 0x6d, 0x67, 0x6f, 0x2e, 0x70, 0x6f, 0x72, 0x74, - 0x66, 0x6f, 0x6c, 0x69, 0x6f, 0x2e, 0x76, 0x31, 0x2e, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x50, - 0x6f, 0x72, 0x74, 0x66, 0x6f, 0x6c, 0x69, 0x6f, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, - 0x1b, 0x2e, 0x6d, 0x67, 0x6f, 0x2e, 0x70, 0x6f, 0x72, 0x74, 0x66, 0x6f, 0x6c, 0x69, 0x6f, 0x2e, - 0x76, 0x31, 0x2e, 0x50, 0x6f, 0x72, 0x74, 0x66, 0x6f, 0x6c, 0x69, 0x6f, 0x12, 0x53, 0x0a, 0x0f, - 0x44, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x50, 0x6f, 0x72, 0x74, 0x66, 0x6f, 0x6c, 0x69, 0x6f, 0x12, - 0x28, 0x2e, 0x6d, 0x67, 0x6f, 0x2e, 0x70, 0x6f, 0x72, 0x74, 0x66, 0x6f, 0x6c, 0x69, 0x6f, 0x2e, - 0x76, 0x31, 0x2e, 0x44, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x50, 0x6f, 0x72, 0x74, 0x66, 0x6f, 0x6c, - 0x69, 0x6f, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x16, 0x2e, 0x67, 0x6f, 0x6f, 0x67, - 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x45, 0x6d, 0x70, 0x74, - 0x79, 0x12, 0x9d, 0x01, 0x0a, 0x14, 0x47, 0x65, 0x74, 0x50, 0x6f, 0x72, 0x74, 0x66, 0x6f, 0x6c, - 0x69, 0x6f, 0x53, 0x6e, 0x61, 0x70, 0x73, 0x68, 0x6f, 0x74, 0x12, 0x2d, 0x2e, 0x6d, 0x67, 0x6f, - 0x2e, 0x70, 0x6f, 0x72, 0x74, 0x66, 0x6f, 0x6c, 0x69, 0x6f, 0x2e, 0x76, 0x31, 0x2e, 0x47, 0x65, - 0x74, 0x50, 0x6f, 0x72, 0x74, 0x66, 0x6f, 0x6c, 0x69, 0x6f, 0x53, 0x6e, 0x61, 0x70, 0x73, 0x68, - 0x6f, 0x74, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x23, 0x2e, 0x6d, 0x67, 0x6f, 0x2e, - 0x70, 0x6f, 0x72, 0x74, 0x66, 0x6f, 0x6c, 0x69, 0x6f, 0x2e, 0x76, 0x31, 0x2e, 0x50, 0x6f, 0x72, - 0x74, 0x66, 0x6f, 0x6c, 0x69, 0x6f, 0x53, 0x6e, 0x61, 0x70, 0x73, 0x68, 0x6f, 0x74, 0x22, 0x31, - 0x82, 0xd3, 0xe4, 0x93, 0x02, 0x28, 0x12, 0x26, 0x2f, 0x76, 0x31, 0x2f, 0x70, 0x6f, 0x72, 0x74, - 0x66, 0x6f, 0x6c, 0x69, 0x6f, 0x73, 0x2f, 0x7b, 0x70, 0x6f, 0x72, 0x74, 0x66, 0x6f, 0x6c, 0x69, - 0x6f, 0x5f, 0x69, 0x64, 0x7d, 0x2f, 0x73, 0x6e, 0x61, 0x70, 0x73, 0x68, 0x6f, 0x74, 0x90, 0x02, - 0x01, 0x12, 0xc0, 0x01, 0x0a, 0x1a, 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, 0x50, 0x6f, 0x72, 0x74, - 0x66, 0x6f, 0x6c, 0x69, 0x6f, 0x54, 0x72, 0x61, 0x6e, 0x73, 0x61, 0x63, 0x74, 0x69, 0x6f, 0x6e, - 0x12, 0x33, 0x2e, 0x6d, 0x67, 0x6f, 0x2e, 0x70, 0x6f, 0x72, 0x74, 0x66, 0x6f, 0x6c, 0x69, 0x6f, - 0x2e, 0x76, 0x31, 0x2e, 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, 0x50, 0x6f, 0x72, 0x74, 0x66, 0x6f, - 0x6c, 0x69, 0x6f, 0x54, 0x72, 0x61, 0x6e, 0x73, 0x61, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x52, 0x65, - 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x20, 0x2e, 0x6d, 0x67, 0x6f, 0x2e, 0x70, 0x6f, 0x72, 0x74, - 0x66, 0x6f, 0x6c, 0x69, 0x6f, 0x2e, 0x76, 0x31, 0x2e, 0x50, 0x6f, 0x72, 0x74, 0x66, 0x6f, 0x6c, - 0x69, 0x6f, 0x45, 0x76, 0x65, 0x6e, 0x74, 0x22, 0x4b, 0x82, 0xd3, 0xe4, 0x93, 0x02, 0x45, 0x3a, - 0x0b, 0x74, 0x72, 0x61, 0x6e, 0x73, 0x61, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x22, 0x36, 0x2f, 0x76, - 0x31, 0x2f, 0x70, 0x6f, 0x72, 0x74, 0x66, 0x6f, 0x6c, 0x69, 0x6f, 0x73, 0x2f, 0x7b, 0x74, 0x72, - 0x61, 0x6e, 0x73, 0x61, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x2e, 0x70, 0x6f, 0x72, 0x74, 0x66, 0x6f, - 0x6c, 0x69, 0x6f, 0x5f, 0x69, 0x64, 0x7d, 0x2f, 0x74, 0x72, 0x61, 0x6e, 0x73, 0x61, 0x63, 0x74, - 0x69, 0x6f, 0x6e, 0x73, 0x12, 0x8f, 0x01, 0x0a, 0x17, 0x47, 0x65, 0x74, 0x50, 0x6f, 0x72, 0x74, - 0x66, 0x6f, 0x6c, 0x69, 0x6f, 0x54, 0x72, 0x61, 0x6e, 0x73, 0x61, 0x63, 0x74, 0x69, 0x6f, 0x6e, - 0x12, 0x30, 0x2e, 0x6d, 0x67, 0x6f, 0x2e, 0x70, 0x6f, 0x72, 0x74, 0x66, 0x6f, 0x6c, 0x69, 0x6f, - 0x2e, 0x76, 0x31, 0x2e, 0x47, 0x65, 0x74, 0x50, 0x6f, 0x72, 0x74, 0x66, 0x6f, 0x6c, 0x69, 0x6f, - 0x54, 0x72, 0x61, 0x6e, 0x73, 0x61, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x52, 0x65, 0x71, 0x75, 0x65, - 0x73, 0x74, 0x1a, 0x20, 0x2e, 0x6d, 0x67, 0x6f, 0x2e, 0x70, 0x6f, 0x72, 0x74, 0x66, 0x6f, 0x6c, - 0x69, 0x6f, 0x2e, 0x76, 0x31, 0x2e, 0x50, 0x6f, 0x72, 0x74, 0x66, 0x6f, 0x6c, 0x69, 0x6f, 0x45, - 0x76, 0x65, 0x6e, 0x74, 0x22, 0x20, 0x82, 0xd3, 0xe4, 0x93, 0x02, 0x17, 0x12, 0x15, 0x2f, 0x76, - 0x31, 0x2f, 0x74, 0x72, 0x61, 0x6e, 0x73, 0x61, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x2f, 0x7b, - 0x69, 0x64, 0x7d, 0x90, 0x02, 0x01, 0x12, 0xbb, 0x01, 0x0a, 0x19, 0x4c, 0x69, 0x73, 0x74, 0x50, - 0x6f, 0x72, 0x74, 0x66, 0x6f, 0x6c, 0x69, 0x6f, 0x54, 0x72, 0x61, 0x6e, 0x73, 0x61, 0x63, 0x74, - 0x69, 0x6f, 0x6e, 0x73, 0x12, 0x32, 0x2e, 0x6d, 0x67, 0x6f, 0x2e, 0x70, 0x6f, 0x72, 0x74, 0x66, - 0x6f, 0x6c, 0x69, 0x6f, 0x2e, 0x76, 0x31, 0x2e, 0x4c, 0x69, 0x73, 0x74, 0x50, 0x6f, 0x72, 0x74, - 0x66, 0x6f, 0x6c, 0x69, 0x6f, 0x54, 0x72, 0x61, 0x6e, 0x73, 0x61, 0x63, 0x74, 0x69, 0x6f, 0x6e, - 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x33, 0x2e, 0x6d, 0x67, 0x6f, 0x2e, 0x70, - 0x6f, 0x72, 0x74, 0x66, 0x6f, 0x6c, 0x69, 0x6f, 0x2e, 0x76, 0x31, 0x2e, 0x4c, 0x69, 0x73, 0x74, - 0x50, 0x6f, 0x72, 0x74, 0x66, 0x6f, 0x6c, 0x69, 0x6f, 0x54, 0x72, 0x61, 0x6e, 0x73, 0x61, 0x63, - 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x35, 0x82, - 0xd3, 0xe4, 0x93, 0x02, 0x2c, 0x12, 0x2a, 0x2f, 0x76, 0x31, 0x2f, 0x70, 0x6f, 0x72, 0x74, 0x66, - 0x6f, 0x6c, 0x69, 0x6f, 0x73, 0x2f, 0x7b, 0x70, 0x6f, 0x72, 0x74, 0x66, 0x6f, 0x6c, 0x69, 0x6f, - 0x5f, 0x69, 0x64, 0x7d, 0x2f, 0x74, 0x72, 0x61, 0x6e, 0x73, 0x61, 0x63, 0x74, 0x69, 0x6f, 0x6e, - 0x73, 0x90, 0x02, 0x01, 0x12, 0xab, 0x01, 0x0a, 0x1a, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x50, - 0x6f, 0x72, 0x74, 0x66, 0x6f, 0x6c, 0x69, 0x6f, 0x54, 0x72, 0x61, 0x6e, 0x73, 0x61, 0x63, 0x74, - 0x69, 0x6f, 0x6e, 0x12, 0x33, 0x2e, 0x6d, 0x67, 0x6f, 0x2e, 0x70, 0x6f, 0x72, 0x74, 0x66, 0x6f, - 0x6c, 0x69, 0x6f, 0x2e, 0x76, 0x31, 0x2e, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x50, 0x6f, 0x72, - 0x74, 0x66, 0x6f, 0x6c, 0x69, 0x6f, 0x54, 0x72, 0x61, 0x6e, 0x73, 0x61, 0x63, 0x74, 0x69, 0x6f, - 0x6e, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x20, 0x2e, 0x6d, 0x67, 0x6f, 0x2e, 0x70, - 0x6f, 0x72, 0x74, 0x66, 0x6f, 0x6c, 0x69, 0x6f, 0x2e, 0x76, 0x31, 0x2e, 0x50, 0x6f, 0x72, 0x74, - 0x66, 0x6f, 0x6c, 0x69, 0x6f, 0x45, 0x76, 0x65, 0x6e, 0x74, 0x22, 0x36, 0x82, 0xd3, 0xe4, 0x93, - 0x02, 0x30, 0x3a, 0x0b, 0x74, 0x72, 0x61, 0x6e, 0x73, 0x61, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x1a, - 0x21, 0x2f, 0x76, 0x31, 0x2f, 0x74, 0x72, 0x61, 0x6e, 0x73, 0x61, 0x63, 0x74, 0x69, 0x6f, 0x6e, - 0x73, 0x2f, 0x7b, 0x74, 0x72, 0x61, 0x6e, 0x73, 0x61, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x2e, 0x69, - 0x64, 0x7d, 0x12, 0x69, 0x0a, 0x1a, 0x44, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x50, 0x6f, 0x72, 0x74, - 0x66, 0x6f, 0x6c, 0x69, 0x6f, 0x54, 0x72, 0x61, 0x6e, 0x73, 0x61, 0x63, 0x74, 0x69, 0x6f, 0x6e, - 0x12, 0x33, 0x2e, 0x6d, 0x67, 0x6f, 0x2e, 0x70, 0x6f, 0x72, 0x74, 0x66, 0x6f, 0x6c, 0x69, 0x6f, - 0x2e, 0x76, 0x31, 0x2e, 0x44, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x50, 0x6f, 0x72, 0x74, 0x66, 0x6f, - 0x6c, 0x69, 0x6f, 0x54, 0x72, 0x61, 0x6e, 0x73, 0x61, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x52, 0x65, - 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x16, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, - 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x12, 0x59, 0x0a, - 0x12, 0x49, 0x6d, 0x70, 0x6f, 0x72, 0x74, 0x54, 0x72, 0x61, 0x6e, 0x73, 0x61, 0x63, 0x74, 0x69, - 0x6f, 0x6e, 0x73, 0x12, 0x2b, 0x2e, 0x6d, 0x67, 0x6f, 0x2e, 0x70, 0x6f, 0x72, 0x74, 0x66, 0x6f, - 0x6c, 0x69, 0x6f, 0x2e, 0x76, 0x31, 0x2e, 0x49, 0x6d, 0x70, 0x6f, 0x72, 0x74, 0x54, 0x72, 0x61, - 0x6e, 0x73, 0x61, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, - 0x1a, 0x16, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, - 0x75, 0x66, 0x2e, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x12, 0x5e, 0x0a, 0x11, 0x43, 0x72, 0x65, 0x61, - 0x74, 0x65, 0x42, 0x61, 0x6e, 0x6b, 0x41, 0x63, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x12, 0x2a, 0x2e, - 0x6d, 0x67, 0x6f, 0x2e, 0x70, 0x6f, 0x72, 0x74, 0x66, 0x6f, 0x6c, 0x69, 0x6f, 0x2e, 0x76, 0x31, - 0x2e, 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, 0x42, 0x61, 0x6e, 0x6b, 0x41, 0x63, 0x63, 0x6f, 0x75, - 0x6e, 0x74, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x1d, 0x2e, 0x6d, 0x67, 0x6f, 0x2e, - 0x70, 0x6f, 0x72, 0x74, 0x66, 0x6f, 0x6c, 0x69, 0x6f, 0x2e, 0x76, 0x31, 0x2e, 0x42, 0x61, 0x6e, - 0x6b, 0x41, 0x63, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x12, 0x5e, 0x0a, 0x11, 0x55, 0x70, 0x64, 0x61, - 0x74, 0x65, 0x42, 0x61, 0x6e, 0x6b, 0x41, 0x63, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x12, 0x2a, 0x2e, - 0x6d, 0x67, 0x6f, 0x2e, 0x70, 0x6f, 0x72, 0x74, 0x66, 0x6f, 0x6c, 0x69, 0x6f, 0x2e, 0x76, 0x31, - 0x2e, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x42, 0x61, 0x6e, 0x6b, 0x41, 0x63, 0x63, 0x6f, 0x75, - 0x6e, 0x74, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x1d, 0x2e, 0x6d, 0x67, 0x6f, 0x2e, - 0x70, 0x6f, 0x72, 0x74, 0x66, 0x6f, 0x6c, 0x69, 0x6f, 0x2e, 0x76, 0x31, 0x2e, 0x42, 0x61, 0x6e, - 0x6b, 0x41, 0x63, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x12, 0x57, 0x0a, 0x11, 0x44, 0x65, 0x6c, 0x65, - 0x74, 0x65, 0x42, 0x61, 0x6e, 0x6b, 0x41, 0x63, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x12, 0x2a, 0x2e, - 0x6d, 0x67, 0x6f, 0x2e, 0x70, 0x6f, 0x72, 0x74, 0x66, 0x6f, 0x6c, 0x69, 0x6f, 0x2e, 0x76, 0x31, - 0x2e, 0x44, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x42, 0x61, 0x6e, 0x6b, 0x41, 0x63, 0x63, 0x6f, 0x75, - 0x6e, 0x74, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x16, 0x2e, 0x67, 0x6f, 0x6f, 0x67, - 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x45, 0x6d, 0x70, 0x74, - 0x79, 0x32, 0xfe, 0x04, 0x0a, 0x11, 0x53, 0x65, 0x63, 0x75, 0x72, 0x69, 0x74, 0x69, 0x65, 0x73, - 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x12, 0x7e, 0x0a, 0x0e, 0x4c, 0x69, 0x73, 0x74, 0x53, - 0x65, 0x63, 0x75, 0x72, 0x69, 0x74, 0x69, 0x65, 0x73, 0x12, 0x27, 0x2e, 0x6d, 0x67, 0x6f, 0x2e, - 0x70, 0x6f, 0x72, 0x74, 0x66, 0x6f, 0x6c, 0x69, 0x6f, 0x2e, 0x76, 0x31, 0x2e, 0x4c, 0x69, 0x73, - 0x74, 0x53, 0x65, 0x63, 0x75, 0x72, 0x69, 0x74, 0x69, 0x65, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, - 0x73, 0x74, 0x1a, 0x28, 0x2e, 0x6d, 0x67, 0x6f, 0x2e, 0x70, 0x6f, 0x72, 0x74, 0x66, 0x6f, 0x6c, - 0x69, 0x6f, 0x2e, 0x76, 0x31, 0x2e, 0x4c, 0x69, 0x73, 0x74, 0x53, 0x65, 0x63, 0x75, 0x72, 0x69, - 0x74, 0x69, 0x65, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x19, 0x82, 0xd3, - 0xe4, 0x93, 0x02, 0x10, 0x12, 0x0e, 0x2f, 0x76, 0x31, 0x2f, 0x73, 0x65, 0x63, 0x75, 0x72, 0x69, - 0x74, 0x69, 0x65, 0x73, 0x90, 0x02, 0x01, 0x12, 0x6f, 0x0a, 0x0b, 0x47, 0x65, 0x74, 0x53, 0x65, - 0x63, 0x75, 0x72, 0x69, 0x74, 0x79, 0x12, 0x24, 0x2e, 0x6d, 0x67, 0x6f, 0x2e, 0x70, 0x6f, 0x72, - 0x74, 0x66, 0x6f, 0x6c, 0x69, 0x6f, 0x2e, 0x76, 0x31, 0x2e, 0x47, 0x65, 0x74, 0x53, 0x65, 0x63, - 0x75, 0x72, 0x69, 0x74, 0x79, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x1a, 0x2e, 0x6d, - 0x67, 0x6f, 0x2e, 0x70, 0x6f, 0x72, 0x74, 0x66, 0x6f, 0x6c, 0x69, 0x6f, 0x2e, 0x76, 0x31, 0x2e, - 0x53, 0x65, 0x63, 0x75, 0x72, 0x69, 0x74, 0x79, 0x22, 0x1e, 0x82, 0xd3, 0xe4, 0x93, 0x02, 0x15, - 0x12, 0x13, 0x2f, 0x76, 0x31, 0x2f, 0x73, 0x65, 0x63, 0x75, 0x72, 0x69, 0x74, 0x69, 0x65, 0x73, - 0x2f, 0x7b, 0x69, 0x64, 0x7d, 0x90, 0x02, 0x01, 0x12, 0x55, 0x0a, 0x0e, 0x43, 0x72, 0x65, 0x61, - 0x74, 0x65, 0x53, 0x65, 0x63, 0x75, 0x72, 0x69, 0x74, 0x79, 0x12, 0x27, 0x2e, 0x6d, 0x67, 0x6f, - 0x2e, 0x70, 0x6f, 0x72, 0x74, 0x66, 0x6f, 0x6c, 0x69, 0x6f, 0x2e, 0x76, 0x31, 0x2e, 0x43, 0x72, - 0x65, 0x61, 0x74, 0x65, 0x53, 0x65, 0x63, 0x75, 0x72, 0x69, 0x74, 0x79, 0x52, 0x65, 0x71, 0x75, - 0x65, 0x73, 0x74, 0x1a, 0x1a, 0x2e, 0x6d, 0x67, 0x6f, 0x2e, 0x70, 0x6f, 0x72, 0x74, 0x66, 0x6f, - 0x6c, 0x69, 0x6f, 0x2e, 0x76, 0x31, 0x2e, 0x53, 0x65, 0x63, 0x75, 0x72, 0x69, 0x74, 0x79, 0x12, - 0x55, 0x0a, 0x0e, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x53, 0x65, 0x63, 0x75, 0x72, 0x69, 0x74, - 0x79, 0x12, 0x27, 0x2e, 0x6d, 0x67, 0x6f, 0x2e, 0x70, 0x6f, 0x72, 0x74, 0x66, 0x6f, 0x6c, 0x69, - 0x6f, 0x2e, 0x76, 0x31, 0x2e, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x53, 0x65, 0x63, 0x75, 0x72, - 0x69, 0x74, 0x79, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x1a, 0x2e, 0x6d, 0x67, 0x6f, - 0x2e, 0x70, 0x6f, 0x72, 0x74, 0x66, 0x6f, 0x6c, 0x69, 0x6f, 0x2e, 0x76, 0x31, 0x2e, 0x53, 0x65, - 0x63, 0x75, 0x72, 0x69, 0x74, 0x79, 0x12, 0x51, 0x0a, 0x0e, 0x44, 0x65, 0x6c, 0x65, 0x74, 0x65, - 0x53, 0x65, 0x63, 0x75, 0x72, 0x69, 0x74, 0x79, 0x12, 0x27, 0x2e, 0x6d, 0x67, 0x6f, 0x2e, 0x70, - 0x6f, 0x72, 0x74, 0x66, 0x6f, 0x6c, 0x69, 0x6f, 0x2e, 0x76, 0x31, 0x2e, 0x44, 0x65, 0x6c, 0x65, - 0x74, 0x65, 0x53, 0x65, 0x63, 0x75, 0x72, 0x69, 0x74, 0x79, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, - 0x74, 0x1a, 0x16, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, - 0x62, 0x75, 0x66, 0x2e, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x12, 0x77, 0x0a, 0x1a, 0x54, 0x72, 0x69, - 0x67, 0x67, 0x65, 0x72, 0x53, 0x65, 0x63, 0x75, 0x72, 0x69, 0x74, 0x79, 0x51, 0x75, 0x6f, 0x74, - 0x65, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x12, 0x2b, 0x2e, 0x6d, 0x67, 0x6f, 0x2e, 0x70, 0x6f, - 0x72, 0x74, 0x66, 0x6f, 0x6c, 0x69, 0x6f, 0x2e, 0x76, 0x31, 0x2e, 0x54, 0x72, 0x69, 0x67, 0x67, - 0x65, 0x72, 0x51, 0x75, 0x6f, 0x74, 0x65, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x52, 0x65, 0x71, - 0x75, 0x65, 0x73, 0x74, 0x1a, 0x2c, 0x2e, 0x6d, 0x67, 0x6f, 0x2e, 0x70, 0x6f, 0x72, 0x74, 0x66, - 0x6f, 0x6c, 0x69, 0x6f, 0x2e, 0x76, 0x31, 0x2e, 0x54, 0x72, 0x69, 0x67, 0x67, 0x65, 0x72, 0x51, - 0x75, 0x6f, 0x74, 0x65, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, - 0x73, 0x65, 0x42, 0x30, 0x5a, 0x2e, 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, - 0x2f, 0x6f, 0x78, 0x69, 0x73, 0x74, 0x6f, 0x2f, 0x6d, 0x6f, 0x6e, 0x65, 0x79, 0x2d, 0x67, 0x6f, - 0x70, 0x68, 0x65, 0x72, 0x2f, 0x67, 0x65, 0x6e, 0x3b, 0x70, 0x6f, 0x72, 0x74, 0x66, 0x6f, 0x6c, - 0x69, 0x6f, 0x76, 0x31, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, -} - -var ( - file_mgo_proto_rawDescOnce sync.Once - file_mgo_proto_rawDescData = file_mgo_proto_rawDesc -) - -func file_mgo_proto_rawDescGZIP() []byte { - file_mgo_proto_rawDescOnce.Do(func() { - file_mgo_proto_rawDescData = protoimpl.X.CompressGZIP(file_mgo_proto_rawDescData) - }) - return file_mgo_proto_rawDescData -} - -var file_mgo_proto_enumTypes = make([]protoimpl.EnumInfo, 1) -var file_mgo_proto_msgTypes = make([]protoimpl.MessageInfo, 35) -var file_mgo_proto_goTypes = []any{ - (PortfolioEventType)(0), // 0: mgo.portfolio.v1.PortfolioEventType - (*Currency)(nil), // 1: mgo.portfolio.v1.Currency - (*CreatePortfolioRequest)(nil), // 2: mgo.portfolio.v1.CreatePortfolioRequest - (*ListPortfoliosRequest)(nil), // 3: mgo.portfolio.v1.ListPortfoliosRequest - (*ListPortfoliosResponse)(nil), // 4: mgo.portfolio.v1.ListPortfoliosResponse - (*GetPortfolioRequest)(nil), // 5: mgo.portfolio.v1.GetPortfolioRequest - (*UpdatePortfolioRequest)(nil), // 6: mgo.portfolio.v1.UpdatePortfolioRequest - (*DeletePortfolioRequest)(nil), // 7: mgo.portfolio.v1.DeletePortfolioRequest - (*GetPortfolioSnapshotRequest)(nil), // 8: mgo.portfolio.v1.GetPortfolioSnapshotRequest - (*CreatePortfolioTransactionRequest)(nil), // 9: mgo.portfolio.v1.CreatePortfolioTransactionRequest - (*GetPortfolioTransactionRequest)(nil), // 10: mgo.portfolio.v1.GetPortfolioTransactionRequest - (*ListPortfolioTransactionsRequest)(nil), // 11: mgo.portfolio.v1.ListPortfolioTransactionsRequest - (*ListPortfolioTransactionsResponse)(nil), // 12: mgo.portfolio.v1.ListPortfolioTransactionsResponse - (*UpdatePortfolioTransactionRequest)(nil), // 13: mgo.portfolio.v1.UpdatePortfolioTransactionRequest - (*DeletePortfolioTransactionRequest)(nil), // 14: mgo.portfolio.v1.DeletePortfolioTransactionRequest - (*ImportTransactionsRequest)(nil), // 15: mgo.portfolio.v1.ImportTransactionsRequest - (*CreateBankAccountRequest)(nil), // 16: mgo.portfolio.v1.CreateBankAccountRequest - (*UpdateBankAccountRequest)(nil), // 17: mgo.portfolio.v1.UpdateBankAccountRequest - (*DeleteBankAccountRequest)(nil), // 18: mgo.portfolio.v1.DeleteBankAccountRequest - (*Portfolio)(nil), // 19: mgo.portfolio.v1.Portfolio - (*BankAccount)(nil), // 20: mgo.portfolio.v1.BankAccount - (*PortfolioSnapshot)(nil), // 21: mgo.portfolio.v1.PortfolioSnapshot - (*PortfolioPosition)(nil), // 22: mgo.portfolio.v1.PortfolioPosition - (*PortfolioEvent)(nil), // 23: mgo.portfolio.v1.PortfolioEvent - (*Security)(nil), // 24: mgo.portfolio.v1.Security - (*ListedSecurity)(nil), // 25: mgo.portfolio.v1.ListedSecurity - (*ListSecuritiesRequest)(nil), // 26: mgo.portfolio.v1.ListSecuritiesRequest - (*ListSecuritiesResponse)(nil), // 27: mgo.portfolio.v1.ListSecuritiesResponse - (*GetSecurityRequest)(nil), // 28: mgo.portfolio.v1.GetSecurityRequest - (*CreateSecurityRequest)(nil), // 29: mgo.portfolio.v1.CreateSecurityRequest - (*UpdateSecurityRequest)(nil), // 30: mgo.portfolio.v1.UpdateSecurityRequest - (*DeleteSecurityRequest)(nil), // 31: mgo.portfolio.v1.DeleteSecurityRequest - (*TriggerQuoteUpdateRequest)(nil), // 32: mgo.portfolio.v1.TriggerQuoteUpdateRequest - (*TriggerQuoteUpdateResponse)(nil), // 33: mgo.portfolio.v1.TriggerQuoteUpdateResponse - nil, // 34: mgo.portfolio.v1.PortfolioSnapshot.PositionsEntry - (*ListSecuritiesRequest_Filter)(nil), // 35: mgo.portfolio.v1.ListSecuritiesRequest.Filter - (*fieldmaskpb.FieldMask)(nil), // 36: google.protobuf.FieldMask - (*timestamppb.Timestamp)(nil), // 37: google.protobuf.Timestamp - (*emptypb.Empty)(nil), // 38: google.protobuf.Empty -} -var file_mgo_proto_depIdxs = []int32{ - 19, // 0: mgo.portfolio.v1.CreatePortfolioRequest.portfolio:type_name -> mgo.portfolio.v1.Portfolio - 19, // 1: mgo.portfolio.v1.ListPortfoliosResponse.portfolios:type_name -> mgo.portfolio.v1.Portfolio - 19, // 2: mgo.portfolio.v1.UpdatePortfolioRequest.portfolio:type_name -> mgo.portfolio.v1.Portfolio - 36, // 3: mgo.portfolio.v1.UpdatePortfolioRequest.updateMask:type_name -> google.protobuf.FieldMask - 37, // 4: mgo.portfolio.v1.GetPortfolioSnapshotRequest.time:type_name -> google.protobuf.Timestamp - 23, // 5: mgo.portfolio.v1.CreatePortfolioTransactionRequest.transaction:type_name -> mgo.portfolio.v1.PortfolioEvent - 23, // 6: mgo.portfolio.v1.ListPortfolioTransactionsResponse.transactions:type_name -> mgo.portfolio.v1.PortfolioEvent - 23, // 7: mgo.portfolio.v1.UpdatePortfolioTransactionRequest.transaction:type_name -> mgo.portfolio.v1.PortfolioEvent - 36, // 8: mgo.portfolio.v1.UpdatePortfolioTransactionRequest.updateMask:type_name -> google.protobuf.FieldMask - 20, // 9: mgo.portfolio.v1.CreateBankAccountRequest.bank_account:type_name -> mgo.portfolio.v1.BankAccount - 20, // 10: mgo.portfolio.v1.UpdateBankAccountRequest.account:type_name -> mgo.portfolio.v1.BankAccount - 36, // 11: mgo.portfolio.v1.UpdateBankAccountRequest.updateMask:type_name -> google.protobuf.FieldMask - 23, // 12: mgo.portfolio.v1.Portfolio.events:type_name -> mgo.portfolio.v1.PortfolioEvent - 37, // 13: mgo.portfolio.v1.PortfolioSnapshot.time:type_name -> google.protobuf.Timestamp - 34, // 14: mgo.portfolio.v1.PortfolioSnapshot.positions:type_name -> mgo.portfolio.v1.PortfolioSnapshot.PositionsEntry - 37, // 15: mgo.portfolio.v1.PortfolioSnapshot.first_transaction_time:type_name -> google.protobuf.Timestamp - 1, // 16: mgo.portfolio.v1.PortfolioSnapshot.total_purchase_value:type_name -> mgo.portfolio.v1.Currency - 1, // 17: mgo.portfolio.v1.PortfolioSnapshot.total_market_value:type_name -> mgo.portfolio.v1.Currency - 1, // 18: mgo.portfolio.v1.PortfolioSnapshot.total_profit_or_loss:type_name -> mgo.portfolio.v1.Currency - 1, // 19: mgo.portfolio.v1.PortfolioSnapshot.cash:type_name -> mgo.portfolio.v1.Currency - 1, // 20: mgo.portfolio.v1.PortfolioSnapshot.total_portfolio_value:type_name -> mgo.portfolio.v1.Currency - 24, // 21: mgo.portfolio.v1.PortfolioPosition.security:type_name -> mgo.portfolio.v1.Security - 1, // 22: mgo.portfolio.v1.PortfolioPosition.purchase_value:type_name -> mgo.portfolio.v1.Currency - 1, // 23: mgo.portfolio.v1.PortfolioPosition.purchase_price:type_name -> mgo.portfolio.v1.Currency - 1, // 24: mgo.portfolio.v1.PortfolioPosition.market_value:type_name -> mgo.portfolio.v1.Currency - 1, // 25: mgo.portfolio.v1.PortfolioPosition.market_price:type_name -> mgo.portfolio.v1.Currency - 1, // 26: mgo.portfolio.v1.PortfolioPosition.total_fees:type_name -> mgo.portfolio.v1.Currency - 1, // 27: mgo.portfolio.v1.PortfolioPosition.profit_or_loss:type_name -> mgo.portfolio.v1.Currency - 0, // 28: mgo.portfolio.v1.PortfolioEvent.type:type_name -> mgo.portfolio.v1.PortfolioEventType - 37, // 29: mgo.portfolio.v1.PortfolioEvent.time:type_name -> google.protobuf.Timestamp - 1, // 30: mgo.portfolio.v1.PortfolioEvent.price:type_name -> mgo.portfolio.v1.Currency - 1, // 31: mgo.portfolio.v1.PortfolioEvent.fees:type_name -> mgo.portfolio.v1.Currency - 1, // 32: mgo.portfolio.v1.PortfolioEvent.taxes:type_name -> mgo.portfolio.v1.Currency - 25, // 33: mgo.portfolio.v1.Security.listed_on:type_name -> mgo.portfolio.v1.ListedSecurity - 1, // 34: mgo.portfolio.v1.ListedSecurity.latest_quote:type_name -> mgo.portfolio.v1.Currency - 37, // 35: mgo.portfolio.v1.ListedSecurity.latest_quote_timestamp:type_name -> google.protobuf.Timestamp - 35, // 36: mgo.portfolio.v1.ListSecuritiesRequest.filter:type_name -> mgo.portfolio.v1.ListSecuritiesRequest.Filter - 24, // 37: mgo.portfolio.v1.ListSecuritiesResponse.securities:type_name -> mgo.portfolio.v1.Security - 24, // 38: mgo.portfolio.v1.CreateSecurityRequest.security:type_name -> mgo.portfolio.v1.Security - 24, // 39: mgo.portfolio.v1.UpdateSecurityRequest.security:type_name -> mgo.portfolio.v1.Security - 36, // 40: mgo.portfolio.v1.UpdateSecurityRequest.updateMask:type_name -> google.protobuf.FieldMask - 22, // 41: mgo.portfolio.v1.PortfolioSnapshot.PositionsEntry.value:type_name -> mgo.portfolio.v1.PortfolioPosition - 2, // 42: mgo.portfolio.v1.PortfolioService.CreatePortfolio:input_type -> mgo.portfolio.v1.CreatePortfolioRequest - 3, // 43: mgo.portfolio.v1.PortfolioService.ListPortfolios:input_type -> mgo.portfolio.v1.ListPortfoliosRequest - 5, // 44: mgo.portfolio.v1.PortfolioService.GetPortfolio:input_type -> mgo.portfolio.v1.GetPortfolioRequest - 6, // 45: mgo.portfolio.v1.PortfolioService.UpdatePortfolio:input_type -> mgo.portfolio.v1.UpdatePortfolioRequest - 7, // 46: mgo.portfolio.v1.PortfolioService.DeletePortfolio:input_type -> mgo.portfolio.v1.DeletePortfolioRequest - 8, // 47: mgo.portfolio.v1.PortfolioService.GetPortfolioSnapshot:input_type -> mgo.portfolio.v1.GetPortfolioSnapshotRequest - 9, // 48: mgo.portfolio.v1.PortfolioService.CreatePortfolioTransaction:input_type -> mgo.portfolio.v1.CreatePortfolioTransactionRequest - 10, // 49: mgo.portfolio.v1.PortfolioService.GetPortfolioTransaction:input_type -> mgo.portfolio.v1.GetPortfolioTransactionRequest - 11, // 50: mgo.portfolio.v1.PortfolioService.ListPortfolioTransactions:input_type -> mgo.portfolio.v1.ListPortfolioTransactionsRequest - 13, // 51: mgo.portfolio.v1.PortfolioService.UpdatePortfolioTransaction:input_type -> mgo.portfolio.v1.UpdatePortfolioTransactionRequest - 14, // 52: mgo.portfolio.v1.PortfolioService.DeletePortfolioTransaction:input_type -> mgo.portfolio.v1.DeletePortfolioTransactionRequest - 15, // 53: mgo.portfolio.v1.PortfolioService.ImportTransactions:input_type -> mgo.portfolio.v1.ImportTransactionsRequest - 16, // 54: mgo.portfolio.v1.PortfolioService.CreateBankAccount:input_type -> mgo.portfolio.v1.CreateBankAccountRequest - 17, // 55: mgo.portfolio.v1.PortfolioService.UpdateBankAccount:input_type -> mgo.portfolio.v1.UpdateBankAccountRequest - 18, // 56: mgo.portfolio.v1.PortfolioService.DeleteBankAccount:input_type -> mgo.portfolio.v1.DeleteBankAccountRequest - 26, // 57: mgo.portfolio.v1.SecuritiesService.ListSecurities:input_type -> mgo.portfolio.v1.ListSecuritiesRequest - 28, // 58: mgo.portfolio.v1.SecuritiesService.GetSecurity:input_type -> mgo.portfolio.v1.GetSecurityRequest - 29, // 59: mgo.portfolio.v1.SecuritiesService.CreateSecurity:input_type -> mgo.portfolio.v1.CreateSecurityRequest - 30, // 60: mgo.portfolio.v1.SecuritiesService.UpdateSecurity:input_type -> mgo.portfolio.v1.UpdateSecurityRequest - 31, // 61: mgo.portfolio.v1.SecuritiesService.DeleteSecurity:input_type -> mgo.portfolio.v1.DeleteSecurityRequest - 32, // 62: mgo.portfolio.v1.SecuritiesService.TriggerSecurityQuoteUpdate:input_type -> mgo.portfolio.v1.TriggerQuoteUpdateRequest - 19, // 63: mgo.portfolio.v1.PortfolioService.CreatePortfolio:output_type -> mgo.portfolio.v1.Portfolio - 4, // 64: mgo.portfolio.v1.PortfolioService.ListPortfolios:output_type -> mgo.portfolio.v1.ListPortfoliosResponse - 19, // 65: mgo.portfolio.v1.PortfolioService.GetPortfolio:output_type -> mgo.portfolio.v1.Portfolio - 19, // 66: mgo.portfolio.v1.PortfolioService.UpdatePortfolio:output_type -> mgo.portfolio.v1.Portfolio - 38, // 67: mgo.portfolio.v1.PortfolioService.DeletePortfolio:output_type -> google.protobuf.Empty - 21, // 68: mgo.portfolio.v1.PortfolioService.GetPortfolioSnapshot:output_type -> mgo.portfolio.v1.PortfolioSnapshot - 23, // 69: mgo.portfolio.v1.PortfolioService.CreatePortfolioTransaction:output_type -> mgo.portfolio.v1.PortfolioEvent - 23, // 70: mgo.portfolio.v1.PortfolioService.GetPortfolioTransaction:output_type -> mgo.portfolio.v1.PortfolioEvent - 12, // 71: mgo.portfolio.v1.PortfolioService.ListPortfolioTransactions:output_type -> mgo.portfolio.v1.ListPortfolioTransactionsResponse - 23, // 72: mgo.portfolio.v1.PortfolioService.UpdatePortfolioTransaction:output_type -> mgo.portfolio.v1.PortfolioEvent - 38, // 73: mgo.portfolio.v1.PortfolioService.DeletePortfolioTransaction:output_type -> google.protobuf.Empty - 38, // 74: mgo.portfolio.v1.PortfolioService.ImportTransactions:output_type -> google.protobuf.Empty - 20, // 75: mgo.portfolio.v1.PortfolioService.CreateBankAccount:output_type -> mgo.portfolio.v1.BankAccount - 20, // 76: mgo.portfolio.v1.PortfolioService.UpdateBankAccount:output_type -> mgo.portfolio.v1.BankAccount - 38, // 77: mgo.portfolio.v1.PortfolioService.DeleteBankAccount:output_type -> google.protobuf.Empty - 27, // 78: mgo.portfolio.v1.SecuritiesService.ListSecurities:output_type -> mgo.portfolio.v1.ListSecuritiesResponse - 24, // 79: mgo.portfolio.v1.SecuritiesService.GetSecurity:output_type -> mgo.portfolio.v1.Security - 24, // 80: mgo.portfolio.v1.SecuritiesService.CreateSecurity:output_type -> mgo.portfolio.v1.Security - 24, // 81: mgo.portfolio.v1.SecuritiesService.UpdateSecurity:output_type -> mgo.portfolio.v1.Security - 38, // 82: mgo.portfolio.v1.SecuritiesService.DeleteSecurity:output_type -> google.protobuf.Empty - 33, // 83: mgo.portfolio.v1.SecuritiesService.TriggerSecurityQuoteUpdate:output_type -> mgo.portfolio.v1.TriggerQuoteUpdateResponse - 63, // [63:84] is the sub-list for method output_type - 42, // [42:63] is the sub-list for method input_type - 42, // [42:42] is the sub-list for extension type_name - 42, // [42:42] is the sub-list for extension extendee - 0, // [0:42] is the sub-list for field type_name -} - -func init() { file_mgo_proto_init() } -func file_mgo_proto_init() { - if File_mgo_proto != nil { - return - } - file_mgo_proto_msgTypes[20].OneofWrappers = []any{} - file_mgo_proto_msgTypes[23].OneofWrappers = []any{} - file_mgo_proto_msgTypes[24].OneofWrappers = []any{} - file_mgo_proto_msgTypes[25].OneofWrappers = []any{} - type x struct{} - out := protoimpl.TypeBuilder{ - File: protoimpl.DescBuilder{ - GoPackagePath: reflect.TypeOf(x{}).PkgPath(), - RawDescriptor: file_mgo_proto_rawDesc, - NumEnums: 1, - NumMessages: 35, - NumExtensions: 0, - NumServices: 2, - }, - GoTypes: file_mgo_proto_goTypes, - DependencyIndexes: file_mgo_proto_depIdxs, - EnumInfos: file_mgo_proto_enumTypes, - MessageInfos: file_mgo_proto_msgTypes, - }.Build() - File_mgo_proto = out.File - file_mgo_proto_rawDesc = nil - file_mgo_proto_goTypes = nil - file_mgo_proto_depIdxs = nil -} diff --git a/gen/portfolio.go b/gen/portfolio.go deleted file mode 100644 index 110e60b4..00000000 --- a/gen/portfolio.go +++ /dev/null @@ -1,90 +0,0 @@ -// Copyright 2023 Christian Banse -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. -// -// This file is part of The Money Gopher. - -package portfoliov1 - -import ( - "hash/fnv" - "log/slog" - "strconv" - "time" -) - -func (p *Portfolio) EventMap() (m map[string][]*PortfolioEvent) { - m = make(map[string][]*PortfolioEvent) - - for _, tx := range p.Events { - name := tx.GetSecurityId() - if name != "" { - m[name] = append(m[name], tx) - } else { - // a little bit of a hack - m["cash"] = append(m["cash"], tx) - } - } - - return -} - -func EventsBefore(txs []*PortfolioEvent, t time.Time) (out []*PortfolioEvent) { - out = make([]*PortfolioEvent, 0, len(txs)) - - for _, tx := range txs { - if tx.GetTime().AsTime().After(t) { - continue - } - - out = append(out, tx) - } - - return -} - -func (tx *PortfolioEvent) MakeUniqueID() { - // Create a unique ID based on a hash containing: - // - security ID - // - portfolio ID - // - date - // - amount - h := fnv.New64a() - h.Write([]byte(tx.SecurityId)) - h.Write([]byte(tx.PortfolioId)) - h.Write([]byte(tx.Time.AsTime().Local().Format(time.DateTime))) - h.Write([]byte(strconv.FormatInt(int64(tx.Type), 10))) - h.Write([]byte(strconv.FormatInt(int64(tx.Amount), 10))) - - tx.Id = strconv.FormatUint(h.Sum64(), 16) -} - -// LogValue implements slog.LogValuer. -func (tx *PortfolioEvent) LogValue() slog.Value { - return slog.GroupValue( - slog.String("id", tx.Id), - slog.String("security.id", tx.SecurityId), - slog.Float64("amount", float64(tx.Amount)), - slog.String("price", tx.Price.Pretty()), - slog.String("fees", tx.Fees.Pretty()), - slog.String("taxes", tx.Taxes.Pretty()), - ) -} - -// LogValue implements slog.LogValuer. -func (ls *ListedSecurity) LogValue() slog.Value { - return slog.GroupValue( - slog.String("id", ls.SecurityId), - slog.String("ticker", ls.Ticker), - ) -} diff --git a/gen/portfolio_sql.go b/gen/portfolio_sql.go deleted file mode 100644 index 2ca4eb75..00000000 --- a/gen/portfolio_sql.go +++ /dev/null @@ -1,231 +0,0 @@ -// Copyright 2023 Christian Banse -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. -// -// This file is part of The Money Gopher. - -package portfoliov1 - -import ( - "database/sql" - "errors" - "strings" - "time" - - "github.com/oxisto/money-gopher/persistence" - - timestamppb "google.golang.org/protobuf/types/known/timestamppb" -) - -var _ persistence.StorageObject = &Portfolio{} - -func (*Portfolio) InitTables(db *persistence.DB) (err error) { - _, err1 := db.Exec(`CREATE TABLE IF NOT EXISTS portfolios ( -id TEXT PRIMARY KEY, -display_name TEXT NOT NULL -);`) - err2 := (&PortfolioEvent{}).InitTables(db) - - return errors.Join(err1, err2) -} - -func (*Portfolio) PrepareReplace(db *persistence.DB) (stmt *sql.Stmt, err error) { - return db.Prepare(`REPLACE INTO portfolios (id, display_name, bank_account_id) VALUES (?,?,?);`) -} - -func (*Portfolio) PrepareList(db *persistence.DB) (stmt *sql.Stmt, err error) { - return db.Prepare(`SELECT id, display_name FROM portfolios`) -} - -func (*Portfolio) PrepareGet(db *persistence.DB) (stmt *sql.Stmt, err error) { - return db.Prepare(`SELECT id, display_name FROM portfolios WHERE id = ?`) -} - -func (*Portfolio) PrepareUpdate(db *persistence.DB, columns []string) (stmt *sql.Stmt, err error) { - // We need to make sure to quote columns here because they are potentially evil user input - var ( - query string - set []string - ) - - set = make([]string, len(columns)) - for i, col := range columns { - set[i] = persistence.Quote(col) + " = ?" - } - - query += "UPDATE portfolios SET " + strings.Join(set, ", ") + " WHERE id = ?;" - - return db.Prepare(query) -} - -func (*Portfolio) PrepareDelete(db *persistence.DB) (stmt *sql.Stmt, err error) { - return db.Prepare(`DELETE FROM portfolios WHERE id = ?`) -} - -func (p *Portfolio) ReplaceIntoArgs() []any { - return []any{p.Id, p.DisplayName, p.BankAccountId} -} - -func (p *Portfolio) UpdateArgs(columns []string) (args []any) { - for _, col := range columns { - switch col { - case "id": - args = append(args, p.Id) - case "display_name": - args = append(args, p.DisplayName) - case "bank_account_id": - args = append(args, p.BankAccountId) - } - } - - return args -} - -func (*Portfolio) Scan(sc persistence.Scanner) (obj persistence.StorageObject, err error) { - var ( - p Portfolio - ) - - err = sc.Scan(&p.Id, &p.DisplayName) - if err != nil { - return nil, err - } - - return &p, nil -} - -func (*PortfolioEvent) InitTables(db *persistence.DB) (err error) { - _, err = db.Exec(`CREATE TABLE IF NOT EXISTS portfolio_events ( -id TEXT PRIMARY KEY, -type INTEGER NOT NULL, -time DATETIME NOT NULL, -portfolio_id TEXT NOT NULL, -security_id TEXT NOT NULL, -amount REAL, -price INTEGER, -fees INTEGER, -taxes INTEGER -);`) - if err != nil { - return err - } - - return -} - -func (*PortfolioEvent) PrepareReplace(db *persistence.DB) (stmt *sql.Stmt, err error) { - return db.Prepare(`REPLACE INTO portfolio_events -(id, type, time, portfolio_id, security_id, amount, price, fees, taxes) -VALUES (?,?,?,?,?,?,?,?,?);`) -} - -func (*PortfolioEvent) PrepareList(db *persistence.DB) (stmt *sql.Stmt, err error) { - return db.Prepare(`SELECT id, type, time, portfolio_id, security_id, amount, price, fees, taxes -FROM portfolio_events WHERE portfolio_id = ? ORDER BY time ASC`) -} - -func (*PortfolioEvent) PrepareGet(db *persistence.DB) (stmt *sql.Stmt, err error) { - return db.Prepare(`SELECT * FROM portfolio_events WHERE id = ?`) -} - -func (*PortfolioEvent) PrepareUpdate(db *persistence.DB, columns []string) (stmt *sql.Stmt, err error) { - // We need to make sure to quote columns here because they are potentially evil user input - var ( - query string - set []string - ) - - set = make([]string, len(columns)) - for i, col := range columns { - set[i] = persistence.Quote(col) + " = ?" - } - - query += "UPDATE portfolio_events SET " + strings.Join(set, ", ") + " WHERE id = ?;" - - return db.Prepare(query) -} - -func (*PortfolioEvent) PrepareDelete(db *persistence.DB) (stmt *sql.Stmt, err error) { - return db.Prepare(`DELETE FROM portfolio_events WHERE id = ?`) -} - -func (e *PortfolioEvent) ReplaceIntoArgs() []any { - return []any{ - e.Id, - e.Type, - e.Time.AsTime(), - e.PortfolioId, - e.SecurityId, - e.Amount, - e.Price.GetValue(), - e.Fees.GetValue(), - e.Taxes.GetValue(), - } -} - -func (e *PortfolioEvent) UpdateArgs(columns []string) (args []any) { - for _, col := range columns { - switch col { - case "id": - args = append(args, e.Id) - case "type": - args = append(args, e.Type) - case "time": - args = append(args, e.Time.AsTime()) - case "portfolio_id": - args = append(args, e.PortfolioId) - case "security_id": - args = append(args, e.SecurityId) - case "amount": - args = append(args, e.Amount) - case "price": - args = append(args, e.Price.GetValue()) - case "fees": - args = append(args, e.Fees.GetValue()) - case "taxes": - args = append(args, e.Taxes.GetValue()) - } - } - - return args -} - -func (*PortfolioEvent) Scan(sc persistence.Scanner) (obj persistence.StorageObject, err error) { - var ( - e PortfolioEvent - t time.Time - ) - - e.Price = Zero() - e.Fees = Zero() - e.Taxes = Zero() - - err = sc.Scan( - &e.Id, - &e.Type, - &t, - &e.PortfolioId, - &e.SecurityId, - &e.Amount, - &e.Price.Value, - &e.Fees.Value, - &e.Taxes.Value, - ) - if err != nil { - return nil, err - } - - e.Time = timestamppb.New(t) - - return &e, nil -} diff --git a/gen/portfolio_test.go b/gen/portfolio_test.go deleted file mode 100644 index f351892a..00000000 --- a/gen/portfolio_test.go +++ /dev/null @@ -1,75 +0,0 @@ -// Copyright 2023 Christian Banse -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. -// -// This file is part of The Money Gopher. - -package portfoliov1 - -import ( - "testing" - "time" - - "github.com/oxisto/assert" - "google.golang.org/protobuf/types/known/timestamppb" -) - -func TestPortfolioEvent_MakeUniqueName(t *testing.T) { - type fields struct { - Name string - Type PortfolioEventType - Time *timestamppb.Timestamp - PortfolioName string - SecurityId string - Amount float64 - Price *Currency - Fees *Currency - Taxes *Currency - } - tests := []struct { - name string - fields fields - want assert.Want[*PortfolioEvent] - }{ - { - name: "happy path", - fields: fields{ - SecurityId: "stock", - PortfolioName: "mybank-myportfolio", - Amount: 10, - Type: PortfolioEventType_PORTFOLIO_EVENT_TYPE_BUY, - Time: timestamppb.New(time.Date(2022, 1, 1, 0, 0, 0, 0, time.Local)), - }, - want: func(t *testing.T, tx *PortfolioEvent) bool { - return assert.Equals(t, "a04f32c39c6b9086", tx.Id) - }, - }, - } - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - tx := &PortfolioEvent{ - Id: tt.fields.Name, - Type: tt.fields.Type, - Time: tt.fields.Time, - PortfolioId: tt.fields.PortfolioName, - SecurityId: tt.fields.SecurityId, - Amount: tt.fields.Amount, - Price: tt.fields.Price, - Fees: tt.fields.Fees, - Taxes: tt.fields.Taxes, - } - tx.MakeUniqueID() - tt.want(t, tx) - }) - } -} diff --git a/gen/portfoliov1connect/mgo.connect.go b/gen/portfoliov1connect/mgo.connect.go deleted file mode 100644 index b0c39b9e..00000000 --- a/gen/portfoliov1connect/mgo.connect.go +++ /dev/null @@ -1,773 +0,0 @@ -// Code generated by protoc-gen-connect-go. DO NOT EDIT. -// -// Source: mgo.proto - -package portfoliov1connect - -import ( - connect "connectrpc.com/connect" - context "context" - errors "errors" - gen "github.com/oxisto/money-gopher/gen" - emptypb "google.golang.org/protobuf/types/known/emptypb" - http "net/http" - strings "strings" -) - -// This is a compile-time assertion to ensure that this generated file and the connect package are -// compatible. If you get a compiler error that this constant is not defined, this code was -// generated with a version of connect newer than the one compiled into your binary. You can fix the -// problem by either regenerating this code with an older version of connect or updating the connect -// version compiled into your binary. -const _ = connect.IsAtLeastVersion1_13_0 - -const ( - // PortfolioServiceName is the fully-qualified name of the PortfolioService service. - PortfolioServiceName = "mgo.portfolio.v1.PortfolioService" - // SecuritiesServiceName is the fully-qualified name of the SecuritiesService service. - SecuritiesServiceName = "mgo.portfolio.v1.SecuritiesService" -) - -// These constants are the fully-qualified names of the RPCs defined in this package. They're -// exposed at runtime as Spec.Procedure and as the final two segments of the HTTP route. -// -// Note that these are different from the fully-qualified method names used by -// google.golang.org/protobuf/reflect/protoreflect. To convert from these constants to -// reflection-formatted method names, remove the leading slash and convert the remaining slash to a -// period. -const ( - // PortfolioServiceCreatePortfolioProcedure is the fully-qualified name of the PortfolioService's - // CreatePortfolio RPC. - PortfolioServiceCreatePortfolioProcedure = "/mgo.portfolio.v1.PortfolioService/CreatePortfolio" - // PortfolioServiceListPortfoliosProcedure is the fully-qualified name of the PortfolioService's - // ListPortfolios RPC. - PortfolioServiceListPortfoliosProcedure = "/mgo.portfolio.v1.PortfolioService/ListPortfolios" - // PortfolioServiceGetPortfolioProcedure is the fully-qualified name of the PortfolioService's - // GetPortfolio RPC. - PortfolioServiceGetPortfolioProcedure = "/mgo.portfolio.v1.PortfolioService/GetPortfolio" - // PortfolioServiceUpdatePortfolioProcedure is the fully-qualified name of the PortfolioService's - // UpdatePortfolio RPC. - PortfolioServiceUpdatePortfolioProcedure = "/mgo.portfolio.v1.PortfolioService/UpdatePortfolio" - // PortfolioServiceDeletePortfolioProcedure is the fully-qualified name of the PortfolioService's - // DeletePortfolio RPC. - PortfolioServiceDeletePortfolioProcedure = "/mgo.portfolio.v1.PortfolioService/DeletePortfolio" - // PortfolioServiceGetPortfolioSnapshotProcedure is the fully-qualified name of the - // PortfolioService's GetPortfolioSnapshot RPC. - PortfolioServiceGetPortfolioSnapshotProcedure = "/mgo.portfolio.v1.PortfolioService/GetPortfolioSnapshot" - // PortfolioServiceCreatePortfolioTransactionProcedure is the fully-qualified name of the - // PortfolioService's CreatePortfolioTransaction RPC. - PortfolioServiceCreatePortfolioTransactionProcedure = "/mgo.portfolio.v1.PortfolioService/CreatePortfolioTransaction" - // PortfolioServiceGetPortfolioTransactionProcedure is the fully-qualified name of the - // PortfolioService's GetPortfolioTransaction RPC. - PortfolioServiceGetPortfolioTransactionProcedure = "/mgo.portfolio.v1.PortfolioService/GetPortfolioTransaction" - // PortfolioServiceListPortfolioTransactionsProcedure is the fully-qualified name of the - // PortfolioService's ListPortfolioTransactions RPC. - PortfolioServiceListPortfolioTransactionsProcedure = "/mgo.portfolio.v1.PortfolioService/ListPortfolioTransactions" - // PortfolioServiceUpdatePortfolioTransactionProcedure is the fully-qualified name of the - // PortfolioService's UpdatePortfolioTransaction RPC. - PortfolioServiceUpdatePortfolioTransactionProcedure = "/mgo.portfolio.v1.PortfolioService/UpdatePortfolioTransaction" - // PortfolioServiceDeletePortfolioTransactionProcedure is the fully-qualified name of the - // PortfolioService's DeletePortfolioTransaction RPC. - PortfolioServiceDeletePortfolioTransactionProcedure = "/mgo.portfolio.v1.PortfolioService/DeletePortfolioTransaction" - // PortfolioServiceImportTransactionsProcedure is the fully-qualified name of the PortfolioService's - // ImportTransactions RPC. - PortfolioServiceImportTransactionsProcedure = "/mgo.portfolio.v1.PortfolioService/ImportTransactions" - // PortfolioServiceCreateBankAccountProcedure is the fully-qualified name of the PortfolioService's - // CreateBankAccount RPC. - PortfolioServiceCreateBankAccountProcedure = "/mgo.portfolio.v1.PortfolioService/CreateBankAccount" - // PortfolioServiceUpdateBankAccountProcedure is the fully-qualified name of the PortfolioService's - // UpdateBankAccount RPC. - PortfolioServiceUpdateBankAccountProcedure = "/mgo.portfolio.v1.PortfolioService/UpdateBankAccount" - // PortfolioServiceDeleteBankAccountProcedure is the fully-qualified name of the PortfolioService's - // DeleteBankAccount RPC. - PortfolioServiceDeleteBankAccountProcedure = "/mgo.portfolio.v1.PortfolioService/DeleteBankAccount" - // SecuritiesServiceListSecuritiesProcedure is the fully-qualified name of the SecuritiesService's - // ListSecurities RPC. - SecuritiesServiceListSecuritiesProcedure = "/mgo.portfolio.v1.SecuritiesService/ListSecurities" - // SecuritiesServiceGetSecurityProcedure is the fully-qualified name of the SecuritiesService's - // GetSecurity RPC. - SecuritiesServiceGetSecurityProcedure = "/mgo.portfolio.v1.SecuritiesService/GetSecurity" - // SecuritiesServiceCreateSecurityProcedure is the fully-qualified name of the SecuritiesService's - // CreateSecurity RPC. - SecuritiesServiceCreateSecurityProcedure = "/mgo.portfolio.v1.SecuritiesService/CreateSecurity" - // SecuritiesServiceUpdateSecurityProcedure is the fully-qualified name of the SecuritiesService's - // UpdateSecurity RPC. - SecuritiesServiceUpdateSecurityProcedure = "/mgo.portfolio.v1.SecuritiesService/UpdateSecurity" - // SecuritiesServiceDeleteSecurityProcedure is the fully-qualified name of the SecuritiesService's - // DeleteSecurity RPC. - SecuritiesServiceDeleteSecurityProcedure = "/mgo.portfolio.v1.SecuritiesService/DeleteSecurity" - // SecuritiesServiceTriggerSecurityQuoteUpdateProcedure is the fully-qualified name of the - // SecuritiesService's TriggerSecurityQuoteUpdate RPC. - SecuritiesServiceTriggerSecurityQuoteUpdateProcedure = "/mgo.portfolio.v1.SecuritiesService/TriggerSecurityQuoteUpdate" -) - -// These variables are the protoreflect.Descriptor objects for the RPCs defined in this package. -var ( - portfolioServiceServiceDescriptor = gen.File_mgo_proto.Services().ByName("PortfolioService") - portfolioServiceCreatePortfolioMethodDescriptor = portfolioServiceServiceDescriptor.Methods().ByName("CreatePortfolio") - portfolioServiceListPortfoliosMethodDescriptor = portfolioServiceServiceDescriptor.Methods().ByName("ListPortfolios") - portfolioServiceGetPortfolioMethodDescriptor = portfolioServiceServiceDescriptor.Methods().ByName("GetPortfolio") - portfolioServiceUpdatePortfolioMethodDescriptor = portfolioServiceServiceDescriptor.Methods().ByName("UpdatePortfolio") - portfolioServiceDeletePortfolioMethodDescriptor = portfolioServiceServiceDescriptor.Methods().ByName("DeletePortfolio") - portfolioServiceGetPortfolioSnapshotMethodDescriptor = portfolioServiceServiceDescriptor.Methods().ByName("GetPortfolioSnapshot") - portfolioServiceCreatePortfolioTransactionMethodDescriptor = portfolioServiceServiceDescriptor.Methods().ByName("CreatePortfolioTransaction") - portfolioServiceGetPortfolioTransactionMethodDescriptor = portfolioServiceServiceDescriptor.Methods().ByName("GetPortfolioTransaction") - portfolioServiceListPortfolioTransactionsMethodDescriptor = portfolioServiceServiceDescriptor.Methods().ByName("ListPortfolioTransactions") - portfolioServiceUpdatePortfolioTransactionMethodDescriptor = portfolioServiceServiceDescriptor.Methods().ByName("UpdatePortfolioTransaction") - portfolioServiceDeletePortfolioTransactionMethodDescriptor = portfolioServiceServiceDescriptor.Methods().ByName("DeletePortfolioTransaction") - portfolioServiceImportTransactionsMethodDescriptor = portfolioServiceServiceDescriptor.Methods().ByName("ImportTransactions") - portfolioServiceCreateBankAccountMethodDescriptor = portfolioServiceServiceDescriptor.Methods().ByName("CreateBankAccount") - portfolioServiceUpdateBankAccountMethodDescriptor = portfolioServiceServiceDescriptor.Methods().ByName("UpdateBankAccount") - portfolioServiceDeleteBankAccountMethodDescriptor = portfolioServiceServiceDescriptor.Methods().ByName("DeleteBankAccount") - securitiesServiceServiceDescriptor = gen.File_mgo_proto.Services().ByName("SecuritiesService") - securitiesServiceListSecuritiesMethodDescriptor = securitiesServiceServiceDescriptor.Methods().ByName("ListSecurities") - securitiesServiceGetSecurityMethodDescriptor = securitiesServiceServiceDescriptor.Methods().ByName("GetSecurity") - securitiesServiceCreateSecurityMethodDescriptor = securitiesServiceServiceDescriptor.Methods().ByName("CreateSecurity") - securitiesServiceUpdateSecurityMethodDescriptor = securitiesServiceServiceDescriptor.Methods().ByName("UpdateSecurity") - securitiesServiceDeleteSecurityMethodDescriptor = securitiesServiceServiceDescriptor.Methods().ByName("DeleteSecurity") - securitiesServiceTriggerSecurityQuoteUpdateMethodDescriptor = securitiesServiceServiceDescriptor.Methods().ByName("TriggerSecurityQuoteUpdate") -) - -// PortfolioServiceClient is a client for the mgo.portfolio.v1.PortfolioService service. -type PortfolioServiceClient interface { - CreatePortfolio(context.Context, *connect.Request[gen.CreatePortfolioRequest]) (*connect.Response[gen.Portfolio], error) - ListPortfolios(context.Context, *connect.Request[gen.ListPortfoliosRequest]) (*connect.Response[gen.ListPortfoliosResponse], error) - GetPortfolio(context.Context, *connect.Request[gen.GetPortfolioRequest]) (*connect.Response[gen.Portfolio], error) - UpdatePortfolio(context.Context, *connect.Request[gen.UpdatePortfolioRequest]) (*connect.Response[gen.Portfolio], error) - DeletePortfolio(context.Context, *connect.Request[gen.DeletePortfolioRequest]) (*connect.Response[emptypb.Empty], error) - GetPortfolioSnapshot(context.Context, *connect.Request[gen.GetPortfolioSnapshotRequest]) (*connect.Response[gen.PortfolioSnapshot], error) - CreatePortfolioTransaction(context.Context, *connect.Request[gen.CreatePortfolioTransactionRequest]) (*connect.Response[gen.PortfolioEvent], error) - GetPortfolioTransaction(context.Context, *connect.Request[gen.GetPortfolioTransactionRequest]) (*connect.Response[gen.PortfolioEvent], error) - ListPortfolioTransactions(context.Context, *connect.Request[gen.ListPortfolioTransactionsRequest]) (*connect.Response[gen.ListPortfolioTransactionsResponse], error) - UpdatePortfolioTransaction(context.Context, *connect.Request[gen.UpdatePortfolioTransactionRequest]) (*connect.Response[gen.PortfolioEvent], error) - DeletePortfolioTransaction(context.Context, *connect.Request[gen.DeletePortfolioTransactionRequest]) (*connect.Response[emptypb.Empty], error) - ImportTransactions(context.Context, *connect.Request[gen.ImportTransactionsRequest]) (*connect.Response[emptypb.Empty], error) - CreateBankAccount(context.Context, *connect.Request[gen.CreateBankAccountRequest]) (*connect.Response[gen.BankAccount], error) - UpdateBankAccount(context.Context, *connect.Request[gen.UpdateBankAccountRequest]) (*connect.Response[gen.BankAccount], error) - DeleteBankAccount(context.Context, *connect.Request[gen.DeleteBankAccountRequest]) (*connect.Response[emptypb.Empty], error) -} - -// NewPortfolioServiceClient constructs a client for the mgo.portfolio.v1.PortfolioService service. -// By default, it uses the Connect protocol with the binary Protobuf Codec, asks for gzipped -// responses, and sends uncompressed requests. To use the gRPC or gRPC-Web protocols, supply the -// connect.WithGRPC() or connect.WithGRPCWeb() options. -// -// The URL supplied here should be the base URL for the Connect or gRPC server (for example, -// http://api.acme.com or https://acme.com/grpc). -func NewPortfolioServiceClient(httpClient connect.HTTPClient, baseURL string, opts ...connect.ClientOption) PortfolioServiceClient { - baseURL = strings.TrimRight(baseURL, "/") - return &portfolioServiceClient{ - createPortfolio: connect.NewClient[gen.CreatePortfolioRequest, gen.Portfolio]( - httpClient, - baseURL+PortfolioServiceCreatePortfolioProcedure, - connect.WithSchema(portfolioServiceCreatePortfolioMethodDescriptor), - connect.WithClientOptions(opts...), - ), - listPortfolios: connect.NewClient[gen.ListPortfoliosRequest, gen.ListPortfoliosResponse]( - httpClient, - baseURL+PortfolioServiceListPortfoliosProcedure, - connect.WithSchema(portfolioServiceListPortfoliosMethodDescriptor), - connect.WithIdempotency(connect.IdempotencyNoSideEffects), - connect.WithClientOptions(opts...), - ), - getPortfolio: connect.NewClient[gen.GetPortfolioRequest, gen.Portfolio]( - httpClient, - baseURL+PortfolioServiceGetPortfolioProcedure, - connect.WithSchema(portfolioServiceGetPortfolioMethodDescriptor), - connect.WithIdempotency(connect.IdempotencyNoSideEffects), - connect.WithClientOptions(opts...), - ), - updatePortfolio: connect.NewClient[gen.UpdatePortfolioRequest, gen.Portfolio]( - httpClient, - baseURL+PortfolioServiceUpdatePortfolioProcedure, - connect.WithSchema(portfolioServiceUpdatePortfolioMethodDescriptor), - connect.WithClientOptions(opts...), - ), - deletePortfolio: connect.NewClient[gen.DeletePortfolioRequest, emptypb.Empty]( - httpClient, - baseURL+PortfolioServiceDeletePortfolioProcedure, - connect.WithSchema(portfolioServiceDeletePortfolioMethodDescriptor), - connect.WithClientOptions(opts...), - ), - getPortfolioSnapshot: connect.NewClient[gen.GetPortfolioSnapshotRequest, gen.PortfolioSnapshot]( - httpClient, - baseURL+PortfolioServiceGetPortfolioSnapshotProcedure, - connect.WithSchema(portfolioServiceGetPortfolioSnapshotMethodDescriptor), - connect.WithIdempotency(connect.IdempotencyNoSideEffects), - connect.WithClientOptions(opts...), - ), - createPortfolioTransaction: connect.NewClient[gen.CreatePortfolioTransactionRequest, gen.PortfolioEvent]( - httpClient, - baseURL+PortfolioServiceCreatePortfolioTransactionProcedure, - connect.WithSchema(portfolioServiceCreatePortfolioTransactionMethodDescriptor), - connect.WithClientOptions(opts...), - ), - getPortfolioTransaction: connect.NewClient[gen.GetPortfolioTransactionRequest, gen.PortfolioEvent]( - httpClient, - baseURL+PortfolioServiceGetPortfolioTransactionProcedure, - connect.WithSchema(portfolioServiceGetPortfolioTransactionMethodDescriptor), - connect.WithIdempotency(connect.IdempotencyNoSideEffects), - connect.WithClientOptions(opts...), - ), - listPortfolioTransactions: connect.NewClient[gen.ListPortfolioTransactionsRequest, gen.ListPortfolioTransactionsResponse]( - httpClient, - baseURL+PortfolioServiceListPortfolioTransactionsProcedure, - connect.WithSchema(portfolioServiceListPortfolioTransactionsMethodDescriptor), - connect.WithIdempotency(connect.IdempotencyNoSideEffects), - connect.WithClientOptions(opts...), - ), - updatePortfolioTransaction: connect.NewClient[gen.UpdatePortfolioTransactionRequest, gen.PortfolioEvent]( - httpClient, - baseURL+PortfolioServiceUpdatePortfolioTransactionProcedure, - connect.WithSchema(portfolioServiceUpdatePortfolioTransactionMethodDescriptor), - connect.WithClientOptions(opts...), - ), - deletePortfolioTransaction: connect.NewClient[gen.DeletePortfolioTransactionRequest, emptypb.Empty]( - httpClient, - baseURL+PortfolioServiceDeletePortfolioTransactionProcedure, - connect.WithSchema(portfolioServiceDeletePortfolioTransactionMethodDescriptor), - connect.WithClientOptions(opts...), - ), - importTransactions: connect.NewClient[gen.ImportTransactionsRequest, emptypb.Empty]( - httpClient, - baseURL+PortfolioServiceImportTransactionsProcedure, - connect.WithSchema(portfolioServiceImportTransactionsMethodDescriptor), - connect.WithClientOptions(opts...), - ), - createBankAccount: connect.NewClient[gen.CreateBankAccountRequest, gen.BankAccount]( - httpClient, - baseURL+PortfolioServiceCreateBankAccountProcedure, - connect.WithSchema(portfolioServiceCreateBankAccountMethodDescriptor), - connect.WithClientOptions(opts...), - ), - updateBankAccount: connect.NewClient[gen.UpdateBankAccountRequest, gen.BankAccount]( - httpClient, - baseURL+PortfolioServiceUpdateBankAccountProcedure, - connect.WithSchema(portfolioServiceUpdateBankAccountMethodDescriptor), - connect.WithClientOptions(opts...), - ), - deleteBankAccount: connect.NewClient[gen.DeleteBankAccountRequest, emptypb.Empty]( - httpClient, - baseURL+PortfolioServiceDeleteBankAccountProcedure, - connect.WithSchema(portfolioServiceDeleteBankAccountMethodDescriptor), - connect.WithClientOptions(opts...), - ), - } -} - -// portfolioServiceClient implements PortfolioServiceClient. -type portfolioServiceClient struct { - createPortfolio *connect.Client[gen.CreatePortfolioRequest, gen.Portfolio] - listPortfolios *connect.Client[gen.ListPortfoliosRequest, gen.ListPortfoliosResponse] - getPortfolio *connect.Client[gen.GetPortfolioRequest, gen.Portfolio] - updatePortfolio *connect.Client[gen.UpdatePortfolioRequest, gen.Portfolio] - deletePortfolio *connect.Client[gen.DeletePortfolioRequest, emptypb.Empty] - getPortfolioSnapshot *connect.Client[gen.GetPortfolioSnapshotRequest, gen.PortfolioSnapshot] - createPortfolioTransaction *connect.Client[gen.CreatePortfolioTransactionRequest, gen.PortfolioEvent] - getPortfolioTransaction *connect.Client[gen.GetPortfolioTransactionRequest, gen.PortfolioEvent] - listPortfolioTransactions *connect.Client[gen.ListPortfolioTransactionsRequest, gen.ListPortfolioTransactionsResponse] - updatePortfolioTransaction *connect.Client[gen.UpdatePortfolioTransactionRequest, gen.PortfolioEvent] - deletePortfolioTransaction *connect.Client[gen.DeletePortfolioTransactionRequest, emptypb.Empty] - importTransactions *connect.Client[gen.ImportTransactionsRequest, emptypb.Empty] - createBankAccount *connect.Client[gen.CreateBankAccountRequest, gen.BankAccount] - updateBankAccount *connect.Client[gen.UpdateBankAccountRequest, gen.BankAccount] - deleteBankAccount *connect.Client[gen.DeleteBankAccountRequest, emptypb.Empty] -} - -// CreatePortfolio calls mgo.portfolio.v1.PortfolioService.CreatePortfolio. -func (c *portfolioServiceClient) CreatePortfolio(ctx context.Context, req *connect.Request[gen.CreatePortfolioRequest]) (*connect.Response[gen.Portfolio], error) { - return c.createPortfolio.CallUnary(ctx, req) -} - -// ListPortfolios calls mgo.portfolio.v1.PortfolioService.ListPortfolios. -func (c *portfolioServiceClient) ListPortfolios(ctx context.Context, req *connect.Request[gen.ListPortfoliosRequest]) (*connect.Response[gen.ListPortfoliosResponse], error) { - return c.listPortfolios.CallUnary(ctx, req) -} - -// GetPortfolio calls mgo.portfolio.v1.PortfolioService.GetPortfolio. -func (c *portfolioServiceClient) GetPortfolio(ctx context.Context, req *connect.Request[gen.GetPortfolioRequest]) (*connect.Response[gen.Portfolio], error) { - return c.getPortfolio.CallUnary(ctx, req) -} - -// UpdatePortfolio calls mgo.portfolio.v1.PortfolioService.UpdatePortfolio. -func (c *portfolioServiceClient) UpdatePortfolio(ctx context.Context, req *connect.Request[gen.UpdatePortfolioRequest]) (*connect.Response[gen.Portfolio], error) { - return c.updatePortfolio.CallUnary(ctx, req) -} - -// DeletePortfolio calls mgo.portfolio.v1.PortfolioService.DeletePortfolio. -func (c *portfolioServiceClient) DeletePortfolio(ctx context.Context, req *connect.Request[gen.DeletePortfolioRequest]) (*connect.Response[emptypb.Empty], error) { - return c.deletePortfolio.CallUnary(ctx, req) -} - -// GetPortfolioSnapshot calls mgo.portfolio.v1.PortfolioService.GetPortfolioSnapshot. -func (c *portfolioServiceClient) GetPortfolioSnapshot(ctx context.Context, req *connect.Request[gen.GetPortfolioSnapshotRequest]) (*connect.Response[gen.PortfolioSnapshot], error) { - return c.getPortfolioSnapshot.CallUnary(ctx, req) -} - -// CreatePortfolioTransaction calls mgo.portfolio.v1.PortfolioService.CreatePortfolioTransaction. -func (c *portfolioServiceClient) CreatePortfolioTransaction(ctx context.Context, req *connect.Request[gen.CreatePortfolioTransactionRequest]) (*connect.Response[gen.PortfolioEvent], error) { - return c.createPortfolioTransaction.CallUnary(ctx, req) -} - -// GetPortfolioTransaction calls mgo.portfolio.v1.PortfolioService.GetPortfolioTransaction. -func (c *portfolioServiceClient) GetPortfolioTransaction(ctx context.Context, req *connect.Request[gen.GetPortfolioTransactionRequest]) (*connect.Response[gen.PortfolioEvent], error) { - return c.getPortfolioTransaction.CallUnary(ctx, req) -} - -// ListPortfolioTransactions calls mgo.portfolio.v1.PortfolioService.ListPortfolioTransactions. -func (c *portfolioServiceClient) ListPortfolioTransactions(ctx context.Context, req *connect.Request[gen.ListPortfolioTransactionsRequest]) (*connect.Response[gen.ListPortfolioTransactionsResponse], error) { - return c.listPortfolioTransactions.CallUnary(ctx, req) -} - -// UpdatePortfolioTransaction calls mgo.portfolio.v1.PortfolioService.UpdatePortfolioTransaction. -func (c *portfolioServiceClient) UpdatePortfolioTransaction(ctx context.Context, req *connect.Request[gen.UpdatePortfolioTransactionRequest]) (*connect.Response[gen.PortfolioEvent], error) { - return c.updatePortfolioTransaction.CallUnary(ctx, req) -} - -// DeletePortfolioTransaction calls mgo.portfolio.v1.PortfolioService.DeletePortfolioTransaction. -func (c *portfolioServiceClient) DeletePortfolioTransaction(ctx context.Context, req *connect.Request[gen.DeletePortfolioTransactionRequest]) (*connect.Response[emptypb.Empty], error) { - return c.deletePortfolioTransaction.CallUnary(ctx, req) -} - -// ImportTransactions calls mgo.portfolio.v1.PortfolioService.ImportTransactions. -func (c *portfolioServiceClient) ImportTransactions(ctx context.Context, req *connect.Request[gen.ImportTransactionsRequest]) (*connect.Response[emptypb.Empty], error) { - return c.importTransactions.CallUnary(ctx, req) -} - -// CreateBankAccount calls mgo.portfolio.v1.PortfolioService.CreateBankAccount. -func (c *portfolioServiceClient) CreateBankAccount(ctx context.Context, req *connect.Request[gen.CreateBankAccountRequest]) (*connect.Response[gen.BankAccount], error) { - return c.createBankAccount.CallUnary(ctx, req) -} - -// UpdateBankAccount calls mgo.portfolio.v1.PortfolioService.UpdateBankAccount. -func (c *portfolioServiceClient) UpdateBankAccount(ctx context.Context, req *connect.Request[gen.UpdateBankAccountRequest]) (*connect.Response[gen.BankAccount], error) { - return c.updateBankAccount.CallUnary(ctx, req) -} - -// DeleteBankAccount calls mgo.portfolio.v1.PortfolioService.DeleteBankAccount. -func (c *portfolioServiceClient) DeleteBankAccount(ctx context.Context, req *connect.Request[gen.DeleteBankAccountRequest]) (*connect.Response[emptypb.Empty], error) { - return c.deleteBankAccount.CallUnary(ctx, req) -} - -// PortfolioServiceHandler is an implementation of the mgo.portfolio.v1.PortfolioService service. -type PortfolioServiceHandler interface { - CreatePortfolio(context.Context, *connect.Request[gen.CreatePortfolioRequest]) (*connect.Response[gen.Portfolio], error) - ListPortfolios(context.Context, *connect.Request[gen.ListPortfoliosRequest]) (*connect.Response[gen.ListPortfoliosResponse], error) - GetPortfolio(context.Context, *connect.Request[gen.GetPortfolioRequest]) (*connect.Response[gen.Portfolio], error) - UpdatePortfolio(context.Context, *connect.Request[gen.UpdatePortfolioRequest]) (*connect.Response[gen.Portfolio], error) - DeletePortfolio(context.Context, *connect.Request[gen.DeletePortfolioRequest]) (*connect.Response[emptypb.Empty], error) - GetPortfolioSnapshot(context.Context, *connect.Request[gen.GetPortfolioSnapshotRequest]) (*connect.Response[gen.PortfolioSnapshot], error) - CreatePortfolioTransaction(context.Context, *connect.Request[gen.CreatePortfolioTransactionRequest]) (*connect.Response[gen.PortfolioEvent], error) - GetPortfolioTransaction(context.Context, *connect.Request[gen.GetPortfolioTransactionRequest]) (*connect.Response[gen.PortfolioEvent], error) - ListPortfolioTransactions(context.Context, *connect.Request[gen.ListPortfolioTransactionsRequest]) (*connect.Response[gen.ListPortfolioTransactionsResponse], error) - UpdatePortfolioTransaction(context.Context, *connect.Request[gen.UpdatePortfolioTransactionRequest]) (*connect.Response[gen.PortfolioEvent], error) - DeletePortfolioTransaction(context.Context, *connect.Request[gen.DeletePortfolioTransactionRequest]) (*connect.Response[emptypb.Empty], error) - ImportTransactions(context.Context, *connect.Request[gen.ImportTransactionsRequest]) (*connect.Response[emptypb.Empty], error) - CreateBankAccount(context.Context, *connect.Request[gen.CreateBankAccountRequest]) (*connect.Response[gen.BankAccount], error) - UpdateBankAccount(context.Context, *connect.Request[gen.UpdateBankAccountRequest]) (*connect.Response[gen.BankAccount], error) - DeleteBankAccount(context.Context, *connect.Request[gen.DeleteBankAccountRequest]) (*connect.Response[emptypb.Empty], error) -} - -// NewPortfolioServiceHandler builds an HTTP handler from the service implementation. It returns the -// path on which to mount the handler and the handler itself. -// -// By default, handlers support the Connect, gRPC, and gRPC-Web protocols with the binary Protobuf -// and JSON codecs. They also support gzip compression. -func NewPortfolioServiceHandler(svc PortfolioServiceHandler, opts ...connect.HandlerOption) (string, http.Handler) { - portfolioServiceCreatePortfolioHandler := connect.NewUnaryHandler( - PortfolioServiceCreatePortfolioProcedure, - svc.CreatePortfolio, - connect.WithSchema(portfolioServiceCreatePortfolioMethodDescriptor), - connect.WithHandlerOptions(opts...), - ) - portfolioServiceListPortfoliosHandler := connect.NewUnaryHandler( - PortfolioServiceListPortfoliosProcedure, - svc.ListPortfolios, - connect.WithSchema(portfolioServiceListPortfoliosMethodDescriptor), - connect.WithIdempotency(connect.IdempotencyNoSideEffects), - connect.WithHandlerOptions(opts...), - ) - portfolioServiceGetPortfolioHandler := connect.NewUnaryHandler( - PortfolioServiceGetPortfolioProcedure, - svc.GetPortfolio, - connect.WithSchema(portfolioServiceGetPortfolioMethodDescriptor), - connect.WithIdempotency(connect.IdempotencyNoSideEffects), - connect.WithHandlerOptions(opts...), - ) - portfolioServiceUpdatePortfolioHandler := connect.NewUnaryHandler( - PortfolioServiceUpdatePortfolioProcedure, - svc.UpdatePortfolio, - connect.WithSchema(portfolioServiceUpdatePortfolioMethodDescriptor), - connect.WithHandlerOptions(opts...), - ) - portfolioServiceDeletePortfolioHandler := connect.NewUnaryHandler( - PortfolioServiceDeletePortfolioProcedure, - svc.DeletePortfolio, - connect.WithSchema(portfolioServiceDeletePortfolioMethodDescriptor), - connect.WithHandlerOptions(opts...), - ) - portfolioServiceGetPortfolioSnapshotHandler := connect.NewUnaryHandler( - PortfolioServiceGetPortfolioSnapshotProcedure, - svc.GetPortfolioSnapshot, - connect.WithSchema(portfolioServiceGetPortfolioSnapshotMethodDescriptor), - connect.WithIdempotency(connect.IdempotencyNoSideEffects), - connect.WithHandlerOptions(opts...), - ) - portfolioServiceCreatePortfolioTransactionHandler := connect.NewUnaryHandler( - PortfolioServiceCreatePortfolioTransactionProcedure, - svc.CreatePortfolioTransaction, - connect.WithSchema(portfolioServiceCreatePortfolioTransactionMethodDescriptor), - connect.WithHandlerOptions(opts...), - ) - portfolioServiceGetPortfolioTransactionHandler := connect.NewUnaryHandler( - PortfolioServiceGetPortfolioTransactionProcedure, - svc.GetPortfolioTransaction, - connect.WithSchema(portfolioServiceGetPortfolioTransactionMethodDescriptor), - connect.WithIdempotency(connect.IdempotencyNoSideEffects), - connect.WithHandlerOptions(opts...), - ) - portfolioServiceListPortfolioTransactionsHandler := connect.NewUnaryHandler( - PortfolioServiceListPortfolioTransactionsProcedure, - svc.ListPortfolioTransactions, - connect.WithSchema(portfolioServiceListPortfolioTransactionsMethodDescriptor), - connect.WithIdempotency(connect.IdempotencyNoSideEffects), - connect.WithHandlerOptions(opts...), - ) - portfolioServiceUpdatePortfolioTransactionHandler := connect.NewUnaryHandler( - PortfolioServiceUpdatePortfolioTransactionProcedure, - svc.UpdatePortfolioTransaction, - connect.WithSchema(portfolioServiceUpdatePortfolioTransactionMethodDescriptor), - connect.WithHandlerOptions(opts...), - ) - portfolioServiceDeletePortfolioTransactionHandler := connect.NewUnaryHandler( - PortfolioServiceDeletePortfolioTransactionProcedure, - svc.DeletePortfolioTransaction, - connect.WithSchema(portfolioServiceDeletePortfolioTransactionMethodDescriptor), - connect.WithHandlerOptions(opts...), - ) - portfolioServiceImportTransactionsHandler := connect.NewUnaryHandler( - PortfolioServiceImportTransactionsProcedure, - svc.ImportTransactions, - connect.WithSchema(portfolioServiceImportTransactionsMethodDescriptor), - connect.WithHandlerOptions(opts...), - ) - portfolioServiceCreateBankAccountHandler := connect.NewUnaryHandler( - PortfolioServiceCreateBankAccountProcedure, - svc.CreateBankAccount, - connect.WithSchema(portfolioServiceCreateBankAccountMethodDescriptor), - connect.WithHandlerOptions(opts...), - ) - portfolioServiceUpdateBankAccountHandler := connect.NewUnaryHandler( - PortfolioServiceUpdateBankAccountProcedure, - svc.UpdateBankAccount, - connect.WithSchema(portfolioServiceUpdateBankAccountMethodDescriptor), - connect.WithHandlerOptions(opts...), - ) - portfolioServiceDeleteBankAccountHandler := connect.NewUnaryHandler( - PortfolioServiceDeleteBankAccountProcedure, - svc.DeleteBankAccount, - connect.WithSchema(portfolioServiceDeleteBankAccountMethodDescriptor), - connect.WithHandlerOptions(opts...), - ) - return "/mgo.portfolio.v1.PortfolioService/", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - switch r.URL.Path { - case PortfolioServiceCreatePortfolioProcedure: - portfolioServiceCreatePortfolioHandler.ServeHTTP(w, r) - case PortfolioServiceListPortfoliosProcedure: - portfolioServiceListPortfoliosHandler.ServeHTTP(w, r) - case PortfolioServiceGetPortfolioProcedure: - portfolioServiceGetPortfolioHandler.ServeHTTP(w, r) - case PortfolioServiceUpdatePortfolioProcedure: - portfolioServiceUpdatePortfolioHandler.ServeHTTP(w, r) - case PortfolioServiceDeletePortfolioProcedure: - portfolioServiceDeletePortfolioHandler.ServeHTTP(w, r) - case PortfolioServiceGetPortfolioSnapshotProcedure: - portfolioServiceGetPortfolioSnapshotHandler.ServeHTTP(w, r) - case PortfolioServiceCreatePortfolioTransactionProcedure: - portfolioServiceCreatePortfolioTransactionHandler.ServeHTTP(w, r) - case PortfolioServiceGetPortfolioTransactionProcedure: - portfolioServiceGetPortfolioTransactionHandler.ServeHTTP(w, r) - case PortfolioServiceListPortfolioTransactionsProcedure: - portfolioServiceListPortfolioTransactionsHandler.ServeHTTP(w, r) - case PortfolioServiceUpdatePortfolioTransactionProcedure: - portfolioServiceUpdatePortfolioTransactionHandler.ServeHTTP(w, r) - case PortfolioServiceDeletePortfolioTransactionProcedure: - portfolioServiceDeletePortfolioTransactionHandler.ServeHTTP(w, r) - case PortfolioServiceImportTransactionsProcedure: - portfolioServiceImportTransactionsHandler.ServeHTTP(w, r) - case PortfolioServiceCreateBankAccountProcedure: - portfolioServiceCreateBankAccountHandler.ServeHTTP(w, r) - case PortfolioServiceUpdateBankAccountProcedure: - portfolioServiceUpdateBankAccountHandler.ServeHTTP(w, r) - case PortfolioServiceDeleteBankAccountProcedure: - portfolioServiceDeleteBankAccountHandler.ServeHTTP(w, r) - default: - http.NotFound(w, r) - } - }) -} - -// UnimplementedPortfolioServiceHandler returns CodeUnimplemented from all methods. -type UnimplementedPortfolioServiceHandler struct{} - -func (UnimplementedPortfolioServiceHandler) CreatePortfolio(context.Context, *connect.Request[gen.CreatePortfolioRequest]) (*connect.Response[gen.Portfolio], error) { - return nil, connect.NewError(connect.CodeUnimplemented, errors.New("mgo.portfolio.v1.PortfolioService.CreatePortfolio is not implemented")) -} - -func (UnimplementedPortfolioServiceHandler) ListPortfolios(context.Context, *connect.Request[gen.ListPortfoliosRequest]) (*connect.Response[gen.ListPortfoliosResponse], error) { - return nil, connect.NewError(connect.CodeUnimplemented, errors.New("mgo.portfolio.v1.PortfolioService.ListPortfolios is not implemented")) -} - -func (UnimplementedPortfolioServiceHandler) GetPortfolio(context.Context, *connect.Request[gen.GetPortfolioRequest]) (*connect.Response[gen.Portfolio], error) { - return nil, connect.NewError(connect.CodeUnimplemented, errors.New("mgo.portfolio.v1.PortfolioService.GetPortfolio is not implemented")) -} - -func (UnimplementedPortfolioServiceHandler) UpdatePortfolio(context.Context, *connect.Request[gen.UpdatePortfolioRequest]) (*connect.Response[gen.Portfolio], error) { - return nil, connect.NewError(connect.CodeUnimplemented, errors.New("mgo.portfolio.v1.PortfolioService.UpdatePortfolio is not implemented")) -} - -func (UnimplementedPortfolioServiceHandler) DeletePortfolio(context.Context, *connect.Request[gen.DeletePortfolioRequest]) (*connect.Response[emptypb.Empty], error) { - return nil, connect.NewError(connect.CodeUnimplemented, errors.New("mgo.portfolio.v1.PortfolioService.DeletePortfolio is not implemented")) -} - -func (UnimplementedPortfolioServiceHandler) GetPortfolioSnapshot(context.Context, *connect.Request[gen.GetPortfolioSnapshotRequest]) (*connect.Response[gen.PortfolioSnapshot], error) { - return nil, connect.NewError(connect.CodeUnimplemented, errors.New("mgo.portfolio.v1.PortfolioService.GetPortfolioSnapshot is not implemented")) -} - -func (UnimplementedPortfolioServiceHandler) CreatePortfolioTransaction(context.Context, *connect.Request[gen.CreatePortfolioTransactionRequest]) (*connect.Response[gen.PortfolioEvent], error) { - return nil, connect.NewError(connect.CodeUnimplemented, errors.New("mgo.portfolio.v1.PortfolioService.CreatePortfolioTransaction is not implemented")) -} - -func (UnimplementedPortfolioServiceHandler) GetPortfolioTransaction(context.Context, *connect.Request[gen.GetPortfolioTransactionRequest]) (*connect.Response[gen.PortfolioEvent], error) { - return nil, connect.NewError(connect.CodeUnimplemented, errors.New("mgo.portfolio.v1.PortfolioService.GetPortfolioTransaction is not implemented")) -} - -func (UnimplementedPortfolioServiceHandler) ListPortfolioTransactions(context.Context, *connect.Request[gen.ListPortfolioTransactionsRequest]) (*connect.Response[gen.ListPortfolioTransactionsResponse], error) { - return nil, connect.NewError(connect.CodeUnimplemented, errors.New("mgo.portfolio.v1.PortfolioService.ListPortfolioTransactions is not implemented")) -} - -func (UnimplementedPortfolioServiceHandler) UpdatePortfolioTransaction(context.Context, *connect.Request[gen.UpdatePortfolioTransactionRequest]) (*connect.Response[gen.PortfolioEvent], error) { - return nil, connect.NewError(connect.CodeUnimplemented, errors.New("mgo.portfolio.v1.PortfolioService.UpdatePortfolioTransaction is not implemented")) -} - -func (UnimplementedPortfolioServiceHandler) DeletePortfolioTransaction(context.Context, *connect.Request[gen.DeletePortfolioTransactionRequest]) (*connect.Response[emptypb.Empty], error) { - return nil, connect.NewError(connect.CodeUnimplemented, errors.New("mgo.portfolio.v1.PortfolioService.DeletePortfolioTransaction is not implemented")) -} - -func (UnimplementedPortfolioServiceHandler) ImportTransactions(context.Context, *connect.Request[gen.ImportTransactionsRequest]) (*connect.Response[emptypb.Empty], error) { - return nil, connect.NewError(connect.CodeUnimplemented, errors.New("mgo.portfolio.v1.PortfolioService.ImportTransactions is not implemented")) -} - -func (UnimplementedPortfolioServiceHandler) CreateBankAccount(context.Context, *connect.Request[gen.CreateBankAccountRequest]) (*connect.Response[gen.BankAccount], error) { - return nil, connect.NewError(connect.CodeUnimplemented, errors.New("mgo.portfolio.v1.PortfolioService.CreateBankAccount is not implemented")) -} - -func (UnimplementedPortfolioServiceHandler) UpdateBankAccount(context.Context, *connect.Request[gen.UpdateBankAccountRequest]) (*connect.Response[gen.BankAccount], error) { - return nil, connect.NewError(connect.CodeUnimplemented, errors.New("mgo.portfolio.v1.PortfolioService.UpdateBankAccount is not implemented")) -} - -func (UnimplementedPortfolioServiceHandler) DeleteBankAccount(context.Context, *connect.Request[gen.DeleteBankAccountRequest]) (*connect.Response[emptypb.Empty], error) { - return nil, connect.NewError(connect.CodeUnimplemented, errors.New("mgo.portfolio.v1.PortfolioService.DeleteBankAccount is not implemented")) -} - -// SecuritiesServiceClient is a client for the mgo.portfolio.v1.SecuritiesService service. -type SecuritiesServiceClient interface { - ListSecurities(context.Context, *connect.Request[gen.ListSecuritiesRequest]) (*connect.Response[gen.ListSecuritiesResponse], error) - GetSecurity(context.Context, *connect.Request[gen.GetSecurityRequest]) (*connect.Response[gen.Security], error) - CreateSecurity(context.Context, *connect.Request[gen.CreateSecurityRequest]) (*connect.Response[gen.Security], error) - UpdateSecurity(context.Context, *connect.Request[gen.UpdateSecurityRequest]) (*connect.Response[gen.Security], error) - DeleteSecurity(context.Context, *connect.Request[gen.DeleteSecurityRequest]) (*connect.Response[emptypb.Empty], error) - TriggerSecurityQuoteUpdate(context.Context, *connect.Request[gen.TriggerQuoteUpdateRequest]) (*connect.Response[gen.TriggerQuoteUpdateResponse], error) -} - -// NewSecuritiesServiceClient constructs a client for the mgo.portfolio.v1.SecuritiesService -// service. By default, it uses the Connect protocol with the binary Protobuf Codec, asks for -// gzipped responses, and sends uncompressed requests. To use the gRPC or gRPC-Web protocols, supply -// the connect.WithGRPC() or connect.WithGRPCWeb() options. -// -// The URL supplied here should be the base URL for the Connect or gRPC server (for example, -// http://api.acme.com or https://acme.com/grpc). -func NewSecuritiesServiceClient(httpClient connect.HTTPClient, baseURL string, opts ...connect.ClientOption) SecuritiesServiceClient { - baseURL = strings.TrimRight(baseURL, "/") - return &securitiesServiceClient{ - listSecurities: connect.NewClient[gen.ListSecuritiesRequest, gen.ListSecuritiesResponse]( - httpClient, - baseURL+SecuritiesServiceListSecuritiesProcedure, - connect.WithSchema(securitiesServiceListSecuritiesMethodDescriptor), - connect.WithIdempotency(connect.IdempotencyNoSideEffects), - connect.WithClientOptions(opts...), - ), - getSecurity: connect.NewClient[gen.GetSecurityRequest, gen.Security]( - httpClient, - baseURL+SecuritiesServiceGetSecurityProcedure, - connect.WithSchema(securitiesServiceGetSecurityMethodDescriptor), - connect.WithIdempotency(connect.IdempotencyNoSideEffects), - connect.WithClientOptions(opts...), - ), - createSecurity: connect.NewClient[gen.CreateSecurityRequest, gen.Security]( - httpClient, - baseURL+SecuritiesServiceCreateSecurityProcedure, - connect.WithSchema(securitiesServiceCreateSecurityMethodDescriptor), - connect.WithClientOptions(opts...), - ), - updateSecurity: connect.NewClient[gen.UpdateSecurityRequest, gen.Security]( - httpClient, - baseURL+SecuritiesServiceUpdateSecurityProcedure, - connect.WithSchema(securitiesServiceUpdateSecurityMethodDescriptor), - connect.WithClientOptions(opts...), - ), - deleteSecurity: connect.NewClient[gen.DeleteSecurityRequest, emptypb.Empty]( - httpClient, - baseURL+SecuritiesServiceDeleteSecurityProcedure, - connect.WithSchema(securitiesServiceDeleteSecurityMethodDescriptor), - connect.WithClientOptions(opts...), - ), - triggerSecurityQuoteUpdate: connect.NewClient[gen.TriggerQuoteUpdateRequest, gen.TriggerQuoteUpdateResponse]( - httpClient, - baseURL+SecuritiesServiceTriggerSecurityQuoteUpdateProcedure, - connect.WithSchema(securitiesServiceTriggerSecurityQuoteUpdateMethodDescriptor), - connect.WithClientOptions(opts...), - ), - } -} - -// securitiesServiceClient implements SecuritiesServiceClient. -type securitiesServiceClient struct { - listSecurities *connect.Client[gen.ListSecuritiesRequest, gen.ListSecuritiesResponse] - getSecurity *connect.Client[gen.GetSecurityRequest, gen.Security] - createSecurity *connect.Client[gen.CreateSecurityRequest, gen.Security] - updateSecurity *connect.Client[gen.UpdateSecurityRequest, gen.Security] - deleteSecurity *connect.Client[gen.DeleteSecurityRequest, emptypb.Empty] - triggerSecurityQuoteUpdate *connect.Client[gen.TriggerQuoteUpdateRequest, gen.TriggerQuoteUpdateResponse] -} - -// ListSecurities calls mgo.portfolio.v1.SecuritiesService.ListSecurities. -func (c *securitiesServiceClient) ListSecurities(ctx context.Context, req *connect.Request[gen.ListSecuritiesRequest]) (*connect.Response[gen.ListSecuritiesResponse], error) { - return c.listSecurities.CallUnary(ctx, req) -} - -// GetSecurity calls mgo.portfolio.v1.SecuritiesService.GetSecurity. -func (c *securitiesServiceClient) GetSecurity(ctx context.Context, req *connect.Request[gen.GetSecurityRequest]) (*connect.Response[gen.Security], error) { - return c.getSecurity.CallUnary(ctx, req) -} - -// CreateSecurity calls mgo.portfolio.v1.SecuritiesService.CreateSecurity. -func (c *securitiesServiceClient) CreateSecurity(ctx context.Context, req *connect.Request[gen.CreateSecurityRequest]) (*connect.Response[gen.Security], error) { - return c.createSecurity.CallUnary(ctx, req) -} - -// UpdateSecurity calls mgo.portfolio.v1.SecuritiesService.UpdateSecurity. -func (c *securitiesServiceClient) UpdateSecurity(ctx context.Context, req *connect.Request[gen.UpdateSecurityRequest]) (*connect.Response[gen.Security], error) { - return c.updateSecurity.CallUnary(ctx, req) -} - -// DeleteSecurity calls mgo.portfolio.v1.SecuritiesService.DeleteSecurity. -func (c *securitiesServiceClient) DeleteSecurity(ctx context.Context, req *connect.Request[gen.DeleteSecurityRequest]) (*connect.Response[emptypb.Empty], error) { - return c.deleteSecurity.CallUnary(ctx, req) -} - -// TriggerSecurityQuoteUpdate calls mgo.portfolio.v1.SecuritiesService.TriggerSecurityQuoteUpdate. -func (c *securitiesServiceClient) TriggerSecurityQuoteUpdate(ctx context.Context, req *connect.Request[gen.TriggerQuoteUpdateRequest]) (*connect.Response[gen.TriggerQuoteUpdateResponse], error) { - return c.triggerSecurityQuoteUpdate.CallUnary(ctx, req) -} - -// SecuritiesServiceHandler is an implementation of the mgo.portfolio.v1.SecuritiesService service. -type SecuritiesServiceHandler interface { - ListSecurities(context.Context, *connect.Request[gen.ListSecuritiesRequest]) (*connect.Response[gen.ListSecuritiesResponse], error) - GetSecurity(context.Context, *connect.Request[gen.GetSecurityRequest]) (*connect.Response[gen.Security], error) - CreateSecurity(context.Context, *connect.Request[gen.CreateSecurityRequest]) (*connect.Response[gen.Security], error) - UpdateSecurity(context.Context, *connect.Request[gen.UpdateSecurityRequest]) (*connect.Response[gen.Security], error) - DeleteSecurity(context.Context, *connect.Request[gen.DeleteSecurityRequest]) (*connect.Response[emptypb.Empty], error) - TriggerSecurityQuoteUpdate(context.Context, *connect.Request[gen.TriggerQuoteUpdateRequest]) (*connect.Response[gen.TriggerQuoteUpdateResponse], error) -} - -// NewSecuritiesServiceHandler builds an HTTP handler from the service implementation. It returns -// the path on which to mount the handler and the handler itself. -// -// By default, handlers support the Connect, gRPC, and gRPC-Web protocols with the binary Protobuf -// and JSON codecs. They also support gzip compression. -func NewSecuritiesServiceHandler(svc SecuritiesServiceHandler, opts ...connect.HandlerOption) (string, http.Handler) { - securitiesServiceListSecuritiesHandler := connect.NewUnaryHandler( - SecuritiesServiceListSecuritiesProcedure, - svc.ListSecurities, - connect.WithSchema(securitiesServiceListSecuritiesMethodDescriptor), - connect.WithIdempotency(connect.IdempotencyNoSideEffects), - connect.WithHandlerOptions(opts...), - ) - securitiesServiceGetSecurityHandler := connect.NewUnaryHandler( - SecuritiesServiceGetSecurityProcedure, - svc.GetSecurity, - connect.WithSchema(securitiesServiceGetSecurityMethodDescriptor), - connect.WithIdempotency(connect.IdempotencyNoSideEffects), - connect.WithHandlerOptions(opts...), - ) - securitiesServiceCreateSecurityHandler := connect.NewUnaryHandler( - SecuritiesServiceCreateSecurityProcedure, - svc.CreateSecurity, - connect.WithSchema(securitiesServiceCreateSecurityMethodDescriptor), - connect.WithHandlerOptions(opts...), - ) - securitiesServiceUpdateSecurityHandler := connect.NewUnaryHandler( - SecuritiesServiceUpdateSecurityProcedure, - svc.UpdateSecurity, - connect.WithSchema(securitiesServiceUpdateSecurityMethodDescriptor), - connect.WithHandlerOptions(opts...), - ) - securitiesServiceDeleteSecurityHandler := connect.NewUnaryHandler( - SecuritiesServiceDeleteSecurityProcedure, - svc.DeleteSecurity, - connect.WithSchema(securitiesServiceDeleteSecurityMethodDescriptor), - connect.WithHandlerOptions(opts...), - ) - securitiesServiceTriggerSecurityQuoteUpdateHandler := connect.NewUnaryHandler( - SecuritiesServiceTriggerSecurityQuoteUpdateProcedure, - svc.TriggerSecurityQuoteUpdate, - connect.WithSchema(securitiesServiceTriggerSecurityQuoteUpdateMethodDescriptor), - connect.WithHandlerOptions(opts...), - ) - return "/mgo.portfolio.v1.SecuritiesService/", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - switch r.URL.Path { - case SecuritiesServiceListSecuritiesProcedure: - securitiesServiceListSecuritiesHandler.ServeHTTP(w, r) - case SecuritiesServiceGetSecurityProcedure: - securitiesServiceGetSecurityHandler.ServeHTTP(w, r) - case SecuritiesServiceCreateSecurityProcedure: - securitiesServiceCreateSecurityHandler.ServeHTTP(w, r) - case SecuritiesServiceUpdateSecurityProcedure: - securitiesServiceUpdateSecurityHandler.ServeHTTP(w, r) - case SecuritiesServiceDeleteSecurityProcedure: - securitiesServiceDeleteSecurityHandler.ServeHTTP(w, r) - case SecuritiesServiceTriggerSecurityQuoteUpdateProcedure: - securitiesServiceTriggerSecurityQuoteUpdateHandler.ServeHTTP(w, r) - default: - http.NotFound(w, r) - } - }) -} - -// UnimplementedSecuritiesServiceHandler returns CodeUnimplemented from all methods. -type UnimplementedSecuritiesServiceHandler struct{} - -func (UnimplementedSecuritiesServiceHandler) ListSecurities(context.Context, *connect.Request[gen.ListSecuritiesRequest]) (*connect.Response[gen.ListSecuritiesResponse], error) { - return nil, connect.NewError(connect.CodeUnimplemented, errors.New("mgo.portfolio.v1.SecuritiesService.ListSecurities is not implemented")) -} - -func (UnimplementedSecuritiesServiceHandler) GetSecurity(context.Context, *connect.Request[gen.GetSecurityRequest]) (*connect.Response[gen.Security], error) { - return nil, connect.NewError(connect.CodeUnimplemented, errors.New("mgo.portfolio.v1.SecuritiesService.GetSecurity is not implemented")) -} - -func (UnimplementedSecuritiesServiceHandler) CreateSecurity(context.Context, *connect.Request[gen.CreateSecurityRequest]) (*connect.Response[gen.Security], error) { - return nil, connect.NewError(connect.CodeUnimplemented, errors.New("mgo.portfolio.v1.SecuritiesService.CreateSecurity is not implemented")) -} - -func (UnimplementedSecuritiesServiceHandler) UpdateSecurity(context.Context, *connect.Request[gen.UpdateSecurityRequest]) (*connect.Response[gen.Security], error) { - return nil, connect.NewError(connect.CodeUnimplemented, errors.New("mgo.portfolio.v1.SecuritiesService.UpdateSecurity is not implemented")) -} - -func (UnimplementedSecuritiesServiceHandler) DeleteSecurity(context.Context, *connect.Request[gen.DeleteSecurityRequest]) (*connect.Response[emptypb.Empty], error) { - return nil, connect.NewError(connect.CodeUnimplemented, errors.New("mgo.portfolio.v1.SecuritiesService.DeleteSecurity is not implemented")) -} - -func (UnimplementedSecuritiesServiceHandler) TriggerSecurityQuoteUpdate(context.Context, *connect.Request[gen.TriggerQuoteUpdateRequest]) (*connect.Response[gen.TriggerQuoteUpdateResponse], error) { - return nil, connect.NewError(connect.CodeUnimplemented, errors.New("mgo.portfolio.v1.SecuritiesService.TriggerSecurityQuoteUpdate is not implemented")) -} diff --git a/gen/securities_sql.go b/gen/securities_sql.go deleted file mode 100644 index 49359fa0..00000000 --- a/gen/securities_sql.go +++ /dev/null @@ -1,223 +0,0 @@ -// Copyright 2023 Christian Banse -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. -// -// This file is part of The Money Gopher. - -package portfoliov1 - -import ( - "database/sql" - "strings" - "time" - - "github.com/oxisto/money-gopher/persistence" - - "google.golang.org/protobuf/types/known/timestamppb" -) - -var _ persistence.StorageObject = &Security{} - -func (*Security) InitTables(db *persistence.DB) (err error) { - _, err = db.Exec(`CREATE TABLE IF NOT EXISTS securities ( -id TEXT PRIMARY KEY, -display_name TEXT NOT NULL, -quote_provider TEXT -);`) - if err != nil { - return err - } - - return -} - -func (*ListedSecurity) InitTables(db *persistence.DB) (err error) { - _, err = db.Exec(`CREATE TABLE IF NOT EXISTS listed_securities ( -security_id TEXT, -ticker TEXT NOT NULL, -currency TEXT NOT NULL, -latest_quote INTEGER, -latest_quote_timestamp DATETIME, -PRIMARY KEY (security_id, ticker) -);`) - if err != nil { - return err - } - - return -} - -func (*Security) PrepareReplace(db *persistence.DB) (stmt *sql.Stmt, err error) { - return db.Prepare(`REPLACE INTO securities (id, display_name, quote_provider) VALUES (?,?,?);`) -} - -func (*ListedSecurity) PrepareReplace(db *persistence.DB) (stmt *sql.Stmt, err error) { - return db.Prepare(`REPLACE INTO listed_securities (security_id, ticker, currency, latest_quote, latest_quote_timestamp) VALUES (?,?,?,?,?);`) -} - -func (*Security) PrepareList(db *persistence.DB) (stmt *sql.Stmt, err error) { - return db.Prepare(`SELECT id, display_name, quote_provider FROM securities`) -} - -func (*ListedSecurity) PrepareList(db *persistence.DB) (stmt *sql.Stmt, err error) { - return db.Prepare(`SELECT security_id, ticker, currency, latest_quote, latest_quote_timestamp FROM listed_securities WHERE security_id = ?`) -} - -func (*Security) PrepareGet(db *persistence.DB) (stmt *sql.Stmt, err error) { - return db.Prepare(`SELECT id, display_name, quote_provider FROM securities WHERE id = ?`) -} - -func (*ListedSecurity) PrepareGet(db *persistence.DB) (stmt *sql.Stmt, err error) { - return db.Prepare(`SELECT * FROM listed_securities WHERE security_id = ? AND ticker = ?`) -} - -func (*Security) PrepareUpdate(db *persistence.DB, columns []string) (stmt *sql.Stmt, err error) { - // We need to make sure to quote columns here because they are potentially evil user input - var ( - query string - set []string - ) - - set = make([]string, len(columns)) - for i, col := range columns { - set[i] = persistence.Quote(col) + " = ?" - } - - query += "UPDATE securities SET " + strings.Join(set, ", ") + " WHERE id = ?;" - - return db.Prepare(query) -} - -func (*ListedSecurity) PrepareUpdate(db *persistence.DB, columns []string) (stmt *sql.Stmt, err error) { - // We need to make sure to quote columns here because they are potentially evil user input - var ( - query string - set []string - ) - - set = make([]string, len(columns)) - for i, col := range columns { - set[i] = persistence.Quote(col) + " = ?" - } - - query += "UPDATE listed_securities SET " + strings.Join(set, ", ") + " WHERE security_id = ? AND ticker = ?;" - - return db.Prepare(query) -} - -func (*Security) PrepareDelete(db *persistence.DB) (stmt *sql.Stmt, err error) { - return db.Prepare(`DELETE FROM securities WHERE id = ?`) -} - -func (*ListedSecurity) PrepareDelete(db *persistence.DB) (stmt *sql.Stmt, err error) { - return db.Prepare(`DELETE FROM listed_securities WHERE security_id = ? AND ticker = ?`) -} - -func (s *Security) ReplaceIntoArgs() []any { - return []any{s.Id, s.DisplayName, s.QuoteProvider} -} - -func (l *ListedSecurity) ReplaceIntoArgs() []any { - var ( - pt *time.Time - t time.Time - value sql.NullInt32 - ) - - if l.LatestQuoteTimestamp != nil { - t = l.LatestQuoteTimestamp.AsTime() - pt = &t - } - - if l.LatestQuote != nil { - value.Int32 = l.LatestQuote.Value - value.Valid = true - } - - return []any{l.SecurityId, l.Ticker, l.Currency, value, pt} -} - -func (s *Security) UpdateArgs(columns []string) (args []any) { - for _, col := range columns { - switch col { - case "id": - args = append(args, s.Id) - case "display_name": - args = append(args, s.DisplayName) - case "quote_provider": - args = append(args, s.QuoteProvider) - } - } - - return args -} - -func (l *ListedSecurity) UpdateArgs(columns []string) (args []any) { - for _, col := range columns { - switch col { - case "security_id": - args = append(args, l.SecurityId) - case "ticker": - args = append(args, l.Ticker) - case "currency": - args = append(args, l.LatestQuote.GetSymbol()) - case "latest_quote": - args = append(args, l.LatestQuote.GetValue()) - case "latest_quote_timestamp": - if l.LatestQuoteTimestamp != nil { - args = append(args, l.LatestQuoteTimestamp.AsTime()) - } else { - args = append(args, nil) - } - } - } - - return args -} - -func (*Security) Scan(sc persistence.Scanner) (obj persistence.StorageObject, err error) { - var ( - s Security - ) - - err = sc.Scan(&s.Id, &s.DisplayName, &s.QuoteProvider) - if err != nil { - return nil, err - } - - return &s, nil -} - -func (*ListedSecurity) Scan(sc persistence.Scanner) (obj persistence.StorageObject, err error) { - var ( - l ListedSecurity - t sql.NullTime - value sql.NullInt32 - ) - - err = sc.Scan(&l.SecurityId, &l.Ticker, &l.Currency, &value, &t) - if err != nil { - return nil, err - } - - if t.Valid { - l.LatestQuoteTimestamp = timestamppb.New(t.Time) - } - - if value.Valid { - l.LatestQuote = Value(value.Int32) - l.LatestQuote.Symbol = l.Currency - } - - return &l, nil -} diff --git a/go.mod b/go.mod index e3c41c77..9153c3e5 100644 --- a/go.mod +++ b/go.mod @@ -20,16 +20,16 @@ require ( github.com/urfave/cli/v3 v3.0.0-beta1 github.com/vektah/gqlparser/v2 v2.5.20 golang.org/x/net v0.33.0 - golang.org/x/text v0.21.0 google.golang.org/protobuf v1.36.1 ) -require github.com/google/go-cmp v0.6.0 // indirect +require golang.org/x/text v0.21.0 // indirect require ( github.com/MicahParks/jwkset v0.5.19 // indirect github.com/agnivade/levenshtein v1.2.0 // indirect github.com/go-viper/mapstructure/v2 v2.2.1 // indirect + github.com/google/go-cmp v0.6.0 // indirect github.com/google/uuid v1.6.0 // indirect github.com/mfridman/interpolate v0.0.2 // indirect github.com/sethvargo/go-retry v0.3.0 // indirect @@ -40,7 +40,7 @@ require ( golang.org/x/sync v0.10.0 // indirect golang.org/x/sys v0.28.0 // indirect golang.org/x/time v0.5.0 // indirect - google.golang.org/genproto/googleapis/api v0.0.0-20241230172942-26aa7a208def + google.golang.org/genproto/googleapis/api v0.0.0-20241230172942-26aa7a208def // indirect google.golang.org/genproto/googleapis/rpc v0.0.0-20241223144023-3abc09e42ca8 // indirect ) diff --git a/gqlgen.yml b/gqlgen.yml index a69e038a..f750b9b6 100644 --- a/gqlgen.yml +++ b/gqlgen.yml @@ -119,3 +119,5 @@ call_argument_directives_with_null: true # if they match it will use them, otherwise it will generate them. autobind: - "github.com/oxisto/money-gopher/persistence" + - "github.com/oxisto/money-gopher/portfolio/events" + - "github.com/oxisto/money-gopher/currency" \ No newline at end of file diff --git a/graph/generated.go b/graph/generated.go index a21849e4..54c38c50 100644 --- a/graph/generated.go +++ b/graph/generated.go @@ -14,8 +14,10 @@ import ( "github.com/99designs/gqlgen/graphql" "github.com/99designs/gqlgen/graphql/introspection" + "github.com/oxisto/money-gopher/currency" "github.com/oxisto/money-gopher/models" "github.com/oxisto/money-gopher/persistence" + "github.com/oxisto/money-gopher/portfolio/events" gqlparser "github.com/vektah/gqlparser/v2" "github.com/vektah/gqlparser/v2/ast" ) @@ -91,13 +93,13 @@ type ComplexityRoot struct { } PortfolioPosition struct { + Amount func(childComplexity int) int Gains func(childComplexity int) int MarketPrice func(childComplexity int) int MarketValue func(childComplexity int) int ProfitOrLoss func(childComplexity int) int PurchasePrice func(childComplexity int) int PurchaseValue func(childComplexity int) int - Quantity func(childComplexity int) int Security func(childComplexity int) int TotalFees func(childComplexity int) int } @@ -107,6 +109,11 @@ type ComplexityRoot struct { FirstTransactionTime func(childComplexity int) int Positions func(childComplexity int) int Time func(childComplexity int) int + TotalGains func(childComplexity int) int + TotalMarketValue func(childComplexity int) int + TotalPortfolioValue func(childComplexity int) int + TotalProfitOrLoss func(childComplexity int) int + TotalPurchaseValue func(childComplexity int) int } Query struct { @@ -126,7 +133,7 @@ type ComplexityRoot struct { type ListedSecurityResolver interface { Security(ctx context.Context, obj *persistence.ListedSecurity) (*persistence.Security, error) - LatestQuote(ctx context.Context, obj *persistence.ListedSecurity) (*persistence.Currency, error) + LatestQuote(ctx context.Context, obj *persistence.ListedSecurity) (*currency.Currency, error) LatestQuoteTimestamp(ctx context.Context, obj *persistence.ListedSecurity) (*string, error) } type MutationResolver interface { @@ -141,7 +148,7 @@ type PortfolioResolver interface { } type PortfolioEventResolver interface { Time(ctx context.Context, obj *persistence.PortfolioEvent) (string, error) - Type(ctx context.Context, obj *persistence.PortfolioEvent) (models.PortfolioEventType, error) + Security(ctx context.Context, obj *persistence.PortfolioEvent) (*persistence.Security, error) } type QueryResolver interface { @@ -334,6 +341,13 @@ func (e *executableSchema) Complexity(typeName, field string, childComplexity in return e.complexity.PortfolioEvent.Type(childComplexity), true + case "PortfolioPosition.amount": + if e.complexity.PortfolioPosition.Amount == nil { + break + } + + return e.complexity.PortfolioPosition.Amount(childComplexity), true + case "PortfolioPosition.gains": if e.complexity.PortfolioPosition.Gains == nil { break @@ -376,13 +390,6 @@ func (e *executableSchema) Complexity(typeName, field string, childComplexity in return e.complexity.PortfolioPosition.PurchaseValue(childComplexity), true - case "PortfolioPosition.quantity": - if e.complexity.PortfolioPosition.Quantity == nil { - break - } - - return e.complexity.PortfolioPosition.Quantity(childComplexity), true - case "PortfolioPosition.security": if e.complexity.PortfolioPosition.Security == nil { break @@ -425,6 +432,41 @@ func (e *executableSchema) Complexity(typeName, field string, childComplexity in return e.complexity.PortfolioSnapshot.Time(childComplexity), true + case "PortfolioSnapshot.totalGains": + if e.complexity.PortfolioSnapshot.TotalGains == nil { + break + } + + return e.complexity.PortfolioSnapshot.TotalGains(childComplexity), true + + case "PortfolioSnapshot.totalMarketValue": + if e.complexity.PortfolioSnapshot.TotalMarketValue == nil { + break + } + + return e.complexity.PortfolioSnapshot.TotalMarketValue(childComplexity), true + + case "PortfolioSnapshot.totalPortfolioValue": + if e.complexity.PortfolioSnapshot.TotalPortfolioValue == nil { + break + } + + return e.complexity.PortfolioSnapshot.TotalPortfolioValue(childComplexity), true + + case "PortfolioSnapshot.totalProfitOrLoss": + if e.complexity.PortfolioSnapshot.TotalProfitOrLoss == nil { + break + } + + return e.complexity.PortfolioSnapshot.TotalProfitOrLoss(childComplexity), true + + case "PortfolioSnapshot.totalPurchaseValue": + if e.complexity.PortfolioSnapshot.TotalPurchaseValue == nil { + break + } + + return e.complexity.PortfolioSnapshot.TotalPurchaseValue(childComplexity), true + case "Query.portfolio": if e.complexity.Query.Portfolio == nil { break @@ -938,7 +980,7 @@ func (ec *executionContext) fieldContext_BankAccount_displayName(_ context.Conte return fc, nil } -func (ec *executionContext) _Currency_value(ctx context.Context, field graphql.CollectedField, obj *persistence.Currency) (ret graphql.Marshaler) { +func (ec *executionContext) _Currency_value(ctx context.Context, field graphql.CollectedField, obj *currency.Currency) (ret graphql.Marshaler) { fc, err := ec.fieldContext_Currency_value(ctx, field) if err != nil { return graphql.Null @@ -982,7 +1024,7 @@ func (ec *executionContext) fieldContext_Currency_value(_ context.Context, field return fc, nil } -func (ec *executionContext) _Currency_symbol(ctx context.Context, field graphql.CollectedField, obj *persistence.Currency) (ret graphql.Marshaler) { +func (ec *executionContext) _Currency_symbol(ctx context.Context, field graphql.CollectedField, obj *currency.Currency) (ret graphql.Marshaler) { fc, err := ec.fieldContext_Currency_symbol(ctx, field) if err != nil { return graphql.Null @@ -1191,9 +1233,9 @@ func (ec *executionContext) _ListedSecurity_latestQuote(ctx context.Context, fie if resTmp == nil { return graphql.Null } - res := resTmp.(*persistence.Currency) + res := resTmp.(*currency.Currency) fc.Result = res - return ec.marshalOCurrency2ᚖgithubᚗcomᚋoxistoᚋmoneyᚑgopherᚋpersistenceᚐCurrency(ctx, field.Selections, res) + return ec.marshalOCurrency2ᚖgithubᚗcomᚋoxistoᚋmoneyᚑgopherᚋcurrencyᚐCurrency(ctx, field.Selections, res) } func (ec *executionContext) fieldContext_ListedSecurity_latestQuote(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { @@ -1621,6 +1663,16 @@ func (ec *executionContext) fieldContext_Portfolio_snapshot(ctx context.Context, return ec.fieldContext_PortfolioSnapshot_positions(ctx, field) case "firstTransactionTime": return ec.fieldContext_PortfolioSnapshot_firstTransactionTime(ctx, field) + case "totalPurchaseValue": + return ec.fieldContext_PortfolioSnapshot_totalPurchaseValue(ctx, field) + case "totalMarketValue": + return ec.fieldContext_PortfolioSnapshot_totalMarketValue(ctx, field) + case "totalProfitOrLoss": + return ec.fieldContext_PortfolioSnapshot_totalProfitOrLoss(ctx, field) + case "totalGains": + return ec.fieldContext_PortfolioSnapshot_totalGains(ctx, field) + case "totalPortfolioValue": + return ec.fieldContext_PortfolioSnapshot_totalPortfolioValue(ctx, field) case "cash": return ec.fieldContext_PortfolioSnapshot_cash(ctx, field) } @@ -1751,7 +1803,7 @@ func (ec *executionContext) _PortfolioEvent_type(ctx context.Context, field grap }() resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (any, error) { ctx = rctx // use context from middleware stack in children - return ec.resolvers.PortfolioEvent().Type(rctx, obj) + return obj.Type, nil }) if err != nil { ec.Error(ctx, err) @@ -1763,17 +1815,17 @@ func (ec *executionContext) _PortfolioEvent_type(ctx context.Context, field grap } return graphql.Null } - res := resTmp.(models.PortfolioEventType) + res := resTmp.(events.PortfolioEventType) fc.Result = res - return ec.marshalNPortfolioEventType2githubᚗcomᚋoxistoᚋmoneyᚑgopherᚋmodelsᚐPortfolioEventType(ctx, field.Selections, res) + return ec.marshalNPortfolioEventType2githubᚗcomᚋoxistoᚋmoneyᚑgopherᚋportfolioᚋeventsᚐPortfolioEventType(ctx, field.Selections, res) } func (ec *executionContext) fieldContext_PortfolioEvent_type(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { fc = &graphql.FieldContext{ Object: "PortfolioEvent", Field: field, - IsMethod: true, - IsResolver: true, + IsMethod: false, + IsResolver: false, Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { return nil, errors.New("field of type PortfolioEventType does not have child fields") }, @@ -1886,8 +1938,8 @@ func (ec *executionContext) fieldContext_PortfolioPosition_security(_ context.Co return fc, nil } -func (ec *executionContext) _PortfolioPosition_quantity(ctx context.Context, field graphql.CollectedField, obj *models.PortfolioPosition) (ret graphql.Marshaler) { - fc, err := ec.fieldContext_PortfolioPosition_quantity(ctx, field) +func (ec *executionContext) _PortfolioPosition_amount(ctx context.Context, field graphql.CollectedField, obj *models.PortfolioPosition) (ret graphql.Marshaler) { + fc, err := ec.fieldContext_PortfolioPosition_amount(ctx, field) if err != nil { return graphql.Null } @@ -1900,7 +1952,7 @@ func (ec *executionContext) _PortfolioPosition_quantity(ctx context.Context, fie }() resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (any, error) { ctx = rctx // use context from middleware stack in children - return obj.Quantity, nil + return obj.Amount, nil }) if err != nil { ec.Error(ctx, err) @@ -1912,19 +1964,19 @@ func (ec *executionContext) _PortfolioPosition_quantity(ctx context.Context, fie } return graphql.Null } - res := resTmp.(int) + res := resTmp.(float64) fc.Result = res - return ec.marshalNInt2int(ctx, field.Selections, res) + return ec.marshalNFloat2float64(ctx, field.Selections, res) } -func (ec *executionContext) fieldContext_PortfolioPosition_quantity(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { +func (ec *executionContext) fieldContext_PortfolioPosition_amount(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { fc = &graphql.FieldContext{ Object: "PortfolioPosition", Field: field, IsMethod: false, IsResolver: false, Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { - return nil, errors.New("field of type Int does not have child fields") + return nil, errors.New("field of type Float does not have child fields") }, } return fc, nil @@ -1956,9 +2008,9 @@ func (ec *executionContext) _PortfolioPosition_purchaseValue(ctx context.Context } return graphql.Null } - res := resTmp.(*persistence.Currency) + res := resTmp.(*currency.Currency) fc.Result = res - return ec.marshalNCurrency2ᚖgithubᚗcomᚋoxistoᚋmoneyᚑgopherᚋpersistenceᚐCurrency(ctx, field.Selections, res) + return ec.marshalNCurrency2ᚖgithubᚗcomᚋoxistoᚋmoneyᚑgopherᚋcurrencyᚐCurrency(ctx, field.Selections, res) } func (ec *executionContext) fieldContext_PortfolioPosition_purchaseValue(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { @@ -2006,9 +2058,9 @@ func (ec *executionContext) _PortfolioPosition_purchasePrice(ctx context.Context } return graphql.Null } - res := resTmp.(*persistence.Currency) + res := resTmp.(*currency.Currency) fc.Result = res - return ec.marshalNCurrency2ᚖgithubᚗcomᚋoxistoᚋmoneyᚑgopherᚋpersistenceᚐCurrency(ctx, field.Selections, res) + return ec.marshalNCurrency2ᚖgithubᚗcomᚋoxistoᚋmoneyᚑgopherᚋcurrencyᚐCurrency(ctx, field.Selections, res) } func (ec *executionContext) fieldContext_PortfolioPosition_purchasePrice(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { @@ -2056,9 +2108,9 @@ func (ec *executionContext) _PortfolioPosition_marketValue(ctx context.Context, } return graphql.Null } - res := resTmp.(*persistence.Currency) + res := resTmp.(*currency.Currency) fc.Result = res - return ec.marshalNCurrency2ᚖgithubᚗcomᚋoxistoᚋmoneyᚑgopherᚋpersistenceᚐCurrency(ctx, field.Selections, res) + return ec.marshalNCurrency2ᚖgithubᚗcomᚋoxistoᚋmoneyᚑgopherᚋcurrencyᚐCurrency(ctx, field.Selections, res) } func (ec *executionContext) fieldContext_PortfolioPosition_marketValue(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { @@ -2106,9 +2158,9 @@ func (ec *executionContext) _PortfolioPosition_marketPrice(ctx context.Context, } return graphql.Null } - res := resTmp.(*persistence.Currency) + res := resTmp.(*currency.Currency) fc.Result = res - return ec.marshalNCurrency2ᚖgithubᚗcomᚋoxistoᚋmoneyᚑgopherᚋpersistenceᚐCurrency(ctx, field.Selections, res) + return ec.marshalNCurrency2ᚖgithubᚗcomᚋoxistoᚋmoneyᚑgopherᚋcurrencyᚐCurrency(ctx, field.Selections, res) } func (ec *executionContext) fieldContext_PortfolioPosition_marketPrice(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { @@ -2156,9 +2208,9 @@ func (ec *executionContext) _PortfolioPosition_totalFees(ctx context.Context, fi } return graphql.Null } - res := resTmp.(*persistence.Currency) + res := resTmp.(*currency.Currency) fc.Result = res - return ec.marshalNCurrency2ᚖgithubᚗcomᚋoxistoᚋmoneyᚑgopherᚋpersistenceᚐCurrency(ctx, field.Selections, res) + return ec.marshalNCurrency2ᚖgithubᚗcomᚋoxistoᚋmoneyᚑgopherᚋcurrencyᚐCurrency(ctx, field.Selections, res) } func (ec *executionContext) fieldContext_PortfolioPosition_totalFees(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { @@ -2206,9 +2258,9 @@ func (ec *executionContext) _PortfolioPosition_profitOrLoss(ctx context.Context, } return graphql.Null } - res := resTmp.(*persistence.Currency) + res := resTmp.(*currency.Currency) fc.Result = res - return ec.marshalNCurrency2ᚖgithubᚗcomᚋoxistoᚋmoneyᚑgopherᚋpersistenceᚐCurrency(ctx, field.Selections, res) + return ec.marshalNCurrency2ᚖgithubᚗcomᚋoxistoᚋmoneyᚑgopherᚋcurrencyᚐCurrency(ctx, field.Selections, res) } func (ec *executionContext) fieldContext_PortfolioPosition_profitOrLoss(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { @@ -2359,8 +2411,8 @@ func (ec *executionContext) fieldContext_PortfolioSnapshot_positions(_ context.C switch field.Name { case "security": return ec.fieldContext_PortfolioPosition_security(ctx, field) - case "quantity": - return ec.fieldContext_PortfolioPosition_quantity(ctx, field) + case "amount": + return ec.fieldContext_PortfolioPosition_amount(ctx, field) case "purchaseValue": return ec.fieldContext_PortfolioPosition_purchaseValue(ctx, field) case "purchasePrice": @@ -2426,6 +2478,247 @@ func (ec *executionContext) fieldContext_PortfolioSnapshot_firstTransactionTime( return fc, nil } +func (ec *executionContext) _PortfolioSnapshot_totalPurchaseValue(ctx context.Context, field graphql.CollectedField, obj *models.PortfolioSnapshot) (ret graphql.Marshaler) { + fc, err := ec.fieldContext_PortfolioSnapshot_totalPurchaseValue(ctx, field) + if err != nil { + return graphql.Null + } + ctx = graphql.WithFieldContext(ctx, fc) + defer func() { + if r := recover(); r != nil { + ec.Error(ctx, ec.Recover(ctx, r)) + ret = graphql.Null + } + }() + resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (any, error) { + ctx = rctx // use context from middleware stack in children + return obj.TotalPurchaseValue, nil + }) + if err != nil { + ec.Error(ctx, err) + return graphql.Null + } + if resTmp == nil { + if !graphql.HasFieldError(ctx, fc) { + ec.Errorf(ctx, "must not be null") + } + return graphql.Null + } + res := resTmp.(*currency.Currency) + fc.Result = res + return ec.marshalNCurrency2ᚖgithubᚗcomᚋoxistoᚋmoneyᚑgopherᚋcurrencyᚐCurrency(ctx, field.Selections, res) +} + +func (ec *executionContext) fieldContext_PortfolioSnapshot_totalPurchaseValue(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { + fc = &graphql.FieldContext{ + Object: "PortfolioSnapshot", + Field: field, + IsMethod: false, + IsResolver: false, + Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + switch field.Name { + case "value": + return ec.fieldContext_Currency_value(ctx, field) + case "symbol": + return ec.fieldContext_Currency_symbol(ctx, field) + } + return nil, fmt.Errorf("no field named %q was found under type Currency", field.Name) + }, + } + return fc, nil +} + +func (ec *executionContext) _PortfolioSnapshot_totalMarketValue(ctx context.Context, field graphql.CollectedField, obj *models.PortfolioSnapshot) (ret graphql.Marshaler) { + fc, err := ec.fieldContext_PortfolioSnapshot_totalMarketValue(ctx, field) + if err != nil { + return graphql.Null + } + ctx = graphql.WithFieldContext(ctx, fc) + defer func() { + if r := recover(); r != nil { + ec.Error(ctx, ec.Recover(ctx, r)) + ret = graphql.Null + } + }() + resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (any, error) { + ctx = rctx // use context from middleware stack in children + return obj.TotalMarketValue, nil + }) + if err != nil { + ec.Error(ctx, err) + return graphql.Null + } + if resTmp == nil { + if !graphql.HasFieldError(ctx, fc) { + ec.Errorf(ctx, "must not be null") + } + return graphql.Null + } + res := resTmp.(*currency.Currency) + fc.Result = res + return ec.marshalNCurrency2ᚖgithubᚗcomᚋoxistoᚋmoneyᚑgopherᚋcurrencyᚐCurrency(ctx, field.Selections, res) +} + +func (ec *executionContext) fieldContext_PortfolioSnapshot_totalMarketValue(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { + fc = &graphql.FieldContext{ + Object: "PortfolioSnapshot", + Field: field, + IsMethod: false, + IsResolver: false, + Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + switch field.Name { + case "value": + return ec.fieldContext_Currency_value(ctx, field) + case "symbol": + return ec.fieldContext_Currency_symbol(ctx, field) + } + return nil, fmt.Errorf("no field named %q was found under type Currency", field.Name) + }, + } + return fc, nil +} + +func (ec *executionContext) _PortfolioSnapshot_totalProfitOrLoss(ctx context.Context, field graphql.CollectedField, obj *models.PortfolioSnapshot) (ret graphql.Marshaler) { + fc, err := ec.fieldContext_PortfolioSnapshot_totalProfitOrLoss(ctx, field) + if err != nil { + return graphql.Null + } + ctx = graphql.WithFieldContext(ctx, fc) + defer func() { + if r := recover(); r != nil { + ec.Error(ctx, ec.Recover(ctx, r)) + ret = graphql.Null + } + }() + resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (any, error) { + ctx = rctx // use context from middleware stack in children + return obj.TotalProfitOrLoss, nil + }) + if err != nil { + ec.Error(ctx, err) + return graphql.Null + } + if resTmp == nil { + if !graphql.HasFieldError(ctx, fc) { + ec.Errorf(ctx, "must not be null") + } + return graphql.Null + } + res := resTmp.(*currency.Currency) + fc.Result = res + return ec.marshalNCurrency2ᚖgithubᚗcomᚋoxistoᚋmoneyᚑgopherᚋcurrencyᚐCurrency(ctx, field.Selections, res) +} + +func (ec *executionContext) fieldContext_PortfolioSnapshot_totalProfitOrLoss(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { + fc = &graphql.FieldContext{ + Object: "PortfolioSnapshot", + Field: field, + IsMethod: false, + IsResolver: false, + Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + switch field.Name { + case "value": + return ec.fieldContext_Currency_value(ctx, field) + case "symbol": + return ec.fieldContext_Currency_symbol(ctx, field) + } + return nil, fmt.Errorf("no field named %q was found under type Currency", field.Name) + }, + } + return fc, nil +} + +func (ec *executionContext) _PortfolioSnapshot_totalGains(ctx context.Context, field graphql.CollectedField, obj *models.PortfolioSnapshot) (ret graphql.Marshaler) { + fc, err := ec.fieldContext_PortfolioSnapshot_totalGains(ctx, field) + if err != nil { + return graphql.Null + } + ctx = graphql.WithFieldContext(ctx, fc) + defer func() { + if r := recover(); r != nil { + ec.Error(ctx, ec.Recover(ctx, r)) + ret = graphql.Null + } + }() + resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (any, error) { + ctx = rctx // use context from middleware stack in children + return obj.TotalGains, nil + }) + if err != nil { + ec.Error(ctx, err) + return graphql.Null + } + if resTmp == nil { + if !graphql.HasFieldError(ctx, fc) { + ec.Errorf(ctx, "must not be null") + } + return graphql.Null + } + res := resTmp.(float64) + fc.Result = res + return ec.marshalNFloat2float64(ctx, field.Selections, res) +} + +func (ec *executionContext) fieldContext_PortfolioSnapshot_totalGains(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { + fc = &graphql.FieldContext{ + Object: "PortfolioSnapshot", + Field: field, + IsMethod: false, + IsResolver: false, + Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + return nil, errors.New("field of type Float does not have child fields") + }, + } + return fc, nil +} + +func (ec *executionContext) _PortfolioSnapshot_totalPortfolioValue(ctx context.Context, field graphql.CollectedField, obj *models.PortfolioSnapshot) (ret graphql.Marshaler) { + fc, err := ec.fieldContext_PortfolioSnapshot_totalPortfolioValue(ctx, field) + if err != nil { + return graphql.Null + } + ctx = graphql.WithFieldContext(ctx, fc) + defer func() { + if r := recover(); r != nil { + ec.Error(ctx, ec.Recover(ctx, r)) + ret = graphql.Null + } + }() + resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (any, error) { + ctx = rctx // use context from middleware stack in children + return obj.TotalPortfolioValue, nil + }) + if err != nil { + ec.Error(ctx, err) + return graphql.Null + } + if resTmp == nil { + return graphql.Null + } + res := resTmp.(*currency.Currency) + fc.Result = res + return ec.marshalOCurrency2ᚖgithubᚗcomᚋoxistoᚋmoneyᚑgopherᚋcurrencyᚐCurrency(ctx, field.Selections, res) +} + +func (ec *executionContext) fieldContext_PortfolioSnapshot_totalPortfolioValue(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { + fc = &graphql.FieldContext{ + Object: "PortfolioSnapshot", + Field: field, + IsMethod: false, + IsResolver: false, + Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + switch field.Name { + case "value": + return ec.fieldContext_Currency_value(ctx, field) + case "symbol": + return ec.fieldContext_Currency_symbol(ctx, field) + } + return nil, fmt.Errorf("no field named %q was found under type Currency", field.Name) + }, + } + return fc, nil +} + func (ec *executionContext) _PortfolioSnapshot_cash(ctx context.Context, field graphql.CollectedField, obj *models.PortfolioSnapshot) (ret graphql.Marshaler) { fc, err := ec.fieldContext_PortfolioSnapshot_cash(ctx, field) if err != nil { @@ -2452,9 +2745,9 @@ func (ec *executionContext) _PortfolioSnapshot_cash(ctx context.Context, field g } return graphql.Null } - res := resTmp.(*persistence.Currency) + res := resTmp.(*currency.Currency) fc.Result = res - return ec.marshalNCurrency2ᚖgithubᚗcomᚋoxistoᚋmoneyᚑgopherᚋpersistenceᚐCurrency(ctx, field.Selections, res) + return ec.marshalNCurrency2ᚖgithubᚗcomᚋoxistoᚋmoneyᚑgopherᚋcurrencyᚐCurrency(ctx, field.Selections, res) } func (ec *executionContext) fieldContext_PortfolioSnapshot_cash(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { @@ -4925,7 +5218,7 @@ func (ec *executionContext) _BankAccount(ctx context.Context, sel ast.SelectionS var currencyImplementors = []string{"Currency"} -func (ec *executionContext) _Currency(ctx context.Context, sel ast.SelectionSet, obj *persistence.Currency) graphql.Marshaler { +func (ec *executionContext) _Currency(ctx context.Context, sel ast.SelectionSet, obj *currency.Currency) graphql.Marshaler { fields := graphql.CollectFields(ec.OperationContext, sel, currencyImplementors) out := graphql.NewFieldSet(fields) @@ -5373,41 +5666,10 @@ func (ec *executionContext) _PortfolioEvent(ctx context.Context, sel ast.Selecti out.Concurrently(i, func(ctx context.Context) graphql.Marshaler { return innerFunc(ctx, out) }) case "type": - field := field - - innerFunc := func(ctx context.Context, fs *graphql.FieldSet) (res graphql.Marshaler) { - defer func() { - if r := recover(); r != nil { - ec.Error(ctx, ec.Recover(ctx, r)) - } - }() - res = ec._PortfolioEvent_type(ctx, field, obj) - if res == graphql.Null { - atomic.AddUint32(&fs.Invalids, 1) - } - return res - } - - if field.Deferrable != nil { - dfs, ok := deferred[field.Deferrable.Label] - di := 0 - if ok { - dfs.AddField(field) - di = len(dfs.Values) - 1 - } else { - dfs = graphql.NewFieldSet([]graphql.CollectedField{field}) - deferred[field.Deferrable.Label] = dfs - } - dfs.Concurrently(di, func(ctx context.Context) graphql.Marshaler { - return innerFunc(ctx, dfs) - }) - - // don't run the out.Concurrently() call below - out.Values[i] = graphql.Null - continue + out.Values[i] = ec._PortfolioEvent_type(ctx, field, obj) + if out.Values[i] == graphql.Null { + atomic.AddUint32(&out.Invalids, 1) } - - out.Concurrently(i, func(ctx context.Context) graphql.Marshaler { return innerFunc(ctx, out) }) case "security": field := field @@ -5480,8 +5742,8 @@ func (ec *executionContext) _PortfolioPosition(ctx context.Context, sel ast.Sele if out.Values[i] == graphql.Null { out.Invalids++ } - case "quantity": - out.Values[i] = ec._PortfolioPosition_quantity(ctx, field, obj) + case "amount": + out.Values[i] = ec._PortfolioPosition_amount(ctx, field, obj) if out.Values[i] == graphql.Null { out.Invalids++ } @@ -5569,6 +5831,28 @@ func (ec *executionContext) _PortfolioSnapshot(ctx context.Context, sel ast.Sele if out.Values[i] == graphql.Null { out.Invalids++ } + case "totalPurchaseValue": + out.Values[i] = ec._PortfolioSnapshot_totalPurchaseValue(ctx, field, obj) + if out.Values[i] == graphql.Null { + out.Invalids++ + } + case "totalMarketValue": + out.Values[i] = ec._PortfolioSnapshot_totalMarketValue(ctx, field, obj) + if out.Values[i] == graphql.Null { + out.Invalids++ + } + case "totalProfitOrLoss": + out.Values[i] = ec._PortfolioSnapshot_totalProfitOrLoss(ctx, field, obj) + if out.Values[i] == graphql.Null { + out.Invalids++ + } + case "totalGains": + out.Values[i] = ec._PortfolioSnapshot_totalGains(ctx, field, obj) + if out.Values[i] == graphql.Null { + out.Invalids++ + } + case "totalPortfolioValue": + out.Values[i] = ec._PortfolioSnapshot_totalPortfolioValue(ctx, field, obj) case "cash": out.Values[i] = ec._PortfolioSnapshot_cash(ctx, field, obj) if out.Values[i] == graphql.Null { @@ -6194,7 +6478,7 @@ func (ec *executionContext) marshalNBoolean2bool(ctx context.Context, sel ast.Se return res } -func (ec *executionContext) marshalNCurrency2ᚖgithubᚗcomᚋoxistoᚋmoneyᚑgopherᚋpersistenceᚐCurrency(ctx context.Context, sel ast.SelectionSet, v *persistence.Currency) graphql.Marshaler { +func (ec *executionContext) marshalNCurrency2ᚖgithubᚗcomᚋoxistoᚋmoneyᚑgopherᚋcurrencyᚐCurrency(ctx context.Context, sel ast.SelectionSet, v *currency.Currency) graphql.Marshaler { if v == nil { if !graphql.HasFieldError(ctx, graphql.GetFieldContext(ctx)) { ec.Errorf(ctx, "the requested element is null which the schema does not allow") @@ -6249,21 +6533,6 @@ func (ec *executionContext) marshalNID2string(ctx context.Context, sel ast.Selec return res } -func (ec *executionContext) unmarshalNInt2int(ctx context.Context, v any) (int, error) { - res, err := graphql.UnmarshalInt(v) - return res, graphql.ErrorOnPath(ctx, err) -} - -func (ec *executionContext) marshalNInt2int(ctx context.Context, sel ast.SelectionSet, v int) graphql.Marshaler { - res := graphql.MarshalInt(v) - if res == graphql.Null { - if !graphql.HasFieldError(ctx, graphql.GetFieldContext(ctx)) { - ec.Errorf(ctx, "the requested element is null which the schema does not allow") - } - } - return res -} - func (ec *executionContext) unmarshalNInt2int32(ctx context.Context, v any) (int32, error) { res, err := graphql.UnmarshalInt32(v) return res, graphql.ErrorOnPath(ctx, err) @@ -6402,16 +6671,35 @@ func (ec *executionContext) marshalNPortfolioEvent2ᚖgithubᚗcomᚋoxistoᚋmo return ec._PortfolioEvent(ctx, sel, v) } -func (ec *executionContext) unmarshalNPortfolioEventType2githubᚗcomᚋoxistoᚋmoneyᚑgopherᚋmodelsᚐPortfolioEventType(ctx context.Context, v any) (models.PortfolioEventType, error) { - var res models.PortfolioEventType - err := res.UnmarshalGQL(v) +func (ec *executionContext) unmarshalNPortfolioEventType2githubᚗcomᚋoxistoᚋmoneyᚑgopherᚋportfolioᚋeventsᚐPortfolioEventType(ctx context.Context, v any) (events.PortfolioEventType, error) { + tmp, err := graphql.UnmarshalString(v) + res := unmarshalNPortfolioEventType2githubᚗcomᚋoxistoᚋmoneyᚑgopherᚋportfolioᚋeventsᚐPortfolioEventType[tmp] return res, graphql.ErrorOnPath(ctx, err) } -func (ec *executionContext) marshalNPortfolioEventType2githubᚗcomᚋoxistoᚋmoneyᚑgopherᚋmodelsᚐPortfolioEventType(ctx context.Context, sel ast.SelectionSet, v models.PortfolioEventType) graphql.Marshaler { - return v +func (ec *executionContext) marshalNPortfolioEventType2githubᚗcomᚋoxistoᚋmoneyᚑgopherᚋportfolioᚋeventsᚐPortfolioEventType(ctx context.Context, sel ast.SelectionSet, v events.PortfolioEventType) graphql.Marshaler { + res := graphql.MarshalString(marshalNPortfolioEventType2githubᚗcomᚋoxistoᚋmoneyᚑgopherᚋportfolioᚋeventsᚐPortfolioEventType[v]) + if res == graphql.Null { + if !graphql.HasFieldError(ctx, graphql.GetFieldContext(ctx)) { + ec.Errorf(ctx, "the requested element is null which the schema does not allow") + } + } + return res } +var ( + unmarshalNPortfolioEventType2githubᚗcomᚋoxistoᚋmoneyᚑgopherᚋportfolioᚋeventsᚐPortfolioEventType = map[string]events.PortfolioEventType{ + "BUY": events.PortfolioEventTypeBuy, + "SELL": events.PortfolioEventTypeSell, + "DIVIDEND": events.PortfolioEventTypeDividend, + } + marshalNPortfolioEventType2githubᚗcomᚋoxistoᚋmoneyᚑgopherᚋportfolioᚋeventsᚐPortfolioEventType = map[events.PortfolioEventType]string{ + events.PortfolioEventTypeBuy: "BUY", + events.PortfolioEventTypeSell: "SELL", + events.PortfolioEventTypeDividend: "DIVIDEND", + } +) + func (ec *executionContext) marshalNPortfolioPosition2ᚕᚖgithubᚗcomᚋoxistoᚋmoneyᚑgopherᚋmodelsᚐPortfolioPositionᚄ(ctx context.Context, sel ast.SelectionSet, v []*models.PortfolioPosition) graphql.Marshaler { ret := make(graphql.Array, len(v)) var wg sync.WaitGroup @@ -6823,7 +7111,7 @@ func (ec *executionContext) marshalOBoolean2ᚖbool(ctx context.Context, sel ast return res } -func (ec *executionContext) marshalOCurrency2ᚖgithubᚗcomᚋoxistoᚋmoneyᚑgopherᚋpersistenceᚐCurrency(ctx context.Context, sel ast.SelectionSet, v *persistence.Currency) graphql.Marshaler { +func (ec *executionContext) marshalOCurrency2ᚖgithubᚗcomᚋoxistoᚋmoneyᚑgopherᚋcurrencyᚐCurrency(ctx context.Context, sel ast.SelectionSet, v *currency.Currency) graphql.Marshaler { if v == nil { return graphql.Null } diff --git a/graph/resolver.go b/graph/resolver.go index c5c5b9f4..4e6b627d 100644 --- a/graph/resolver.go +++ b/graph/resolver.go @@ -2,7 +2,7 @@ package graph import ( "github.com/oxisto/money-gopher/persistence" - "github.com/oxisto/money-gopher/service/securities" + "github.com/oxisto/money-gopher/securities/quote" ) // This file will not be regenerated automatically. @@ -11,7 +11,7 @@ import ( type Resolver struct { DB *persistence.DB - QuoteUpdater securities.QuoteUpdater + QuoteUpdater quote.QuoteUpdater } func withTx[T any](r *Resolver, f func(qtx *persistence.Queries) (*T, error)) (res *T, err error) { diff --git a/graph/schema.graphqls b/graph/schema.graphqls index b8d440dc..b211cd54 100644 --- a/graph/schema.graphqls +++ b/graph/schema.graphqls @@ -1,3 +1,10 @@ +directive @goModel( + model: String + models: [String!] +) on OBJECT | INPUT_OBJECT | SCALAR | ENUM | INTERFACE | UNION + +directive @goEnum(value: String) on ENUM_VALUE + scalar Date type Currency { @@ -28,10 +35,22 @@ type Portfolio { events: [PortfolioEvent!]! } -enum PortfolioEventType { +enum PortfolioEventType + @goModel( + model: "github.com/oxisto/money-gopher/portfolio/events.PortfolioEventType" + ) { BUY + @goEnum( + value: "github.com/oxisto/money-gopher/portfolio/events.PortfolioEventTypeBuy" + ) SELL + @goEnum( + value: "github.com/oxisto/money-gopher/portfolio/events.PortfolioEventTypeSell" + ) DIVIDEND + @goEnum( + value: "github.com/oxisto/money-gopher/portfolio/events.PortfolioEventTypeDividend" + ) } type PortfolioEvent { @@ -44,12 +63,17 @@ type PortfolioSnapshot { time: Date! positions: [PortfolioPosition!]! firstTransactionTime: Date! + totalPurchaseValue: Currency! + totalMarketValue: Currency! + totalProfitOrLoss: Currency! + totalGains: Float! + totalPortfolioValue: Currency cash: Currency! } type PortfolioPosition { security: Security! - quantity: Int! + amount: Float! """ PurchaseValue was the market value of this position when it was bought (net; diff --git a/graph/schema.resolvers.go b/graph/schema.resolvers.go index 86ece88d..e726ba51 100644 --- a/graph/schema.resolvers.go +++ b/graph/schema.resolvers.go @@ -10,38 +10,26 @@ import ( "slices" "time" + "github.com/oxisto/money-gopher/currency" "github.com/oxisto/money-gopher/finance" "github.com/oxisto/money-gopher/models" "github.com/oxisto/money-gopher/persistence" + "github.com/oxisto/money-gopher/securities/quote" ) // Security is the resolver for the security field. func (r *listedSecurityResolver) Security(ctx context.Context, obj *persistence.ListedSecurity) (*persistence.Security, error) { - return r.DB.GetSecurity(ctx, obj.SecurityID) + panic(fmt.Errorf("not implemented: Security - security")) } // LatestQuote is the resolver for the latestQuote field. -func (r *listedSecurityResolver) LatestQuote(ctx context.Context, obj *persistence.ListedSecurity) (*persistence.Currency, error) { - if obj.LatestQuote.Valid { - return &persistence.Currency{ - Value: int32(obj.LatestQuote.Int64), - Symbol: obj.Currency, - }, nil - } else { - return nil, nil - } +func (r *listedSecurityResolver) LatestQuote(ctx context.Context, obj *persistence.ListedSecurity) (*currency.Currency, error) { + panic(fmt.Errorf("not implemented: LatestQuote - latestQuote")) } // LatestQuoteTimestamp is the resolver for the latestQuoteTimestamp field. func (r *listedSecurityResolver) LatestQuoteTimestamp(ctx context.Context, obj *persistence.ListedSecurity) (*string, error) { - var s string - - if obj.LatestQuoteTimestamp.Valid { - s = obj.LatestQuoteTimestamp.Time.Format(time.RFC3339) - return &s, nil - } else { - return nil, nil - } + panic(fmt.Errorf("not implemented: LatestQuoteTimestamp - latestQuoteTimestamp")) } // CreateSecurity is the resolver for the createSecurity field. @@ -121,7 +109,7 @@ func (r *mutationResolver) UpdateSecurity(ctx context.Context, id string, input // TriggerQuoteUpdate is the resolver for the triggerQuoteUpdate field. func (r *mutationResolver) TriggerQuoteUpdate(ctx context.Context, securityIDs []string) (b bool, err error) { - err = r.QuoteUpdater.UpdateQuotes(ctx, securityIDs) + err = quote.UpdateQuotes(ctx, securityIDs, r.DB) if err != nil { return false, err } @@ -147,7 +135,7 @@ func (r *portfolioResolver) Snapshot(ctx context.Context, obj *persistence.Portf } } - return finance.BuildSnapshot(ctx, &t, obj.ID, r.DB) + return finance.BuildSnapshot(ctx, t, obj.ID, r.DB) } // Events is the resolver for the events field. @@ -160,11 +148,6 @@ func (r *portfolioEventResolver) Time(ctx context.Context, obj *persistence.Port panic(fmt.Errorf("not implemented: Time - time")) } -// Type is the resolver for the type field. -func (r *portfolioEventResolver) Type(ctx context.Context, obj *persistence.PortfolioEvent) (models.PortfolioEventType, error) { - panic(fmt.Errorf("not implemented: Type - type")) -} - // Security is the resolver for the security field. func (r *portfolioEventResolver) Security(ctx context.Context, obj *persistence.PortfolioEvent) (*persistence.Security, error) { panic(fmt.Errorf("not implemented: Security - security")) diff --git a/import/csv/csv_importer.go b/import/csv/csv_importer.go index da128b77..90ff9af1 100644 --- a/import/csv/csv_importer.go +++ b/import/csv/csv_importer.go @@ -40,11 +40,12 @@ import ( "time" moneygopher "github.com/oxisto/money-gopher" - portfoliov1 "github.com/oxisto/money-gopher/gen" + "github.com/oxisto/money-gopher/currency" + "github.com/oxisto/money-gopher/persistence" + "github.com/oxisto/money-gopher/portfolio/events" + "github.com/oxisto/money-gopher/securities/quote" "github.com/lmittmann/tint" - "github.com/oxisto/money-gopher/service/securities" - "google.golang.org/protobuf/types/known/timestamppb" ) var ( @@ -59,7 +60,7 @@ var ( // Import imports CSV records from a [io.Reader] containing portfolio // transactions. -func Import(r io.Reader, pname string) (txs []*portfoliov1.PortfolioEvent, secs []*portfoliov1.Security) { +func Import(r io.Reader, pname string) (txs []*persistence.PortfolioEvent, secs []*persistence.Security) { cr := csv.NewReader(r) cr.Comma = ';' @@ -82,17 +83,17 @@ func Import(r io.Reader, pname string) (txs []*portfoliov1.PortfolioEvent, secs } // Compact securities - secs = slices.CompactFunc(secs, func(a *portfoliov1.Security, b *portfoliov1.Security) bool { - return a.Id == b.Id + secs = slices.CompactFunc(secs, func(a *persistence.Security, b *persistence.Security) bool { + return a.ID == b.ID }) return } -func readLine(cr *csv.Reader, pname string) (tx *portfoliov1.PortfolioEvent, sec *portfoliov1.Security, err error) { +func readLine(cr *csv.Reader, pname string) (tx *persistence.PortfolioEvent, sec *persistence.Security, err error) { var ( record []string - value *portfoliov1.Currency + value *currency.Currency ) record, err = cr.Read() @@ -100,14 +101,14 @@ func readLine(cr *csv.Reader, pname string) (tx *portfoliov1.PortfolioEvent, sec return nil, nil, fmt.Errorf("%w: %w", ErrReadingCSV, err) } - tx = new(portfoliov1.PortfolioEvent) + tx = new(persistence.PortfolioEvent) tx.Time, err = txTime(record[0]) if err != nil { return nil, nil, fmt.Errorf("%w: %w", ErrParsingTime, err) } - tx.Type = txType(record[1]) - if tx.Type == portfoliov1.PortfolioEventType_PORTFOLIO_EVENT_TYPE_UNSPECIFIED { + tx.Type = int64(txType(record[1])) + if tx.Type == int64(events.PortfolioEventTypeUnknown) { return nil, nil, ErrParsingType } @@ -132,8 +133,8 @@ func readLine(cr *csv.Reader, pname string) (tx *portfoliov1.PortfolioEvent, sec } // Calculate the price - if tx.Type == portfoliov1.PortfolioEventType_PORTFOLIO_EVENT_TYPE_BUY || - tx.Type == portfoliov1.PortfolioEventType_PORTFOLIO_EVENT_TYPE_DELIVERY_INBOUND { + if tx.Type == events.PortfolioEventTypeBuy || + tx.Type == events.PortfolioEventType { tx.Price = portfoliov1.Divide(portfoliov1.Minus(value, tx.Fees), tx.Amount) } else if tx.Type == portfoliov1.PortfolioEventType_PORTFOLIO_EVENT_TYPE_SELL || tx.Type == portfoliov1.PortfolioEventType_PORTFOLIO_EVENT_TYPE_DELIVERY_OUTBOUND { @@ -153,9 +154,9 @@ func readLine(cr *csv.Reader, pname string) (tx *portfoliov1.PortfolioEvent, sec // Default to YF, but only if we have a ticker symbol, otherwise, let's try ING if len(sec.ListedOn) >= 0 && len(sec.ListedOn[0].Ticker) > 0 { - sec.QuoteProvider = moneygopher.Ref(securities.QuoteProviderYF) + sec.QuoteProvider = moneygopher.Ref(quote.QuoteProviderYF) } else { - sec.QuoteProvider = moneygopher.Ref(securities.QuoteProviderING) + sec.QuoteProvider = moneygopher.Ref(quote.QuoteProviderING) } tx.PortfolioId = pname @@ -165,36 +166,33 @@ func readLine(cr *csv.Reader, pname string) (tx *portfoliov1.PortfolioEvent, sec return } -func txType(typ string) portfoliov1.PortfolioEventType { +func txType(typ string) events.PortfolioEventType { switch typ { case "Buy": - return portfoliov1.PortfolioEventType_PORTFOLIO_EVENT_TYPE_BUY + return events.PortfolioEventTypeBuy case "Sell": - return portfoliov1.PortfolioEventType_PORTFOLIO_EVENT_TYPE_SELL + return events.PortfolioEventTypeSell case "Delivery (Inbound)": - return portfoliov1.PortfolioEventType_PORTFOLIO_EVENT_TYPE_DELIVERY_INBOUND + return events.PortfolioEventTypeDeliveryInbound case "Delivery (Outbound)": - return portfoliov1.PortfolioEventType_PORTFOLIO_EVENT_TYPE_DELIVERY_OUTBOUND + return events.PortfolioEventTypeDeliveryOutbound default: - return portfoliov1.PortfolioEventType_PORTFOLIO_EVENT_TYPE_UNSPECIFIED + return events.PortfolioEventTypeUnknown } } -func txTime(s string) (ts *timestamppb.Timestamp, err error) { - var ( - t time.Time - ) +func txTime(s string) (t time.Time, err error) { // First try without seconds t, err = time.ParseInLocation("2006-01-02T15:04", s, time.Local) if err != nil { // Then with seconds t, err = time.ParseInLocation("2006-01-02T15:04:05", s, time.Local) if err != nil { - return nil, err + return time.Time{}, err } } - return timestamppb.New(t), nil + return t, nil } func parseFloat64(s string) (f float64, err error) { @@ -211,17 +209,17 @@ func parseFloat64(s string) (f float64, err error) { return } -func parseFloatCurrency(s string) (c *portfoliov1.Currency, err error) { +func parseFloatCurrency(s string) (c *currency.Currency, err error) { // Get rid of all , and . s = strings.ReplaceAll(s, ".", "") s = strings.ReplaceAll(s, ",", "") i, err := strconv.ParseInt(s, 10, 32) if err != nil { - return portfoliov1.Zero(), err + return currency.Zero(), err } - return portfoliov1.Value(int32(i)), nil + return currency.Value(int32(i)), nil } func lsCurrency(txCurrency string, tickerCurrency string) string { diff --git a/import/csv/csv_importer_test.go b/import/csv/csv_importer_test.go index 47805924..4b6845cf 100644 --- a/import/csv/csv_importer_test.go +++ b/import/csv/csv_importer_test.go @@ -24,10 +24,9 @@ import ( "time" moneygopher "github.com/oxisto/money-gopher" - portfoliov1 "github.com/oxisto/money-gopher/gen" + "github.com/oxisto/money-gopher/securities/quote" "github.com/oxisto/assert" - "github.com/oxisto/money-gopher/service/securities" "google.golang.org/protobuf/testing/protocmp" "google.golang.org/protobuf/types/known/timestamppb" ) @@ -115,7 +114,7 @@ func Test_readLine(t *testing.T) { wantSec: &portfoliov1.Security{ Id: "US0378331005", DisplayName: "Apple Inc.", - QuoteProvider: moneygopher.Ref(securities.QuoteProviderYF), + QuoteProvider: moneygopher.Ref(quote.QuoteProviderYF), ListedOn: []*portfoliov1.ListedSecurity{ { SecurityId: "US0378331005", @@ -147,7 +146,7 @@ func Test_readLine(t *testing.T) { wantSec: &portfoliov1.Security{ Id: "US00827B1061", DisplayName: "Affirm Holdings Inc.", - QuoteProvider: moneygopher.Ref(securities.QuoteProviderYF), + QuoteProvider: moneygopher.Ref(quote.QuoteProviderYF), ListedOn: []*portfoliov1.ListedSecurity{ { SecurityId: "US00827B1061", @@ -179,7 +178,7 @@ func Test_readLine(t *testing.T) { wantSec: &portfoliov1.Security{ Id: "DE0005557508", DisplayName: "Deutsche Telekom AG", - QuoteProvider: moneygopher.Ref(securities.QuoteProviderYF), + QuoteProvider: moneygopher.Ref(quote.QuoteProviderYF), ListedOn: []*portfoliov1.ListedSecurity{ { SecurityId: "DE0005557508", diff --git a/internal/persistencetest/queries.go b/internal/persistencetest/queries.go new file mode 100644 index 00000000..41af36ad --- /dev/null +++ b/internal/persistencetest/queries.go @@ -0,0 +1,24 @@ +package persistencetest + +import ( + "testing" + + "github.com/oxisto/money-gopher/persistence" +) + +func NewTestDB(t *testing.T, inits ...func(db *persistence.DB)) (db *persistence.DB) { + var ( + err error + ) + + db, err = persistence.OpenDB(persistence.Options{UseInMemory: true}) + if err != nil { + t.Fatalf("Could not create test DB: %v", err) + } + + for _, init := range inits { + init(db) + } + + return +} diff --git a/internal/testing/servertest/servertest.go b/internal/testing/servertest/servertest.go index 12bba2b2..603b0da5 100644 --- a/internal/testing/servertest/servertest.go +++ b/internal/testing/servertest/servertest.go @@ -4,11 +4,9 @@ import ( "net/http" "net/http/httptest" - "github.com/oxisto/money-gopher/gen/portfoliov1connect" "github.com/oxisto/money-gopher/persistence" + "github.com/oxisto/money-gopher/securities/quote" "github.com/oxisto/money-gopher/server" - "github.com/oxisto/money-gopher/service/portfolio" - "github.com/oxisto/money-gopher/service/securities" "golang.org/x/net/http2" "golang.org/x/net/http2/h2c" @@ -17,17 +15,10 @@ import ( func NewServer(db *persistence.DB) *httptest.Server { mux := http.NewServeMux() srv := httptest.NewServer(h2c.NewHandler(mux, &http2.Server{})) - svc := securities.NewService(db) - mux.Handle(portfoliov1connect.NewPortfolioServiceHandler(portfolio.NewService( - portfolio.Options{ - DB: db, - SecuritiesClient: portfoliov1connect.NewSecuritiesServiceClient(srv.Client(), srv.URL), - }, - ))) - mux.Handle(portfoliov1connect.NewSecuritiesServiceHandler(svc)) + qu := quote.NewQuoteUpdater(db) - server.ConfigureGraphQL(mux, db, svc.(securities.QuoteUpdater)) + server.ConfigureGraphQL(mux, db, qu) return srv } diff --git a/models/models_gen.go b/models/models_gen.go index e2c4c0a2..700e9e42 100644 --- a/models/models_gen.go +++ b/models/models_gen.go @@ -3,10 +3,7 @@ package models import ( - "fmt" - "io" - "strconv" - + "github.com/oxisto/money-gopher/currency" "github.com/oxisto/money-gopher/persistence" ) @@ -20,33 +17,38 @@ type Mutation struct { type PortfolioPosition struct { Security *persistence.Security `json:"security"` - Quantity int `json:"quantity"` + Amount float64 `json:"amount"` // PurchaseValue was the market value of this position when it was bought (net; // exclusive of any fees). - PurchaseValue *persistence.Currency `json:"purchaseValue"` + PurchaseValue *currency.Currency `json:"purchaseValue"` // PurchasePrice was the market price of this position when it was bought (net; // exclusive of any fees). - PurchasePrice *persistence.Currency `json:"purchasePrice"` + PurchasePrice *currency.Currency `json:"purchasePrice"` // MarketValue is the current market value of this position, as retrieved from // the securities service. - MarketValue *persistence.Currency `json:"marketValue"` + MarketValue *currency.Currency `json:"marketValue"` // MarketPrice is the current market price of this position, as retrieved from // the securities service. - MarketPrice *persistence.Currency `json:"marketPrice"` + MarketPrice *currency.Currency `json:"marketPrice"` // TotalFees is the total amount of fees accumulating in this position through // various transactions. - TotalFees *persistence.Currency `json:"totalFees"` + TotalFees *currency.Currency `json:"totalFees"` // ProfitOrLoss contains the absolute amount of profit or loss in this position. - ProfitOrLoss *persistence.Currency `json:"profitOrLoss"` + ProfitOrLoss *currency.Currency `json:"profitOrLoss"` // Gains contains the relative amount of profit or loss in this position. Gains float64 `json:"gains"` } type PortfolioSnapshot struct { - Time string `json:"time"` - Positions []*PortfolioPosition `json:"positions"` - FirstTransactionTime string `json:"firstTransactionTime"` - Cash *persistence.Currency `json:"cash"` + Time string `json:"time"` + Positions []*PortfolioPosition `json:"positions"` + FirstTransactionTime string `json:"firstTransactionTime"` + TotalPurchaseValue *currency.Currency `json:"totalPurchaseValue"` + TotalMarketValue *currency.Currency `json:"totalMarketValue"` + TotalProfitOrLoss *currency.Currency `json:"totalProfitOrLoss"` + TotalGains float64 `json:"totalGains"` + TotalPortfolioValue *currency.Currency `json:"totalPortfolioValue,omitempty"` + Cash *currency.Currency `json:"cash"` } type Query struct { @@ -57,46 +59,3 @@ type SecurityInput struct { DisplayName string `json:"displayName"` ListedAs []*ListedSecurityInput `json:"listedAs,omitempty"` } - -type PortfolioEventType string - -const ( - PortfolioEventTypeBuy PortfolioEventType = "BUY" - PortfolioEventTypeSell PortfolioEventType = "SELL" - PortfolioEventTypeDividend PortfolioEventType = "DIVIDEND" -) - -var AllPortfolioEventType = []PortfolioEventType{ - PortfolioEventTypeBuy, - PortfolioEventTypeSell, - PortfolioEventTypeDividend, -} - -func (e PortfolioEventType) IsValid() bool { - switch e { - case PortfolioEventTypeBuy, PortfolioEventTypeSell, PortfolioEventTypeDividend: - return true - } - return false -} - -func (e PortfolioEventType) String() string { - return string(e) -} - -func (e *PortfolioEventType) UnmarshalGQL(v any) error { - str, ok := v.(string) - if !ok { - return fmt.Errorf("enums must be strings") - } - - *e = PortfolioEventType(str) - if !e.IsValid() { - return fmt.Errorf("%s is not a valid PortfolioEventType", str) - } - return nil -} - -func (e PortfolioEventType) MarshalGQL(w io.Writer) { - fmt.Fprint(w, strconv.Quote(e.String())) -} diff --git a/money-gopher.code-workspace b/money-gopher.code-workspace index 5405857b..247cd820 100644 --- a/money-gopher.code-workspace +++ b/money-gopher.code-workspace @@ -13,6 +13,7 @@ "bufbuild", "clitest", "connectrpc", + "DBTX", "emptypb", "fatih", "fieldmaskpb", @@ -33,6 +34,7 @@ "myportfolio", "mysecurity", "oxisto", + "persistencetest", "portfoliotest", "portfoliov", "protobuf", diff --git a/persistence/currency.go b/persistence/currency.go deleted file mode 100644 index 1a616d44..00000000 --- a/persistence/currency.go +++ /dev/null @@ -1,20 +0,0 @@ -package persistence - -// Currency represents a currency with a value and a symbol. -type Currency struct { - // Value is the value of the currency. - Value int32 `json:"value"` - - // Symbol is the symbol of the currency. - Symbol string `json:"symbol"` -} - -func Zero() *Currency { - // TODO(oxisto): Somehow make it possible to change default currency - return &Currency{Symbol: "EUR"} -} - -func Value(v int32) *Currency { - // TODO(oxisto): Somehow make it possible to change default currency - return &Currency{Symbol: "EUR", Value: v} -} diff --git a/persistence/relationships.go b/persistence/extra.go similarity index 91% rename from persistence/relationships.go rename to persistence/extra.go index 6869f92d..1587ea53 100644 --- a/persistence/relationships.go +++ b/persistence/extra.go @@ -1,6 +1,8 @@ package persistence -import "context" +import ( + "context" +) // ListedAs returns the listed securities for the security. func (s *Security) ListedAs(ctx context.Context, db *DB) ([]*ListedSecurity, error) { diff --git a/persistence/models.go b/persistence/models.go index ee77081b..6495d65b 100644 --- a/persistence/models.go +++ b/persistence/models.go @@ -7,6 +7,9 @@ package persistence import ( "database/sql" "time" + + currency "github.com/oxisto/money-gopher/currency" + "github.com/oxisto/money-gopher/portfolio/events" ) type BankAccount struct { @@ -24,8 +27,8 @@ type ListedSecurity struct { Ticker string // Currency is the currency in which the security is traded. Currency string - // LatestQuote is the latest quote for the security. - LatestQuote sql.NullInt64 + // LatestQuote is the latest quote for the security as a [currency.Currency]. + LatestQuote *currency.Currency // LatestQuoteTimestamp is the timestamp of the latest quote. LatestQuoteTimestamp sql.NullTime } @@ -40,18 +43,15 @@ type Portfolio struct { } type PortfolioEvent struct { - ID string - Type int64 - Time time.Time - PortfolioID string - SecurityID string - Amount sql.NullFloat64 - Price sql.NullInt64 - PriceCurrency sql.NullString - Fees sql.NullInt64 - FeesCurrency sql.NullString - Taxes sql.NullInt64 - TaxesCurrency sql.NullString + ID string + Type events.PortfolioEventType + Time time.Time + PortfolioID string + SecurityID string + Amount sql.NullFloat64 + Price *currency.Currency + Fees *currency.Currency + Taxes *currency.Currency } // Security represents a security that can be traded on an exchange. diff --git a/persistence/portfolios.sql.go b/persistence/portfolios.sql.go index 3a5abcd8..d80dfc54 100644 --- a/persistence/portfolios.sql.go +++ b/persistence/portfolios.sql.go @@ -62,7 +62,7 @@ func (q *Queries) GetPortfolio(ctx context.Context, id string) (*Portfolio, erro const listPortfolioEventsByPortfolioID = `-- name: ListPortfolioEventsByPortfolioID :many SELECT - id, type, time, portfolio_id, security_id, amount, price, price_currency, fees, fees_currency, taxes, taxes_currency + id, type, time, portfolio_id, security_id, amount, price, fees, taxes FROM portfolio_events WHERE @@ -86,11 +86,8 @@ func (q *Queries) ListPortfolioEventsByPortfolioID(ctx context.Context, portfoli &i.SecurityID, &i.Amount, &i.Price, - &i.PriceCurrency, &i.Fees, - &i.FeesCurrency, &i.Taxes, - &i.TaxesCurrency, ); err != nil { return nil, err } diff --git a/persistence/securities.sql.go b/persistence/securities.sql.go index 49d85ad2..dbaaa68b 100644 --- a/persistence/securities.sql.go +++ b/persistence/securities.sql.go @@ -9,6 +9,8 @@ import ( "context" "database/sql" "strings" + + currency "github.com/oxisto/money-gopher/currency" ) const createSecurity = `-- name: CreateSecurity :one @@ -233,7 +235,7 @@ type UpsertListedSecurityParams struct { SecurityID string Ticker string Currency string - LatestQuote sql.NullInt64 + LatestQuote *currency.Currency LatestQuoteTimestamp sql.NullTime } diff --git a/persistence/securities.sql_test.go b/persistence/securities.sql_test.go new file mode 100644 index 00000000..ff46c985 --- /dev/null +++ b/persistence/securities.sql_test.go @@ -0,0 +1,70 @@ +package persistence_test + +import ( + "context" + "testing" + + "github.com/oxisto/assert" + "github.com/oxisto/money-gopher/internal/persistencetest" + "github.com/oxisto/money-gopher/persistence" +) + +func TestQueries_ListListedSecuritiesBySecurityID(t *testing.T) { + type fields struct { + db persistence.DBTX + } + type args struct { + ctx context.Context + id string + } + tests := []struct { + name string + fields fields + args args + want []*persistence.ListedSecurity + wantErr bool + }{ + { + name: "happy path", + fields: fields{ + db: persistencetest.NewTestDB(t, func(db *persistence.DB) { + _, err := db.Queries.CreateSecurity(context.Background(), persistence.CreateSecurityParams{ + ID: "DE1234567890", + DisplayName: "My Security", + }) + assert.NoError(t, err) + + _, err = db.Queries.UpsertListedSecurity(context.Background(), persistence.UpsertListedSecurityParams{ + SecurityID: "DE1234567890", + Ticker: "TICK", + Currency: "USD", + }) + assert.NoError(t, err) + }), + }, + args: args{ + ctx: context.TODO(), + id: "DE1234567890", + }, + want: []*persistence.ListedSecurity{ + { + SecurityID: "DE1234567890", + Ticker: "TICK", + Currency: "USD", + }, + }, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + q := persistence.New(tt.fields.db) + got, err := q.ListListedSecuritiesBySecurityID(tt.args.ctx, tt.args.id) + if (err != nil) != tt.wantErr { + t.Errorf("Queries.ListListedSecuritiesBySecurityID() error = %v, wantErr %v", err, tt.wantErr) + return + } + assert.Equals(t, tt.want, got) + }) + } +} diff --git a/persistence/sql/migrations/0001_create_securities.sql b/persistence/sql/migrations/0001_create_securities.sql index 4599739f..e8e5fb46 100644 --- a/persistence/sql/migrations/0001_create_securities.sql +++ b/persistence/sql/migrations/0001_create_securities.sql @@ -13,7 +13,7 @@ CREATE TABLE security_id TEXT NOT NULL, -- SecurityID is the ID of the security. ticker TEXT NOT NULL, -- Ticker is the symbol used to identify the security on the exchange. currency TEXT NOT NULL, -- Currency is the currency in which the security is traded. - latest_quote INTEGER, -- LatestQuote is the latest quote for the security. + latest_quote JSONB, -- LatestQuote is the latest quote for the security as a [currency.Currency]. latest_quote_timestamp DATETIME, -- LatestQuoteTimestamp is the timestamp of the latest quote. FOREIGN KEY (security_id) REFERENCES securities (id) ON DELETE RESTRICT, PRIMARY KEY (security_id, ticker) diff --git a/persistence/sql/migrations/0002_create_portfolio.sql b/persistence/sql/migrations/0002_create_portfolio.sql index 9e27024d..3391b197 100644 --- a/persistence/sql/migrations/0002_create_portfolio.sql +++ b/persistence/sql/migrations/0002_create_portfolio.sql @@ -15,12 +15,9 @@ CREATE TABLE portfolio_id TEXT NOT NULL, security_id TEXT NOT NULL, amount REAL, - price INTEGER, - price_currency TEXT, - fees INTEGER, - fees_currency TEXT, - taxes INTEGER, - taxes_currency TEXT + price JSONB, + fees JSONB, + taxes JSONB ); CREATE TABLE diff --git a/portfolio/events/type.go b/portfolio/events/type.go new file mode 100644 index 00000000..b6435364 --- /dev/null +++ b/portfolio/events/type.go @@ -0,0 +1,23 @@ +package events + +// PortfolioEventType is the type of a portfolio event. +type PortfolioEventType int + +const ( + // PortfolioEventTypeBuy represents a buy event. + PortfolioEventTypeBuy PortfolioEventType = iota + 1 + // PortfolioEventTypeSell represents a sell event. + PortfolioEventTypeSell + // PortfolioEventTypeDividend represents a dividend event. + PortfolioEventTypeDividend + // PortfolioEventTypeTax represents the inbound delivery of a security or cash from another account. + PortfolioEventTypeDeliveryInbound + // PortfolioEventTypeTax represents the outbound delivery of a security or cash to another account. + PortfolioEventTypeDeliveryOutbound + // PortfolioEventTypeDepositCash represents a deposit of cash. + PortfolioEventTypeDepositCash + // PortfolioEventTypeWithdrawCash represents a withdrawal of cash. + PortfolioEventTypeWithdrawCash + // PortfolioEventTypeUnknown represents an unknown event type. + PortfolioEventTypeUnknown +) diff --git a/service/securities/quote.go b/securities/quote/quote.go similarity index 57% rename from service/securities/quote.go rename to securities/quote/quote.go index a4c704eb..668c3a64 100644 --- a/service/securities/quote.go +++ b/securities/quote/quote.go @@ -14,7 +14,10 @@ // // This file is part of The Money Gopher. -package securities +// package quote contains the logic to update quotes for securities. Its main +// way to interface is the [QuoteUpdater] interface. A default implementation +// for the interface can be created using [NewQuoteUpdater]. +package quote import ( "context" @@ -22,41 +25,50 @@ import ( "log/slog" "time" - portfoliov1 "github.com/oxisto/money-gopher/gen" + "github.com/oxisto/money-gopher/currency" "github.com/oxisto/money-gopher/persistence" - "connectrpc.com/connect" "github.com/lmittmann/tint" ) -// UpdateQuotes triggers an update of the quotes for the given securities. -func (svc *service) UpdateQuotes(ctx context.Context, IDs []string) (err error) { +// QuoteProvider is an interface that retrieves quotes for a [ListedSecurity]. They +// can either be historical quotes or the latest quote. +type QuoteUpdater interface { + UpdateQuotes(ctx context.Context, IDs []string) (err error) +} + +// qu is the internal default implementation of the [QuoteUpdater] interface. +type qu struct { + db *persistence.DB +} + +// NewQuoteUpdater creates a new instance of the [QuoteUpdater] interface. +func NewQuoteUpdater(db *persistence.DB) QuoteUpdater { + return &qu{ + db: db, + } +} + +// UpdateQuotes triggers an update of the quotes for the given securities' IDs. +func (qu *qu) UpdateQuotes(ctx context.Context, secIDs []string) (err error) { var ( - sec *persistence.Security secs []*persistence.Security listed []*persistence.ListedSecurity qp QuoteProvider ok bool ) - if len(IDs) == 0 { - secs, err = svc.db.ListSecurities(ctx) - if err != nil { - return err - } - - for _, sec := range secs { - IDs = append(IDs, sec.ID) - } + // Fetch all securities if no IDs are given + if len(secIDs) == 0 { + secs, err = qu.db.ListSecurities(ctx) + } else { + secs, err = qu.db.ListSecuritiesByIDs(ctx, secIDs) + } + if err != nil { + return err } - for _, id := range IDs { - // Fetch security - sec, err = svc.db.GetSecurity(ctx, id) - if err != nil { - return err - } - + for _, sec := range secs { if !sec.QuoteProvider.Valid { slog.Warn("No quote provider configured for security", "security", sec.ID) return @@ -67,7 +79,7 @@ func (svc *service) UpdateQuotes(ctx context.Context, IDs []string) (err error) return } - listed, err = sec.ListedAs(ctx, svc.db) + listed, err = sec.ListedAs(ctx, qu.db) if err != nil { return err } @@ -78,7 +90,7 @@ func (svc *service) UpdateQuotes(ctx context.Context, IDs []string) (err error) go func() { slog.Debug("Triggering quote update", "security", ls, "provider", sec.QuoteProvider) - err = svc.updateQuote(qp, ls) + err = qu.updateQuote(qp, ls) if err != nil { slog.Error("An error occurred during quote update", tint.Err(err), "ls", ls) } @@ -89,20 +101,10 @@ func (svc *service) UpdateQuotes(ctx context.Context, IDs []string) (err error) return } -func (svc *service) TriggerSecurityQuoteUpdate(ctx context.Context, req *connect.Request[portfoliov1.TriggerQuoteUpdateRequest]) (res *connect.Response[portfoliov1.TriggerQuoteUpdateResponse], err error) { - err = svc.UpdateQuotes(ctx, req.Msg.SecurityIds) - if err != nil { - return nil, connect.NewError(connect.CodeInternal, err) - } - - res = connect.NewResponse(&portfoliov1.TriggerQuoteUpdateResponse{}) - - return -} - -func (svc *service) updateQuote(qp QuoteProvider, ls *persistence.ListedSecurity) (err error) { +// updateQuote updates the quote for the given [ListedSecurity] using the given [QuoteProvider]. +func (qu *qu) updateQuote(qp QuoteProvider, ls *persistence.ListedSecurity) (err error) { var ( - quote *persistence.Currency + quote *currency.Currency t time.Time ctx context.Context cancel context.CancelFunc @@ -116,10 +118,10 @@ func (svc *service) updateQuote(qp QuoteProvider, ls *persistence.ListedSecurity return err } - ls.LatestQuote = sql.NullInt64{Int64: int64(quote.Value), Valid: true} + ls.LatestQuote = quote ls.LatestQuoteTimestamp = sql.NullTime{Time: t, Valid: true} - _, err = svc.db.UpsertListedSecurity(ctx, persistence.UpsertListedSecurityParams{ + _, err = qu.db.UpsertListedSecurity(ctx, persistence.UpsertListedSecurityParams{ SecurityID: ls.SecurityID, Ticker: ls.Ticker, Currency: ls.Currency, diff --git a/service/securities/quote_provider.go b/securities/quote/quote_provider.go similarity index 92% rename from service/securities/quote_provider.go rename to securities/quote/quote_provider.go index 698e7f37..2cd9141c 100644 --- a/service/securities/quote_provider.go +++ b/securities/quote/quote_provider.go @@ -14,12 +14,13 @@ // // This file is part of The Money Gopher. -package securities +package quote import ( "context" "time" + "github.com/oxisto/money-gopher/currency" "github.com/oxisto/money-gopher/persistence" ) @@ -39,5 +40,5 @@ func RegisterQuoteProvider(name string, qp QuoteProvider) { // QuoteProvider is an interface that retrieves quotes for a [ListedSecurity]. They // can either be historical quotes or the latest quote. type QuoteProvider interface { - LatestQuote(ctx context.Context, ls *persistence.ListedSecurity) (quote *persistence.Currency, t time.Time, err error) + LatestQuote(ctx context.Context, ls *persistence.ListedSecurity) (quote *currency.Currency, t time.Time, err error) } diff --git a/service/securities/quote_provider_ing.go b/securities/quote/quote_provider_ing.go similarity index 87% rename from service/securities/quote_provider_ing.go rename to securities/quote/quote_provider_ing.go index cc7a579d..764eb66f 100644 --- a/service/securities/quote_provider_ing.go +++ b/securities/quote/quote_provider_ing.go @@ -14,7 +14,7 @@ // // This file is part of The Money Gopher. -package securities +package quote import ( "context" @@ -23,6 +23,7 @@ import ( "net/http" "time" + "github.com/oxisto/money-gopher/currency" "github.com/oxisto/money-gopher/persistence" ) @@ -45,7 +46,7 @@ type header struct { WKN string `json:"wkn"` } -func (ing *ing) LatestQuote(ctx context.Context, ls *persistence.ListedSecurity) (quote *persistence.Currency, t time.Time, err error) { +func (ing *ing) LatestQuote(ctx context.Context, ls *persistence.ListedSecurity) (quote *currency.Currency, t time.Time, err error) { var ( res *http.Response h header @@ -62,8 +63,8 @@ func (ing *ing) LatestQuote(ctx context.Context, ls *persistence.ListedSecurity) } if h.HasBidAsk { - return persistence.Value(int32(h.Bid * 100)), h.BidDate, nil + return currency.Value(int32(h.Bid * 100)), h.BidDate, nil } else { - return persistence.Value(int32(h.Price * 100)), h.PriceChangedDate, nil + return currency.Value(int32(h.Price * 100)), h.PriceChangedDate, nil } } diff --git a/service/securities/quote_provider_ing_test.go b/securities/quote/quote_provider_ing_test.go similarity index 96% rename from service/securities/quote_provider_ing_test.go rename to securities/quote/quote_provider_ing_test.go index dce44953..a97ba24d 100644 --- a/service/securities/quote_provider_ing_test.go +++ b/securities/quote/quote_provider_ing_test.go @@ -14,7 +14,7 @@ // // This file is part of The Money Gopher. -package securities +package quote import ( "context" @@ -25,6 +25,7 @@ import ( "testing" "time" + "github.com/oxisto/money-gopher/currency" "github.com/oxisto/money-gopher/persistence" "google.golang.org/protobuf/testing/protocmp" @@ -43,7 +44,7 @@ func Test_ing_LatestQuote(t *testing.T) { name string fields fields args args - wantQuote *persistence.Currency + wantQuote *currency.Currency wantTime time.Time wantErr assert.Want[error] }{ @@ -102,7 +103,7 @@ func Test_ing_LatestQuote(t *testing.T) { Currency: "EUR", }, }, - wantQuote: persistence.Value(10000), + wantQuote: currency.Value(10000), wantTime: time.Date(2023, 05, 04, 20, 0, 0, 0, time.UTC), wantErr: func(t *testing.T, err error) bool { return true }, }, diff --git a/service/securities/quote_provider_yf.go b/securities/quote/quote_provider_yf.go similarity index 89% rename from service/securities/quote_provider_yf.go rename to securities/quote/quote_provider_yf.go index 412d9691..fdee4600 100644 --- a/service/securities/quote_provider_yf.go +++ b/securities/quote/quote_provider_yf.go @@ -14,7 +14,7 @@ // // This file is part of The Money Gopher. -package securities +package quote import ( "context" @@ -24,6 +24,7 @@ import ( "net/http" "time" + "github.com/oxisto/money-gopher/currency" "github.com/oxisto/money-gopher/persistence" ) @@ -46,7 +47,7 @@ type chart struct { } `json:"chart"` } -func (yf *yf) LatestQuote(ctx context.Context, ls *persistence.ListedSecurity) (quote *persistence.Currency, t time.Time, err error) { +func (yf *yf) LatestQuote(ctx context.Context, ls *persistence.ListedSecurity) (quote *currency.Currency, t time.Time, err error) { var ( res *http.Response ch chart @@ -66,6 +67,6 @@ func (yf *yf) LatestQuote(ctx context.Context, ls *persistence.ListedSecurity) ( return nil, t, ErrEmptyResult } - return persistence.Value(int32(ch.Chart.Results[0].Meta.RegularMarketPrice * 100)), + return currency.Value(int32(ch.Chart.Results[0].Meta.RegularMarketPrice * 100)), time.Unix(ch.Chart.Results[0].Meta.RegularMarketTime, 0), nil } diff --git a/service/securities/quote_provider_yf_test.go b/securities/quote/quote_provider_yf_test.go similarity index 96% rename from service/securities/quote_provider_yf_test.go rename to securities/quote/quote_provider_yf_test.go index 582d991f..79e425f5 100644 --- a/service/securities/quote_provider_yf_test.go +++ b/securities/quote/quote_provider_yf_test.go @@ -14,7 +14,7 @@ // // This file is part of The Money Gopher. -package securities +package quote import ( "context" @@ -25,6 +25,7 @@ import ( "testing" "time" + "github.com/oxisto/money-gopher/currency" "github.com/oxisto/money-gopher/persistence" "google.golang.org/protobuf/testing/protocmp" @@ -57,7 +58,7 @@ func Test_yf_LatestQuote(t *testing.T) { name string fields fields args args - wantQuote *persistence.Currency + wantQuote *currency.Currency wantTime time.Time wantErr assert.Want[error] }{ @@ -136,7 +137,7 @@ func Test_yf_LatestQuote(t *testing.T) { Currency: "USD", }, }, - wantQuote: persistence.Value(10000), + wantQuote: currency.Value(10000), wantTime: time.Date(2023, 05, 04, 20, 0, 0, 0, time.UTC), wantErr: func(t *testing.T, err error) bool { return true }, }, diff --git a/service/securities/quote_test.go b/securities/quote/quote_test.go similarity index 51% rename from service/securities/quote_test.go rename to securities/quote/quote_test.go index c5e88bc4..253fed8b 100644 --- a/service/securities/quote_test.go +++ b/securities/quote/quote_test.go @@ -14,7 +14,7 @@ // // This file is part of The Money Gopher. -package securities +package quote import ( "context" @@ -22,13 +22,11 @@ import ( "testing" "time" - portfoliov1 "github.com/oxisto/money-gopher/gen" + "github.com/oxisto/money-gopher/currency" "github.com/oxisto/money-gopher/internal" "github.com/oxisto/money-gopher/persistence" - "connectrpc.com/connect" "github.com/oxisto/assert" - "golang.org/x/text/currency" ) const QuoteProviderMock = "mock" @@ -36,77 +34,17 @@ const QuoteProviderMock = "mock" type mockQP struct { } -func (m *mockQP) LatestQuote(ctx context.Context, ls *persistence.ListedSecurity) (quote *persistence.Currency, t time.Time, err error) { - return persistence.Value(100), time.Now(), nil -} - -func Test_service_TriggerSecurityQuoteUpdate(t *testing.T) { - RegisterQuoteProvider(QuoteProviderMock, &mockQP{}) - - type fields struct { - db *persistence.DB - } - type args struct { - ctx context.Context - req *connect.Request[portfoliov1.TriggerQuoteUpdateRequest] - } - tests := []struct { - name string - fields fields - args args - wantRes assert.Want[*portfoliov1.TriggerQuoteUpdateResponse] - wantErr bool - }{ - { - name: "happy path", - fields: fields{ - db: internal.NewTestDB(t, func(db *persistence.DB) { - _, err := db.CreateSecurity(context.Background(), persistence.CreateSecurityParams{ - ID: "My Security", - QuoteProvider: sql.NullString{String: QuoteProviderMock, Valid: true}, - }) - assert.NoError(t, err) - _, err = db.UpsertListedSecurity(context.Background(), persistence.UpsertListedSecurityParams{ - SecurityID: "My Security", - Ticker: "SEC", - Currency: currency.EUR.String(), - }) - assert.NoError(t, err) - }), - }, - args: args{ - ctx: context.TODO(), - req: connect.NewRequest(&portfoliov1.TriggerQuoteUpdateRequest{ - SecurityIds: []string{"My Security"}, - }), - }, - wantRes: func(t *testing.T, tqur *portfoliov1.TriggerQuoteUpdateResponse) bool { - return true - }, - }, - } - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - svc := &service{ - db: tt.fields.db, - } - gotRes, err := svc.TriggerSecurityQuoteUpdate(tt.args.ctx, tt.args.req) - if (err != nil) != tt.wantErr { - t.Errorf("service.TriggerSecurityQuoteUpdate() error = %v, wantErr %v", err, tt.wantErr) - return - } - tt.wantRes(t, gotRes.Msg) - }) - } +func (m *mockQP) LatestQuote(ctx context.Context, ls *persistence.ListedSecurity) (quote *currency.Currency, t time.Time, err error) { + return currency.Value(100), time.Now(), nil } type mockQuoteProvider struct{} -func (mockQuoteProvider) LatestQuote(_ context.Context, _ *persistence.ListedSecurity) (quote *persistence.Currency, t time.Time, err error) { - return persistence.Value(100), time.Date(1, 0, 0, 0, 0, 0, 0, time.UTC), nil +func (mockQuoteProvider) LatestQuote(_ context.Context, _ *persistence.ListedSecurity) (quote *currency.Currency, t time.Time, err error) { + return currency.Value(100), time.Date(1, 0, 0, 0, 0, 0, 0, time.UTC), nil } -func Test_service_updateQuote(t *testing.T) { +func Test_qu_updateQuote(t *testing.T) { type fields struct { db *persistence.DB } @@ -133,14 +71,14 @@ func Test_service_updateQuote(t *testing.T) { _, err = db.UpsertListedSecurity(context.Background(), persistence.UpsertListedSecurityParams{ SecurityID: "My Security", Ticker: "SEC", - Currency: currency.EUR.String(), + Currency: "EUR", }) assert.NoError(t, err) }), }, args: args{ qp: &mockQuoteProvider{}, - ls: &persistence.ListedSecurity{SecurityID: "My Security", Ticker: "SEC", Currency: currency.EUR.String()}, + ls: &persistence.ListedSecurity{SecurityID: "My Security", Ticker: "SEC", Currency: "EUR"}, }, want: func(t *testing.T, ls *persistence.ListedSecurity) bool { return assert.Equals(t, 100, int(ls.LatestQuote.Int64)) @@ -149,7 +87,7 @@ func Test_service_updateQuote(t *testing.T) { } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - svc := &service{ + svc := &qu{ db: tt.fields.db, } if err := svc.updateQuote(tt.args.qp, tt.args.ls); (err != nil) != tt.wantErr { diff --git a/securities/securities.go b/securities/securities.go new file mode 100644 index 00000000..48b5a37d --- /dev/null +++ b/securities/securities.go @@ -0,0 +1,13 @@ +package securities + +import ( + "context" + + "github.com/oxisto/money-gopher/persistence" +) + +// SecuritiesLister is the interface for listing securities. +type SecuritiesLister interface { + ListSecurities(ctx context.Context) ([]*persistence.Security, error) + ListSecuritiesByIDs(ctx context.Context, ids []string) ([]*persistence.Security, error) +} diff --git a/server/server.go b/server/server.go index 8485bcef..39642c7e 100644 --- a/server/server.go +++ b/server/server.go @@ -25,11 +25,8 @@ import ( "github.com/99designs/gqlgen/graphql/handler/extension" "github.com/99designs/gqlgen/graphql/handler/transport" "github.com/99designs/gqlgen/graphql/playground" - "github.com/oxisto/money-gopher/gen/portfoliov1connect" "github.com/oxisto/money-gopher/graph" "github.com/oxisto/money-gopher/persistence" - "github.com/oxisto/money-gopher/service/portfolio" - "github.com/oxisto/money-gopher/service/securities" "connectrpc.com/connect" "connectrpc.com/vanguard" diff --git a/service/internal/crud/crud_requests.go b/service/internal/crud/crud_requests.go deleted file mode 100644 index f506e83e..00000000 --- a/service/internal/crud/crud_requests.go +++ /dev/null @@ -1,98 +0,0 @@ -// Copyright 2023 Christian Banse -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. -// -// This file is part of The Money Gopher. - -// package crud contains helpers to handle CRUD (Create, Read, Update and -// Delete) requests that work on [persistence.StorageOperations] in a common -// way. -package crud - -import ( - "fmt" - "log/slog" - "strings" - - "github.com/oxisto/money-gopher/persistence" - - "connectrpc.com/connect" - "google.golang.org/protobuf/types/known/emptypb" -) - -func Create[T any, S persistence.StorageObject](obj S, op persistence.StorageOperations[S], convert func(obj S) *T) (res *connect.Response[T], err error) { - var typ = fmt.Sprintf("%T", obj) - - _, typ, _ = strings.Cut(typ, ".") - typ = strings.ToLower(typ) - - slog.Info( - fmt.Sprintf("Creating %s", typ), - typ, obj, - ) - - // TODO(oxisto): We probably want to have a pure create instead of replace here - err = op.Replace(obj) - if err != nil { - return nil, connect.NewError(connect.CodeInternal, err) - } - - res = connect.NewResponse(convert(obj)) - - return -} - -func List[T any, S persistence.StorageObject](op persistence.StorageOperations[S], setter func(res *connect.Response[T], list []S) error, args ...any) (res *connect.Response[T], err error) { - obj, err := op.List(args...) - if err != nil { - return nil, connect.NewError(connect.CodeInternal, err) - } - - res = connect.NewResponse(new(T)) - err = setter(res, obj) - - return -} - -func Get[T any, S persistence.StorageObject](key any, op persistence.StorageOperations[S], convert func(obj S) *T) (res *connect.Response[T], err error) { - obj, err := op.Get(key) - if err != nil { - return nil, connect.NewError(connect.CodeInternal, err) - } - - res = connect.NewResponse(convert(obj)) - - return -} - -func Update[T any, S persistence.StorageObject](key any, in S, paths []string, op persistence.StorageOperations[S], convert func(obj S) *T) (res *connect.Response[T], err error) { - out, err := op.Update(key, in, paths) - if err != nil { - return nil, connect.NewError(connect.CodeInternal, err) - } - - res = connect.NewResponse(convert(out)) - - return -} - -func Delete[S persistence.StorageObject](key any, op persistence.StorageOperations[S]) (res *connect.Response[emptypb.Empty], err error) { - err = op.Delete(key) - if err != nil { - return nil, connect.NewError(connect.CodeInternal, err) - } - - res = connect.NewResponse(&emptypb.Empty{}) - - return -} diff --git a/service/internal/crud/crud_requests_test.go b/service/internal/crud/crud_requests_test.go deleted file mode 100644 index 5d7c827e..00000000 --- a/service/internal/crud/crud_requests_test.go +++ /dev/null @@ -1,219 +0,0 @@ -// Copyright 2023 Christian Banse -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. -// -// This file is part of The Money Gopher. - -package crud - -import ( - "errors" - "reflect" - "testing" - - portfoliov1 "github.com/oxisto/money-gopher/gen" - "github.com/oxisto/money-gopher/internal" - "github.com/oxisto/money-gopher/persistence" - - "connectrpc.com/connect" - "github.com/oxisto/assert" - "google.golang.org/protobuf/types/known/emptypb" -) - -func TestCreate(t *testing.T) { - type args struct { - obj *portfoliov1.Portfolio - op persistence.StorageOperations[*portfoliov1.Portfolio] - convert func(obj *portfoliov1.Portfolio) *portfoliov1.Portfolio - } - tests := []struct { - name string - args args - wantRes *connect.Response[portfoliov1.Portfolio] - wantErr bool - }{ - { - name: "error", - args: args{ - op: internal.ErrOps[*portfoliov1.Portfolio](errors.New("some-error")), - convert: func(obj *portfoliov1.Portfolio) *portfoliov1.Portfolio { - return obj - }, - }, - wantErr: true, - }, - } - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - gotRes, err := Create(tt.args.obj, tt.args.op, tt.args.convert) - if (err != nil) != tt.wantErr { - t.Errorf("Create() error = %v, wantErr %v", err, tt.wantErr) - return - } - if !reflect.DeepEqual(gotRes, tt.wantRes) { - t.Errorf("Create() = %v, want %v", gotRes, tt.wantRes) - } - }) - } -} - -func TestList(t *testing.T) { - type args struct { - op persistence.StorageOperations[*portfoliov1.Portfolio] - setter func(res *connect.Response[portfoliov1.ListPortfoliosResponse], list []*portfoliov1.Portfolio) error - args []any - } - tests := []struct { - name string - args args - wantRes *connect.Response[portfoliov1.ListPortfoliosResponse] - wantErr bool - }{ - { - name: "error", - args: args{ - op: internal.ErrOps[*portfoliov1.Portfolio](errors.New("some-error")), - setter: func(res *connect.Response[portfoliov1.ListPortfoliosResponse], list []*portfoliov1.Portfolio) error { - res.Msg.Portfolios = list - return nil - }, - args: []any{"some-key"}, - }, - wantErr: true, - }, - } - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - gotRes, err := List(tt.args.op, tt.args.setter, tt.args.args...) - if (err != nil) != tt.wantErr { - t.Errorf("List() error = %v, wantErr %v", err, tt.wantErr) - return - } - if !assert.Equals(t, tt.wantRes, gotRes) { - t.Errorf("List() = %v, want %v", gotRes, tt.wantRes) - } - }) - } -} - -func TestGet(t *testing.T) { - type args struct { - key any - op persistence.StorageOperations[*portfoliov1.Portfolio] - convert func(obj *portfoliov1.Portfolio) *portfoliov1.Portfolio - } - tests := []struct { - name string - args args - wantRes *connect.Response[portfoliov1.Portfolio] - wantErr bool - }{ - { - name: "error", - args: args{ - key: "some-key", - op: internal.ErrOps[*portfoliov1.Portfolio](errors.New("some-error")), - convert: func(obj *portfoliov1.Portfolio) *portfoliov1.Portfolio { - return obj - }, - }, - wantErr: true, - }, - } - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - gotRes, err := Get(tt.args.key, tt.args.op, tt.args.convert) - if (err != nil) != tt.wantErr { - t.Errorf("Get() error = %v, wantErr %v", err, tt.wantErr) - return - } - if !reflect.DeepEqual(gotRes, tt.wantRes) { - t.Errorf("Get() = %v, want %v", gotRes, tt.wantRes) - } - }) - } -} - -func TestUpdate(t *testing.T) { - type args struct { - key any - in *portfoliov1.Portfolio - paths []string - op persistence.StorageOperations[*portfoliov1.Portfolio] - convert func(obj *portfoliov1.Portfolio) *portfoliov1.Portfolio - } - tests := []struct { - name string - args args - wantRes *connect.Response[portfoliov1.Portfolio] - wantErr bool - }{ - { - name: "error", - args: args{ - key: "some-key", - op: internal.ErrOps[*portfoliov1.Portfolio](errors.New("some-error")), - convert: func(obj *portfoliov1.Portfolio) *portfoliov1.Portfolio { - return obj - }, - }, - wantErr: true, - }, - } - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - gotRes, err := Update(tt.args.key, tt.args.in, tt.args.paths, tt.args.op, tt.args.convert) - if (err != nil) != tt.wantErr { - t.Errorf("Update() error = %v, wantErr %v", err, tt.wantErr) - return - } - if !reflect.DeepEqual(gotRes, tt.wantRes) { - t.Errorf("Update() = %v, want %v", gotRes, tt.wantRes) - } - }) - } -} - -func TestDelete(t *testing.T) { - type args struct { - key any - op persistence.StorageOperations[*portfoliov1.Portfolio] - } - tests := []struct { - name string - args args - wantRes *connect.Response[emptypb.Empty] - wantErr bool - }{ - { - name: "error", - args: args{ - key: "some-key", - op: internal.ErrOps[*portfoliov1.Portfolio](errors.New("some-error")), - }, - wantErr: true, - }, - } - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - gotRes, err := Delete(tt.args.key, tt.args.op) - if (err != nil) != tt.wantErr { - t.Errorf("Delete() error = %v, wantErr %v", err, tt.wantErr) - return - } - if !reflect.DeepEqual(gotRes, tt.wantRes) { - t.Errorf("Delete() = %v, want %v", gotRes, tt.wantRes) - } - }) - } -} diff --git a/service/portfolio/account.go b/service/portfolio/account.go deleted file mode 100644 index 8c2b206a..00000000 --- a/service/portfolio/account.go +++ /dev/null @@ -1,55 +0,0 @@ -// Copyright 2023 Christian Banse -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. -// -// This file is part of The Money Gopher. - -package portfolio - -import ( - "context" - - portfoliov1 "github.com/oxisto/money-gopher/gen" - - "connectrpc.com/connect" - "github.com/oxisto/money-gopher/service/internal/crud" - "google.golang.org/protobuf/types/known/emptypb" -) - -var bankAccountSetter = func(obj *portfoliov1.BankAccount) *portfoliov1.BankAccount { - return obj -} - -func (svc *service) CreateBankAccount(ctx context.Context, req *connect.Request[portfoliov1.CreateBankAccountRequest]) (res *connect.Response[portfoliov1.BankAccount], err error) { - return crud.Create( - req.Msg.BankAccount, - svc.bankAccounts, - bankAccountSetter, - ) -} - -func (svc *service) UpdateBankAccount(ctx context.Context, req *connect.Request[portfoliov1.UpdateBankAccountRequest]) (res *connect.Response[portfoliov1.BankAccount], err error) { - return crud.Update( - req.Msg.Account.Id, - req.Msg.Account, - req.Msg.UpdateMask.Paths, - svc.bankAccounts, - func(obj *portfoliov1.BankAccount) *portfoliov1.BankAccount { - return obj - }, - ) -} - -func (svc *service) DeleteBankAccount(ctx context.Context, req *connect.Request[portfoliov1.DeleteBankAccountRequest]) (res *connect.Response[emptypb.Empty], err error) { - return crud.Delete(req.Msg.Id, svc.bankAccounts) -} diff --git a/service/portfolio/account_test.go b/service/portfolio/account_test.go deleted file mode 100644 index 3000af35..00000000 --- a/service/portfolio/account_test.go +++ /dev/null @@ -1,197 +0,0 @@ -// Copyright 2023 Christian Banse -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. -// -// This file is part of The Money Gopher. - -package portfolio - -import ( - "context" - "testing" - - portfoliov1 "github.com/oxisto/money-gopher/gen" - "github.com/oxisto/money-gopher/gen/portfoliov1connect" - "github.com/oxisto/money-gopher/internal" - "github.com/oxisto/money-gopher/persistence" - - "connectrpc.com/connect" - "github.com/oxisto/assert" - "google.golang.org/protobuf/types/known/fieldmaskpb" -) - -func Test_service_CreateBankAccount(t *testing.T) { - type fields struct { - portfolios persistence.StorageOperations[*portfoliov1.Portfolio] - bankAccounts persistence.StorageOperations[*portfoliov1.BankAccount] - securities portfoliov1connect.SecuritiesServiceClient - } - type args struct { - ctx context.Context - req *connect.Request[portfoliov1.CreateBankAccountRequest] - } - tests := []struct { - name string - fields fields - args args - wantRes assert.Want[*connect.Response[portfoliov1.BankAccount]] - wantSvc assert.Want[*service] - wantErr bool - }{ - { - name: "happy path", - fields: fields{ - portfolios: internal.NewTestDBOps[*portfoliov1.Portfolio](t), - bankAccounts: internal.NewTestDBOps[*portfoliov1.BankAccount](t), - }, - args: args{ - req: connect.NewRequest(&portfoliov1.CreateBankAccountRequest{ - BankAccount: &portfoliov1.BankAccount{ - Id: "mybank-mycash", - DisplayName: "My Cash Account", - }, - }), - }, - wantRes: func(t *testing.T, r *connect.Response[portfoliov1.BankAccount]) bool { - return true && - assert.Equals(t, "mybank-mycash", r.Msg.Id) && - assert.Equals(t, "My Cash Account", r.Msg.DisplayName) - }, - wantSvc: func(t *testing.T, s *service) bool { - list, _ := s.bankAccounts.List() - return assert.Equals(t, 1, len(list)) - }, - }, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - svc := &service{ - portfolios: tt.fields.portfolios, - events: persistence.Relationship[*portfoliov1.PortfolioEvent](tt.fields.portfolios), - bankAccounts: tt.fields.bankAccounts, - securities: tt.fields.securities, - } - gotRes, err := svc.CreateBankAccount(tt.args.ctx, tt.args.req) - if (err != nil) != tt.wantErr { - t.Errorf("service.CreateBankAccount() error = %v, wantErr %v", err, tt.wantErr) - return - } - tt.wantRes(t, gotRes) - tt.wantSvc(t, svc) - }) - } -} - -func Test_service_UpdateBankAccount(t *testing.T) { - type fields struct { - portfolios persistence.StorageOperations[*portfoliov1.Portfolio] - bankAccounts persistence.StorageOperations[*portfoliov1.BankAccount] - } - type args struct { - ctx context.Context - req *connect.Request[portfoliov1.UpdateBankAccountRequest] - } - tests := []struct { - name string - fields fields - args args - wantRes assert.Want[*connect.Response[portfoliov1.BankAccount]] - wantErr bool - }{ - { - name: "happy path", - fields: fields{ - portfolios: myPortfolio(t), - bankAccounts: myCash(t), - }, - args: args{ - req: connect.NewRequest(&portfoliov1.UpdateBankAccountRequest{ - Account: &portfoliov1.BankAccount{ - Id: "mybank-mycash", - DisplayName: "My Cash", - }, - UpdateMask: &fieldmaskpb.FieldMask{Paths: []string{"display_name"}}, - }), - }, - wantRes: func(t *testing.T, r *connect.Response[portfoliov1.BankAccount]) bool { - return assert.Equals(t, "My Cash", r.Msg.DisplayName) - }, - }, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - svc := &service{ - portfolios: tt.fields.portfolios, - bankAccounts: tt.fields.bankAccounts, - } - gotRes, err := svc.UpdateBankAccount(tt.args.ctx, tt.args.req) - if (err != nil) != tt.wantErr { - t.Errorf("service.UpdateBankAccount() error = %v, wantErr %v", err, tt.wantErr) - return - } - tt.wantRes(t, gotRes) - }) - } -} - -func Test_service_DeleteBankAccount(t *testing.T) { - type fields struct { - portfolios persistence.StorageOperations[*portfoliov1.Portfolio] - bankAccounts persistence.StorageOperations[*portfoliov1.BankAccount] - } - type args struct { - ctx context.Context - req *connect.Request[portfoliov1.DeleteBankAccountRequest] - } - tests := []struct { - name string - fields fields - args args - wantSvc assert.Want[*service] - wantErr bool - }{ - { - name: "happy path", - fields: fields{ - portfolios: myPortfolio(t), - bankAccounts: myCash(t), - }, - args: args{ - req: connect.NewRequest(&portfoliov1.DeleteBankAccountRequest{ - Id: "mybank-mycash", - }), - }, - wantSvc: func(t *testing.T, s *service) bool { - list, _ := s.bankAccounts.List() - return assert.Equals(t, 0, len(list)) - }, - }, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - svc := &service{ - portfolios: tt.fields.portfolios, - bankAccounts: tt.fields.bankAccounts, - } - _, err := svc.DeleteBankAccount(tt.args.ctx, tt.args.req) - if (err != nil) != tt.wantErr { - t.Errorf("service.DeleteBankAccount() error = %v, wantErr %v", err, tt.wantErr) - return - } - tt.wantSvc(t, svc) - }) - } -} diff --git a/service/portfolio/portfolio.go b/service/portfolio/portfolio.go deleted file mode 100644 index 95ad373a..00000000 --- a/service/portfolio/portfolio.go +++ /dev/null @@ -1,88 +0,0 @@ -// Copyright 2023 Christian Banse -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. -// -// This file is part of The Money Gopher. - -package portfolio - -import ( - "context" - - portfoliov1 "github.com/oxisto/money-gopher/gen" - "github.com/oxisto/money-gopher/service/internal/crud" - - "connectrpc.com/connect" - "google.golang.org/protobuf/types/known/emptypb" -) - -var portfolioSetter = func(obj *portfoliov1.Portfolio) *portfoliov1.Portfolio { - return obj -} - -func (svc *service) CreatePortfolio(ctx context.Context, req *connect.Request[portfoliov1.CreatePortfolioRequest]) (res *connect.Response[portfoliov1.Portfolio], err error) { - return crud.Create( - req.Msg.Portfolio, - svc.portfolios, - portfolioSetter, - ) -} - -func (svc *service) ListPortfolios(ctx context.Context, req *connect.Request[portfoliov1.ListPortfoliosRequest]) (res *connect.Response[portfoliov1.ListPortfoliosResponse], err error) { - return crud.List( - svc.portfolios, - func( - res *connect.Response[portfoliov1.ListPortfoliosResponse], - list []*portfoliov1.Portfolio, - ) error { - res.Msg.Portfolios = list - - for _, p := range res.Msg.Portfolios { - p.Events, err = svc.events.List(p.Id) - if err != nil { - return err - } - } - - return nil - }, - ) -} - -func (svc *service) GetPortfolio(ctx context.Context, req *connect.Request[portfoliov1.GetPortfolioRequest]) (res *connect.Response[portfoliov1.Portfolio], err error) { - return crud.Get( - req.Msg.Id, - svc.portfolios, - func(obj *portfoliov1.Portfolio) *portfoliov1.Portfolio { - obj.Events, _ = svc.events.List(obj.Id) - - return obj - }, - ) -} - -func (svc *service) UpdatePortfolio(ctx context.Context, req *connect.Request[portfoliov1.UpdatePortfolioRequest]) (res *connect.Response[portfoliov1.Portfolio], err error) { - return crud.Update( - req.Msg.Portfolio.Id, - req.Msg.Portfolio, - req.Msg.UpdateMask.Paths, - svc.portfolios, - func(obj *portfoliov1.Portfolio) *portfoliov1.Portfolio { - return obj - }, - ) -} - -func (svc *service) DeletePortfolio(ctx context.Context, req *connect.Request[portfoliov1.DeletePortfolioRequest]) (res *connect.Response[emptypb.Empty], err error) { - return crud.Delete(req.Msg.Id, svc.portfolios) -} diff --git a/service/portfolio/portfolio_test.go b/service/portfolio/portfolio_test.go deleted file mode 100644 index 255b9aba..00000000 --- a/service/portfolio/portfolio_test.go +++ /dev/null @@ -1,369 +0,0 @@ -// Copyright 2023 Christian Banse -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. -// -// This file is part of The Money Gopher. - -package portfolio - -import ( - "context" - "testing" - "time" - - portfoliov1 "github.com/oxisto/money-gopher/gen" - "github.com/oxisto/money-gopher/gen/portfoliov1connect" - "github.com/oxisto/money-gopher/internal" - "github.com/oxisto/money-gopher/persistence" - - "connectrpc.com/connect" - "github.com/oxisto/assert" - "google.golang.org/protobuf/types/known/fieldmaskpb" - "google.golang.org/protobuf/types/known/timestamppb" -) - -func myPortfolio(t *testing.T) persistence.StorageOperations[*portfoliov1.Portfolio] { - return internal.NewTestDBOps(t, func(ops persistence.StorageOperations[*portfoliov1.Portfolio]) { - assert.NoError(t, ops.Replace(&portfoliov1.Portfolio{ - Id: "mybank-myportfolio", - DisplayName: "My Portfolio", - })) - rel := persistence.Relationship[*portfoliov1.PortfolioEvent](ops) - assert.NoError(t, rel.Replace(&portfoliov1.PortfolioEvent{ - Id: "buy", - Type: portfoliov1.PortfolioEventType_PORTFOLIO_EVENT_TYPE_BUY, - PortfolioId: "mybank-myportfolio", - SecurityId: "US0378331005", - Amount: 20, - Price: portfoliov1.Value(10708), - Fees: portfoliov1.Value(1025), - Time: timestamppb.New(time.Date(2020, 1, 1, 0, 0, 0, 0, time.UTC)), - })) - assert.NoError(t, rel.Replace(&portfoliov1.PortfolioEvent{ - Id: "sell", - Type: portfoliov1.PortfolioEventType_PORTFOLIO_EVENT_TYPE_SELL, - PortfolioId: "mybank-myportfolio", - SecurityId: "US0378331005", - Amount: 10, - Price: portfoliov1.Value(14588), - Fees: portfoliov1.Value(855), - Time: timestamppb.New(time.Date(2021, 1, 1, 0, 0, 0, 0, time.UTC)), - })) - }) -} - -func myCash(t *testing.T) persistence.StorageOperations[*portfoliov1.BankAccount] { - return internal.NewTestDBOps(t, func(ops persistence.StorageOperations[*portfoliov1.BankAccount]) { - assert.NoError(t, ops.Replace(&portfoliov1.BankAccount{ - Id: "mybank-mycash", - DisplayName: "My Cash", - })) - }) -} - -func zeroPositions(t *testing.T) persistence.StorageOperations[*portfoliov1.Portfolio] { - return internal.NewTestDBOps(t, func(ops persistence.StorageOperations[*portfoliov1.Portfolio]) { - assert.NoError(t, ops.Replace(&portfoliov1.Portfolio{ - Id: "mybank-myportfolio", - DisplayName: "My Portfolio", - })) - rel := persistence.Relationship[*portfoliov1.PortfolioEvent](ops) - assert.NoError(t, rel.Replace(&portfoliov1.PortfolioEvent{ - Id: "buy", - Type: portfoliov1.PortfolioEventType_PORTFOLIO_EVENT_TYPE_BUY, - PortfolioId: "mybank-myportfolio", - SecurityId: "sec123", - Amount: 10, - Price: portfoliov1.Value(10000), - Fees: portfoliov1.Zero(), - Time: timestamppb.New(time.Date(2023, 1, 1, 0, 0, 0, 0, time.UTC)), - })) - assert.NoError(t, rel.Replace(&portfoliov1.PortfolioEvent{ - Id: "sell", - Type: portfoliov1.PortfolioEventType_PORTFOLIO_EVENT_TYPE_SELL, - PortfolioId: "mybank-myportfolio", - SecurityId: "sec123", - Amount: 10, - Price: portfoliov1.Value(10000), - Fees: portfoliov1.Zero(), - Time: timestamppb.New(time.Date(2023, 1, 2, 0, 0, 0, 0, time.UTC)), - })) - }) -} - -func emptyPortfolio(t *testing.T) persistence.StorageOperations[*portfoliov1.Portfolio] { - return internal.NewTestDBOps(t, func(ops persistence.StorageOperations[*portfoliov1.Portfolio]) { - assert.NoError(t, ops.Replace(&portfoliov1.Portfolio{ - Id: "mybank-myportfolio", - DisplayName: "My Portfolio", - })) - }) -} - -func Test_service_CreatePortfolio(t *testing.T) { - type fields struct { - portfolios persistence.StorageOperations[*portfoliov1.Portfolio] - securities portfoliov1connect.SecuritiesServiceClient - } - type args struct { - ctx context.Context - req *connect.Request[portfoliov1.CreatePortfolioRequest] - } - tests := []struct { - name string - fields fields - args args - wantRes assert.Want[*connect.Response[portfoliov1.Portfolio]] - wantSvc assert.Want[*service] - wantErr bool - }{ - { - name: "happy path", - fields: fields{ - portfolios: internal.NewTestDBOps[*portfoliov1.Portfolio](t), - }, - args: args{ - req: connect.NewRequest(&portfoliov1.CreatePortfolioRequest{ - Portfolio: &portfoliov1.Portfolio{ - Id: "mybank-myportfolio", - DisplayName: "My Portfolio", - }, - }), - }, - wantRes: func(t *testing.T, r *connect.Response[portfoliov1.Portfolio]) bool { - return true && - assert.Equals(t, "mybank-myportfolio", r.Msg.Id) && - assert.Equals(t, "My Portfolio", r.Msg.DisplayName) - }, - wantSvc: func(t *testing.T, s *service) bool { - list, _ := s.portfolios.List() - return assert.Equals(t, 1, len(list)) - }, - }, - } - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - svc := &service{ - portfolios: tt.fields.portfolios, - events: persistence.Relationship[*portfoliov1.PortfolioEvent](tt.fields.portfolios), - securities: tt.fields.securities, - } - gotRes, err := svc.CreatePortfolio(tt.args.ctx, tt.args.req) - if (err != nil) != tt.wantErr { - t.Errorf("service.CreatePortfolio() error = %v, wantErr %v", err, tt.wantErr) - return - } - tt.wantRes(t, gotRes) - tt.wantSvc(t, svc) - }) - } -} - -func Test_service_ListPortfolios(t *testing.T) { - type fields struct { - portfolios persistence.StorageOperations[*portfoliov1.Portfolio] - securities portfoliov1connect.SecuritiesServiceClient - } - type args struct { - ctx context.Context - req *connect.Request[portfoliov1.ListPortfoliosRequest] - } - tests := []struct { - name string - fields fields - args args - wantRes assert.Want[*connect.Response[portfoliov1.ListPortfoliosResponse]] - wantErr bool - }{ - { - name: "happy path", - fields: fields{ - portfolios: myPortfolio(t), - }, - wantRes: func(t *testing.T, r *connect.Response[portfoliov1.ListPortfoliosResponse]) bool { - return true && - assert.Equals(t, "mybank-myportfolio", r.Msg.Portfolios[0].Id) && - assert.Equals(t, "My Portfolio", r.Msg.Portfolios[0].DisplayName) && - assert.Equals(t, 2, len(r.Msg.Portfolios[0].Events)) - }, - }, - } - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - svc := &service{ - portfolios: tt.fields.portfolios, - events: persistence.Relationship[*portfoliov1.PortfolioEvent](tt.fields.portfolios), - securities: tt.fields.securities, - } - gotRes, err := svc.ListPortfolios(tt.args.ctx, tt.args.req) - if (err != nil) != tt.wantErr { - t.Errorf("service.ListPortfolios() error = %v, wantErr %v", err, tt.wantErr) - return - } - tt.wantRes(t, gotRes) - }) - } -} - -func Test_service_GetPortfolio(t *testing.T) { - type fields struct { - portfolios persistence.StorageOperations[*portfoliov1.Portfolio] - securities portfoliov1connect.SecuritiesServiceClient - } - type args struct { - ctx context.Context - req *connect.Request[portfoliov1.GetPortfolioRequest] - } - tests := []struct { - name string - fields fields - args args - wantRes assert.Want[*connect.Response[portfoliov1.Portfolio]] - wantErr bool - }{ - { - name: "happy path", - fields: fields{ - portfolios: myPortfolio(t), - }, - args: args{ - req: connect.NewRequest(&portfoliov1.GetPortfolioRequest{ - Id: "mybank-myportfolio", - }), - }, - wantRes: func(t *testing.T, r *connect.Response[portfoliov1.Portfolio]) bool { - return true && - assert.Equals(t, 2, len(r.Msg.Events)) - }, - }, - } - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - svc := &service{ - portfolios: tt.fields.portfolios, - events: persistence.Relationship[*portfoliov1.PortfolioEvent](tt.fields.portfolios), - securities: tt.fields.securities, - } - gotRes, err := svc.GetPortfolio(tt.args.ctx, tt.args.req) - if (err != nil) != tt.wantErr { - t.Errorf("service.GetPortfolio() error = %v, wantErr %v", err, tt.wantErr) - return - } - tt.wantRes(t, gotRes) - }) - } -} - -func Test_service_UpdatePortfolio(t *testing.T) { - type fields struct { - portfolios persistence.StorageOperations[*portfoliov1.Portfolio] - securities portfoliov1connect.SecuritiesServiceClient - } - type args struct { - ctx context.Context - req *connect.Request[portfoliov1.UpdatePortfolioRequest] - } - tests := []struct { - name string - fields fields - args args - wantRes assert.Want[*connect.Response[portfoliov1.Portfolio]] - wantErr bool - }{ - { - name: "happy path", - fields: fields{ - portfolios: myPortfolio(t), - }, - args: args{ - req: connect.NewRequest(&portfoliov1.UpdatePortfolioRequest{ - Portfolio: &portfoliov1.Portfolio{ - Id: "mybank-myportfolio", - DisplayName: "My Second Portfolio", - }, - UpdateMask: &fieldmaskpb.FieldMask{Paths: []string{"display_name"}}, - }), - }, - wantRes: func(t *testing.T, r *connect.Response[portfoliov1.Portfolio]) bool { - return assert.Equals(t, "My Second Portfolio", r.Msg.DisplayName) - }, - }, - } - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - svc := &service{ - portfolios: tt.fields.portfolios, - events: persistence.Relationship[*portfoliov1.PortfolioEvent](tt.fields.portfolios), - securities: tt.fields.securities, - } - gotRes, err := svc.UpdatePortfolio(tt.args.ctx, tt.args.req) - if (err != nil) != tt.wantErr { - t.Errorf("service.UpdatePortfolio() error = %v, wantErr %v", err, tt.wantErr) - return - } - tt.wantRes(t, gotRes) - }) - } -} - -func Test_service_DeletePortfolio(t *testing.T) { - type fields struct { - portfolios persistence.StorageOperations[*portfoliov1.Portfolio] - events persistence.StorageOperations[*portfoliov1.PortfolioEvent] - securities portfoliov1connect.SecuritiesServiceClient - UnimplementedPortfolioServiceHandler portfoliov1connect.UnimplementedPortfolioServiceHandler - } - type args struct { - ctx context.Context - req *connect.Request[portfoliov1.DeletePortfolioRequest] - } - tests := []struct { - name string - fields fields - args args - wantSvc assert.Want[*service] - wantErr bool - }{ - { - name: "happy path", - fields: fields{ - portfolios: myPortfolio(t), - }, - args: args{ - req: connect.NewRequest(&portfoliov1.DeletePortfolioRequest{ - Id: "mybank-myportfolio", - }), - }, - wantSvc: func(t *testing.T, s *service) bool { - list, _ := s.portfolios.List() - return assert.Equals(t, 0, len(list)) - }, - }, - } - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - svc := &service{ - portfolios: tt.fields.portfolios, - events: tt.fields.events, - securities: tt.fields.securities, - UnimplementedPortfolioServiceHandler: tt.fields.UnimplementedPortfolioServiceHandler, - } - _, err := svc.DeletePortfolio(tt.args.ctx, tt.args.req) - if (err != nil) != tt.wantErr { - t.Errorf("service.DeletePortfolio() error = %v, wantErr %v", err, tt.wantErr) - return - } - tt.wantSvc(t, svc) - }) - } -} diff --git a/service/portfolio/service.go b/service/portfolio/service.go deleted file mode 100644 index eb837d83..00000000 --- a/service/portfolio/service.go +++ /dev/null @@ -1,71 +0,0 @@ -// Copyright 2023 Christian Banse -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. -// -// This file is part of The Money Gopher. - -// package portfolio contains the code for the PortfolioService implementation. -package portfolio - -import ( - "net/http" - - portfoliov1 "github.com/oxisto/money-gopher/gen" - "github.com/oxisto/money-gopher/gen/portfoliov1connect" - "github.com/oxisto/money-gopher/persistence" -) - -const DefaultSecuritiesServiceURL = "http://localhost:8080" - -// service is the main struct fo the [PortfolioService] implementation. -type service struct { - portfolios persistence.StorageOperations[*portfoliov1.Portfolio] - events persistence.StorageOperations[*portfoliov1.PortfolioEvent] - bankAccounts persistence.StorageOperations[*portfoliov1.BankAccount] - securities portfoliov1connect.SecuritiesServiceClient - - portfoliov1connect.UnimplementedPortfolioServiceHandler -} - -type Options struct { - SecuritiesClient portfoliov1connect.SecuritiesServiceClient - DB *persistence.DB -} - -func NewService(opts Options) portfoliov1connect.PortfolioServiceHandler { - var s service - - s.portfolios = persistence.Ops[*portfoliov1.Portfolio](opts.DB) - s.events = persistence.Relationship[*portfoliov1.PortfolioEvent](s.portfolios) - s.bankAccounts = persistence.Ops[*portfoliov1.BankAccount](opts.DB) - - s.securities = opts.SecuritiesClient - if s.securities == nil { - s.securities = portfoliov1connect.NewSecuritiesServiceClient(http.DefaultClient, DefaultSecuritiesServiceURL) - } - - // Add a simple starter portfolio - s.portfolios.Replace(&portfoliov1.Portfolio{ - Id: "mybank-myportfolio", - DisplayName: "My Portfolio", - BankAccountId: "mybank-mycash", - }) - - // Add its cash account - s.bankAccounts.Replace(&portfoliov1.BankAccount{ - Id: "mybank-mycash", - DisplayName: "My Cash Account", - }) - - return &s -} diff --git a/service/portfolio/service_test.go b/service/portfolio/service_test.go deleted file mode 100644 index b372883d..00000000 --- a/service/portfolio/service_test.go +++ /dev/null @@ -1,54 +0,0 @@ -// Copyright 2023 Christian Banse -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. -// -// This file is part of The Money Gopher. - -package portfolio - -import ( - "testing" - - "github.com/oxisto/money-gopher/gen/portfoliov1connect" - "github.com/oxisto/money-gopher/internal" - - "github.com/oxisto/assert" -) - -func TestNewService(t *testing.T) { - type args struct { - opts Options - } - tests := []struct { - name string - args args - want assert.Want[portfoliov1connect.PortfolioServiceHandler] - }{ - { - name: "with default client", - args: args{opts: Options{ - DB: internal.NewTestDB(t), - }}, - want: func(t *testing.T, psh portfoliov1connect.PortfolioServiceHandler) bool { - s := assert.Is[*service](t, psh) - return assert.NotNil(t, s.securities) - }, - }, - } - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - got := NewService(tt.args.opts) - tt.want(t, got) - }) - } -} diff --git a/service/portfolio/snapshot.go b/service/portfolio/snapshot.go deleted file mode 100644 index b69996f6..00000000 --- a/service/portfolio/snapshot.go +++ /dev/null @@ -1,160 +0,0 @@ -// Copyright 2023 Christian Banse -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. -// -// This file is part of The Money Gopher. - -package portfolio - -import ( - "context" - "fmt" - "maps" - "slices" - - moneygopher "github.com/oxisto/money-gopher" - "github.com/oxisto/money-gopher/finance" - portfoliov1 "github.com/oxisto/money-gopher/gen" - - "connectrpc.com/connect" - "google.golang.org/protobuf/types/known/timestamppb" -) - -func (svc *service) GetPortfolioSnapshot(ctx context.Context, req *connect.Request[portfoliov1.GetPortfolioSnapshotRequest]) (res *connect.Response[portfoliov1.PortfolioSnapshot], err error) { - var ( - snap *portfoliov1.PortfolioSnapshot - p portfoliov1.Portfolio - m map[string][]*portfoliov1.PortfolioEvent - names []string - secres *connect.Response[portfoliov1.ListSecuritiesResponse] - secmap map[string]*portfoliov1.Security - ) - - // Retrieve transactions - p.Events, err = svc.events.List(req.Msg.PortfolioId) - if err != nil { - return nil, connect.NewError(connect.CodeInternal, err) - } - - // If no time is specified, we assume it to be now - if req.Msg.Time == nil { - req.Msg.Time = timestamppb.Now() - } - - // Set up the snapshot - snap = &portfoliov1.PortfolioSnapshot{ - Time: req.Msg.Time, - Positions: make(map[string]*portfoliov1.PortfolioPosition), - TotalPurchaseValue: portfoliov1.Zero(), - TotalMarketValue: portfoliov1.Zero(), - TotalProfitOrLoss: portfoliov1.Zero(), - Cash: portfoliov1.Zero(), - } - - // Record the first transaction time - if len(p.Events) > 0 { - snap.FirstTransactionTime = p.Events[0].Time - } - - // Retrieve the event map; a map of events indexed by their security ID - m = p.EventMap() - names = slices.Collect(maps.Keys(m)) - - // Retrieve market value of filtered securities - secres, err = svc.securities.ListSecurities( - context.Background(), - forwardAuth(connect.NewRequest(&portfoliov1.ListSecuritiesRequest{ - Filter: &portfoliov1.ListSecuritiesRequest_Filter{ - SecurityIds: names, - }, - }), req), - ) - if err != nil { - return nil, connect.NewError(connect.CodeInternal, - fmt.Errorf("internal error while calling ListSecurities on securities service: %w", err), - ) - } - - // Make a map out of the securities list so we can access it easier - secmap = moneygopher.Map(secres.Msg.Securities, func(s *portfoliov1.Security) string { - return s.Id - }) - - // We need to look at the portfolio events up to the time of the snapshot - // and calculate the current positions. - for name, txs := range m { - txs = portfoliov1.EventsBefore(txs, snap.Time.AsTime()) - - c := finance.NewCalculation(txs) - - if name == "cash" { - // Add deposited/withdrawn cash directly - snap.Cash.PlusAssign(c.Cash) - continue - } - - if c.Amount == 0 { - continue - } - - // Also add cash that is part of a securities' transaction (e.g., sell/buy) - snap.Cash.PlusAssign(c.Cash) - - pos := &portfoliov1.PortfolioPosition{ - Security: secmap[name], - Amount: c.Amount, - PurchaseValue: c.NetValue(), - PurchasePrice: c.NetPrice(), - MarketValue: portfoliov1.Times(marketPrice(secmap, name, c.NetPrice()), c.Amount), - MarketPrice: marketPrice(secmap, name, c.NetPrice()), - } - - // Calculate loss and gains - pos.ProfitOrLoss = portfoliov1.Minus(pos.MarketValue, pos.PurchaseValue) - pos.Gains = float64(portfoliov1.Minus(pos.MarketValue, pos.PurchaseValue).Value) / float64(pos.PurchaseValue.Value) - - // Add to total value(s) - snap.TotalPurchaseValue.PlusAssign(pos.PurchaseValue) - snap.TotalMarketValue.PlusAssign(pos.MarketValue) - snap.TotalProfitOrLoss.PlusAssign(pos.ProfitOrLoss) - - // Store position in map - snap.Positions[name] = pos - } - - // Calculate total gains - snap.TotalGains = float64(portfoliov1.Minus(snap.TotalMarketValue, snap.TotalPurchaseValue).Value) / float64(snap.TotalPurchaseValue.Value) - - // Calculate total portfolio value - snap.TotalPortfolioValue = snap.TotalMarketValue.Plus(snap.Cash) - - return connect.NewResponse(snap), nil -} - -func marketPrice(secmap map[string]*portfoliov1.Security, name string, netPrice *portfoliov1.Currency) *portfoliov1.Currency { - ls := secmap[name].ListedOn - - if ls == nil || ls[0].LatestQuote == nil { - return netPrice - } else { - return ls[0].LatestQuote - } -} - -// forwardAuth uses the authorization header of [authenticatedReq] to -// authenticate [req]. This is a little workaround, until we have proper -// service-to-service authentication. -func forwardAuth[T any, S any](req *connect.Request[T], authenticatedReq *connect.Request[S]) *connect.Request[T] { - req.Header().Set("Authorization", authenticatedReq.Header().Get("Authorization")) - return req -} diff --git a/service/portfolio/snapshot_test.go b/service/portfolio/snapshot_test.go deleted file mode 100644 index 037615ef..00000000 --- a/service/portfolio/snapshot_test.go +++ /dev/null @@ -1,211 +0,0 @@ -// Copyright 2023 Christian Banse -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. -// -// This file is part of The Money Gopher. - -package portfolio - -import ( - "context" - "io" - "testing" - "time" - - "github.com/oxisto/money-gopher/gen/portfoliov1connect" - "github.com/oxisto/money-gopher/internal" - "github.com/oxisto/money-gopher/persistence" - - "connectrpc.com/connect" - "github.com/oxisto/assert" - portfoliov1 "github.com/oxisto/money-gopher/gen" - "golang.org/x/text/currency" - "google.golang.org/protobuf/testing/protocmp" - "google.golang.org/protobuf/types/known/emptypb" - "google.golang.org/protobuf/types/known/timestamppb" -) - -var mockSecuritiesClientWithData = &mockSecuritiesClient{ - securities: []*portfoliov1.Security{ - { - Id: "US0378331005", - DisplayName: "Apple, Inc.", - ListedOn: []*portfoliov1.ListedSecurity{ - { - SecurityId: "US0378331005", - Ticker: "APC.F", - Currency: currency.EUR.String(), - LatestQuote: portfoliov1.Value(10000), - LatestQuoteTimestamp: timestamppb.Now(), - }, - }, - }, - }, -} - -func Test_service_GetPortfolioSnapshot(t *testing.T) { - type fields struct { - portfolios persistence.StorageOperations[*portfoliov1.Portfolio] - events persistence.StorageOperations[*portfoliov1.PortfolioEvent] - securities portfoliov1connect.SecuritiesServiceClient - } - type args struct { - ctx context.Context - req *connect.Request[portfoliov1.GetPortfolioSnapshotRequest] - } - tests := []struct { - name string - fields fields - args args - wantRes assert.Want[*connect.Response[portfoliov1.PortfolioSnapshot]] - wantErr bool - }{ - { - name: "happy path, now", - fields: fields{ - portfolios: myPortfolio(t), - securities: mockSecuritiesClientWithData, - }, - args: args{req: connect.NewRequest(&portfoliov1.GetPortfolioSnapshotRequest{ - PortfolioId: "mybank-myportfolio", - })}, - wantRes: func(t *testing.T, r *connect.Response[portfoliov1.PortfolioSnapshot]) bool { - return true && - assert.Equals(t, "US0378331005", r.Msg.Positions["US0378331005"].Security.Id) && - assert.Equals(t, 10, r.Msg.Positions["US0378331005"].Amount) && - assert.Equals(t, portfoliov1.Value(107080), r.Msg.Positions["US0378331005"].PurchaseValue, protocmp.Transform()) && - assert.Equals(t, portfoliov1.Value(10708), r.Msg.Positions["US0378331005"].PurchasePrice, protocmp.Transform()) && - assert.Equals(t, portfoliov1.Value(100000), r.Msg.TotalMarketValue, protocmp.Transform()) - }, - }, - { - name: "happy path, before sell", - fields: fields{ - portfolios: myPortfolio(t), - securities: mockSecuritiesClientWithData, - }, - args: args{req: connect.NewRequest(&portfoliov1.GetPortfolioSnapshotRequest{ - PortfolioId: "mybank-myportfolio", - Time: timestamppb.New(time.Date(2020, 1, 1, 0, 0, 0, 1, time.UTC)), - })}, - wantRes: func(t *testing.T, r *connect.Response[portfoliov1.PortfolioSnapshot]) bool { - pos := r.Msg.Positions["US0378331005"] - - return true && - assert.Equals(t, "US0378331005", pos.Security.Id) && - assert.Equals(t, 20, pos.Amount) && - assert.Equals(t, portfoliov1.Value(214160), pos.PurchaseValue, protocmp.Transform()) && - assert.Equals(t, portfoliov1.Value(10708), pos.PurchasePrice, protocmp.Transform()) && - assert.Equals(t, portfoliov1.Value(10000), pos.MarketPrice, protocmp.Transform()) && - assert.Equals(t, portfoliov1.Value(200000), pos.MarketValue, protocmp.Transform()) - }, - }, - { - name: "happy path, position zero'd out", - fields: fields{ - portfolios: zeroPositions(t), - securities: mockSecuritiesClientWithData, - }, - args: args{req: connect.NewRequest(&portfoliov1.GetPortfolioSnapshotRequest{ - PortfolioId: "mybank-myportfolio", - Time: timestamppb.New(time.Date(2020, 1, 1, 0, 0, 0, 1, time.UTC)), - })}, - wantRes: func(t *testing.T, r *connect.Response[portfoliov1.PortfolioSnapshot]) bool { - return true && - len(r.Msg.Positions) == 0 - }, - }, - { - name: "events list error", - fields: fields{ - portfolios: emptyPortfolio(t), - events: internal.ErrOps[*portfoliov1.PortfolioEvent](io.EOF), - securities: &mockSecuritiesClient{listSecuritiesError: io.EOF}, - }, - args: args{req: connect.NewRequest(&portfoliov1.GetPortfolioSnapshotRequest{ - PortfolioId: "mybank-myportfolio", - Time: timestamppb.New(time.Date(2020, 1, 1, 0, 0, 0, 1, time.UTC)), - })}, - wantErr: true, - wantRes: func(t *testing.T, r *connect.Response[portfoliov1.PortfolioSnapshot]) bool { - return true - }, - }, - { - name: "securities list error", - fields: fields{ - portfolios: myPortfolio(t), - securities: &mockSecuritiesClient{listSecuritiesError: io.EOF}, - }, - args: args{req: connect.NewRequest(&portfoliov1.GetPortfolioSnapshotRequest{ - PortfolioId: "mybank-myportfolio", - Time: timestamppb.New(time.Date(2020, 1, 1, 0, 0, 0, 1, time.UTC)), - })}, - wantErr: true, - wantRes: func(t *testing.T, r *connect.Response[portfoliov1.PortfolioSnapshot]) bool { - return true - }, - }, - } - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - svc := &service{ - portfolios: tt.fields.portfolios, - events: persistence.Relationship[*portfoliov1.PortfolioEvent](tt.fields.portfolios), - securities: tt.fields.securities, - } - - if tt.fields.events != nil { - svc.events = tt.fields.events - } - - gotRes, err := svc.GetPortfolioSnapshot(tt.args.ctx, tt.args.req) - if (err != nil) != tt.wantErr { - t.Errorf("service.GetPortfolioSnapshot() error = %v, wantErr %v", err, tt.wantErr) - return - } - tt.wantRes(t, gotRes) - }) - } -} - -type mockSecuritiesClient struct { - securities []*portfoliov1.Security - listSecuritiesError error -} - -func (m *mockSecuritiesClient) ListSecurities(context.Context, *connect.Request[portfoliov1.ListSecuritiesRequest]) (*connect.Response[portfoliov1.ListSecuritiesResponse], error) { - return connect.NewResponse(&portfoliov1.ListSecuritiesResponse{ - Securities: m.securities, - }), m.listSecuritiesError -} - -func (*mockSecuritiesClient) GetSecurity(context.Context, *connect.Request[portfoliov1.GetSecurityRequest]) (*connect.Response[portfoliov1.Security], error) { - return nil, nil -} - -func (*mockSecuritiesClient) CreateSecurity(context.Context, *connect.Request[portfoliov1.CreateSecurityRequest]) (*connect.Response[portfoliov1.Security], error) { - return nil, nil -} - -func (*mockSecuritiesClient) UpdateSecurity(context.Context, *connect.Request[portfoliov1.UpdateSecurityRequest]) (*connect.Response[portfoliov1.Security], error) { - return nil, nil -} - -func (*mockSecuritiesClient) DeleteSecurity(context.Context, *connect.Request[portfoliov1.DeleteSecurityRequest]) (*connect.Response[emptypb.Empty], error) { - return nil, nil -} - -func (*mockSecuritiesClient) TriggerSecurityQuoteUpdate(context.Context, *connect.Request[portfoliov1.TriggerQuoteUpdateRequest]) (*connect.Response[portfoliov1.TriggerQuoteUpdateResponse], error) { - return nil, nil -} diff --git a/service/portfolio/transactions.go b/service/portfolio/transactions.go deleted file mode 100644 index 6f91adcd..00000000 --- a/service/portfolio/transactions.go +++ /dev/null @@ -1,156 +0,0 @@ -// Copyright 2023 Christian Banse -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. -// -// This file is part of The Money Gopher. - -package portfolio - -import ( - "bytes" - "context" - "errors" - "log/slog" - - portfoliov1 "github.com/oxisto/money-gopher/gen" - "github.com/oxisto/money-gopher/import/csv" - "github.com/oxisto/money-gopher/service/internal/crud" - - "connectrpc.com/connect" - "google.golang.org/protobuf/types/known/emptypb" -) - -var portfolioEventSetter = func(obj *portfoliov1.PortfolioEvent) *portfoliov1.PortfolioEvent { - return obj -} - -var ( - ErrMissingSecurityId = errors.New("the specified transaction type requires a security ID") - ErrMissingPrice = errors.New("a transaction requires a price") - ErrMissingAmount = errors.New("the specified transaction type requires an amount") -) - -func (svc *service) CreatePortfolioTransaction(ctx context.Context, req *connect.Request[portfoliov1.CreatePortfolioTransactionRequest]) (res *connect.Response[portfoliov1.PortfolioEvent], err error) { - var ( - tx *portfoliov1.PortfolioEvent = req.Msg.Transaction - ) - - // Do some basic validation depending on the type - switch tx.Type { - case portfoliov1.PortfolioEventType_PORTFOLIO_EVENT_TYPE_SELL: - fallthrough - case portfoliov1.PortfolioEventType_PORTFOLIO_EVENT_TYPE_BUY: - if tx.SecurityId == "" { - return nil, connect.NewError(connect.CodeInvalidArgument, ErrMissingSecurityId) - } else if tx.Amount == 0 { - return nil, connect.NewError(connect.CodeInvalidArgument, ErrMissingAmount) - } - } - - // We always need a price - if tx.Price.IsZero() { - return nil, connect.NewError(connect.CodeInvalidArgument, ErrMissingPrice) - } - - // Create a unique name for the transaction - tx.MakeUniqueID() - - slog.Info( - "Creating transaction", - "transaction", req.Msg.Transaction, - ) - - return crud.Create( - req.Msg.Transaction, - svc.events, - portfolioEventSetter, - ) -} - -func (svc *service) GetPortfolioTransaction(ctx context.Context, req *connect.Request[portfoliov1.GetPortfolioTransactionRequest]) (res *connect.Response[portfoliov1.PortfolioEvent], err error) { - return crud.Get( - req.Msg.Id, - svc.events, - func(obj *portfoliov1.PortfolioEvent) *portfoliov1.PortfolioEvent { - return obj - }, - ) -} - -func (svc *service) ListPortfolioTransactions(ctx context.Context, req *connect.Request[portfoliov1.ListPortfolioTransactionsRequest]) (res *connect.Response[portfoliov1.ListPortfolioTransactionsResponse], err error) { - return crud.List( - svc.events, - func( - res *connect.Response[portfoliov1.ListPortfolioTransactionsResponse], - list []*portfoliov1.PortfolioEvent, - ) error { - res.Msg.Transactions = list - return nil - }, - req.Msg.PortfolioId, - ) -} - -func (svc *service) UpdatePortfolioTransaction(ctx context.Context, req *connect.Request[portfoliov1.UpdatePortfolioTransactionRequest]) (res *connect.Response[portfoliov1.PortfolioEvent], err error) { - slog.Info( - "Updating transaction", - "tx", req.Msg.Transaction, - "update-mask", req.Msg.UpdateMask.Paths, - ) - - return crud.Update( - req.Msg.Transaction.Id, - req.Msg.Transaction, - req.Msg.UpdateMask.Paths, - svc.events, - portfolioEventSetter, - ) -} - -func (svc *service) DeletePortfolioTransactions(ctx context.Context, req *connect.Request[portfoliov1.DeletePortfolioTransactionRequest]) (res *connect.Response[emptypb.Empty], err error) { - return crud.Delete( - req.Msg.TransactionId, - svc.events, - ) -} - -func (svc *service) ImportTransactions(ctx context.Context, req *connect.Request[portfoliov1.ImportTransactionsRequest]) (res *connect.Response[emptypb.Empty], err error) { - var ( - txs []*portfoliov1.PortfolioEvent - secs []*portfoliov1.Security - ) - - txs, secs = csv.Import(bytes.NewReader([]byte(req.Msg.FromCsv)), req.Msg.PortfolioId) - - for _, sec := range secs { - // TODO(oxisto): Once "Create" is really create and not replace, we need - // to change this to something else. - svc.securities.CreateSecurity( - context.Background(), - connect.NewRequest(&portfoliov1.CreateSecurityRequest{ - Security: sec, - }), - ) - } - - for _, tx := range txs { - err = svc.events.Replace(tx) - if err != nil { - return nil, connect.NewError(connect.CodeInternal, err) - } - } - - res = connect.NewResponse(&emptypb.Empty{}) - - return -} diff --git a/service/portfolio/transactions_test.go b/service/portfolio/transactions_test.go deleted file mode 100644 index ded7692f..00000000 --- a/service/portfolio/transactions_test.go +++ /dev/null @@ -1,396 +0,0 @@ -// Copyright 2023 Christian Banse -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. -// -// This file is part of The Money Gopher. - -package portfolio - -import ( - "context" - "testing" - - portfoliov1 "github.com/oxisto/money-gopher/gen" - "github.com/oxisto/money-gopher/gen/portfoliov1connect" - "github.com/oxisto/money-gopher/persistence" - - "connectrpc.com/connect" - "github.com/oxisto/assert" - "google.golang.org/protobuf/types/known/fieldmaskpb" -) - -func Test_service_CreatePortfolioTransaction(t *testing.T) { - type fields struct { - portfolios persistence.StorageOperations[*portfoliov1.Portfolio] - securities portfoliov1connect.SecuritiesServiceClient - } - type args struct { - ctx context.Context - req *connect.Request[portfoliov1.CreatePortfolioTransactionRequest] - } - tests := []struct { - name string - fields fields - args args - wantRes assert.Want[*connect.Response[portfoliov1.PortfolioEvent]] - wantSvc assert.Want[*service] - wantErr bool - }{ - { - name: "happy path buy", - fields: fields{ - portfolios: myPortfolio(t), - }, - args: args{ - req: connect.NewRequest(&portfoliov1.CreatePortfolioTransactionRequest{ - Transaction: &portfoliov1.PortfolioEvent{ - PortfolioId: "mybank-myportfolio", - Type: portfoliov1.PortfolioEventType_PORTFOLIO_EVENT_TYPE_BUY, - SecurityId: "My Security", - Amount: 1, - Price: portfoliov1.Value(2000), - }, - }), - }, - wantRes: func(t *testing.T, r *connect.Response[portfoliov1.PortfolioEvent]) bool { - return assert.Equals(t, "My Security", r.Msg.GetSecurityId()) - }, - wantSvc: func(t *testing.T, s *service) bool { - list, _ := s.events.List("mybank-myportfolio") - return assert.Equals(t, 3, len(list)) - }, - }, - { - name: "happy path sell", - fields: fields{ - portfolios: myPortfolio(t), - }, - args: args{ - req: connect.NewRequest(&portfoliov1.CreatePortfolioTransactionRequest{ - Transaction: &portfoliov1.PortfolioEvent{ - PortfolioId: "mybank-myportfolio", - Type: portfoliov1.PortfolioEventType_PORTFOLIO_EVENT_TYPE_SELL, - SecurityId: "My Security", - Amount: 1, - Price: portfoliov1.Value(2000), - }, - }), - }, - wantRes: func(t *testing.T, r *connect.Response[portfoliov1.PortfolioEvent]) bool { - return assert.Equals(t, "My Security", r.Msg.GetSecurityId()) - }, - wantSvc: func(t *testing.T, s *service) bool { - list, _ := s.events.List("mybank-myportfolio") - return assert.Equals(t, 3, len(list)) - }, - }, - { - name: "missing security ID", - fields: fields{ - portfolios: myPortfolio(t), - }, - args: args{ - req: connect.NewRequest(&portfoliov1.CreatePortfolioTransactionRequest{ - Transaction: &portfoliov1.PortfolioEvent{ - PortfolioId: "mybank-myportfolio", - Type: portfoliov1.PortfolioEventType_PORTFOLIO_EVENT_TYPE_SELL, - Amount: 1, - Price: portfoliov1.Value(2000), - }, - }), - }, - wantRes: func(t *testing.T, r *connect.Response[portfoliov1.PortfolioEvent]) bool { - if r != nil { - t.Fatal("not nil") - } - - return true - }, - wantErr: true, - }, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - svc := &service{ - portfolios: tt.fields.portfolios, - events: persistence.Relationship[*portfoliov1.PortfolioEvent](tt.fields.portfolios), - securities: tt.fields.securities, - } - gotRes, err := svc.CreatePortfolioTransaction(tt.args.ctx, tt.args.req) - if (err != nil) != tt.wantErr { - t.Errorf("service.CreatePortfolioTransaction() error = %v, wantErr %v", err, tt.wantErr) - return - } - - tt.wantRes(t, gotRes) - if tt.wantSvc != nil { - tt.wantSvc(t, svc) - } - }) - } -} - -func Test_service_GetPortfolioTransaction(t *testing.T) { - type fields struct { - portfolios persistence.StorageOperations[*portfoliov1.Portfolio] - securities portfoliov1connect.SecuritiesServiceClient - } - type args struct { - ctx context.Context - req *connect.Request[portfoliov1.GetPortfolioTransactionRequest] - } - tests := []struct { - name string - fields fields - args args - wantRes assert.Want[*connect.Response[portfoliov1.PortfolioEvent]] - wantErr bool - }{ - { - name: "happy path", - fields: fields{ - portfolios: myPortfolio(t), - }, - args: args{ - req: connect.NewRequest(&portfoliov1.GetPortfolioTransactionRequest{ - Id: "buy", - }), - }, - wantRes: func(t *testing.T, r *connect.Response[portfoliov1.PortfolioEvent]) bool { - return assert.Equals(t, "buy", r.Msg.Id) && assert.Equals(t, "mybank-myportfolio", r.Msg.PortfolioId) - }, - }, - } - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - svc := &service{ - portfolios: tt.fields.portfolios, - events: persistence.Relationship[*portfoliov1.PortfolioEvent](tt.fields.portfolios), - securities: tt.fields.securities, - } - gotRes, err := svc.GetPortfolioTransaction(tt.args.ctx, tt.args.req) - if (err != nil) != tt.wantErr { - t.Errorf("service.GetPortfolioTransaction() error = %v, wantErr %v", err, tt.wantErr) - return - } - tt.wantRes(t, gotRes) - }) - } -} - -func Test_service_ListPortfolioTransactions(t *testing.T) { - type fields struct { - portfolios persistence.StorageOperations[*portfoliov1.Portfolio] - securities portfoliov1connect.SecuritiesServiceClient - } - type args struct { - ctx context.Context - req *connect.Request[portfoliov1.ListPortfolioTransactionsRequest] - } - tests := []struct { - name string - fields fields - args args - wantRes assert.Want[*connect.Response[portfoliov1.ListPortfolioTransactionsResponse]] - wantErr bool - }{ - { - name: "happy path", - fields: fields{ - portfolios: myPortfolio(t), - }, - args: args{ - req: connect.NewRequest(&portfoliov1.ListPortfolioTransactionsRequest{ - PortfolioId: "mybank-myportfolio", - }), - }, - wantRes: func(t *testing.T, r *connect.Response[portfoliov1.ListPortfolioTransactionsResponse]) bool { - return assert.Equals(t, 2, len(r.Msg.Transactions)) - }, - }, - } - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - svc := &service{ - portfolios: tt.fields.portfolios, - events: persistence.Relationship[*portfoliov1.PortfolioEvent](tt.fields.portfolios), - securities: tt.fields.securities, - } - gotRes, err := svc.ListPortfolioTransactions(tt.args.ctx, tt.args.req) - if (err != nil) != tt.wantErr { - t.Errorf("service.ListPortfolioTransactions() error = %v, wantErr %v", err, tt.wantErr) - return - } - tt.wantRes(t, gotRes) - }) - } -} - -func Test_service_UpdatePortfolioTransaction(t *testing.T) { - type fields struct { - portfolios persistence.StorageOperations[*portfoliov1.Portfolio] - securities portfoliov1connect.SecuritiesServiceClient - } - type args struct { - ctx context.Context - req *connect.Request[portfoliov1.UpdatePortfolioTransactionRequest] - } - tests := []struct { - name string - fields fields - args args - wantRes assert.Want[*connect.Response[portfoliov1.PortfolioEvent]] - wantErr bool - }{ - { - name: "happy path", - fields: fields{ - portfolios: myPortfolio(t), - }, - args: args{ - req: connect.NewRequest(&portfoliov1.UpdatePortfolioTransactionRequest{ - Transaction: &portfoliov1.PortfolioEvent{ - Id: "buy", - Type: portfoliov1.PortfolioEventType_PORTFOLIO_EVENT_TYPE_BUY, - SecurityId: "My Second Security", - }, - UpdateMask: &fieldmaskpb.FieldMask{Paths: []string{"security_id"}}, - }), - }, - wantRes: func(t *testing.T, r *connect.Response[portfoliov1.PortfolioEvent]) bool { - return assert.Equals(t, "My Second Security", r.Msg.SecurityId) - }, - }, - } - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - svc := &service{ - portfolios: tt.fields.portfolios, - events: persistence.Relationship[*portfoliov1.PortfolioEvent](tt.fields.portfolios), - securities: tt.fields.securities, - } - gotRes, err := svc.UpdatePortfolioTransaction(tt.args.ctx, tt.args.req) - if (err != nil) != tt.wantErr { - t.Errorf("service.UpdatePortfolioTransactions() error = %v, wantErr %v", err, tt.wantErr) - return - } - tt.wantRes(t, gotRes) - }) - } -} - -func Test_service_DeletePortfolioTransactions(t *testing.T) { - type fields struct { - portfolios persistence.StorageOperations[*portfoliov1.Portfolio] - securities portfoliov1connect.SecuritiesServiceClient - } - type args struct { - ctx context.Context - req *connect.Request[portfoliov1.DeletePortfolioTransactionRequest] - } - tests := []struct { - name string - fields fields - args args - wantSvc assert.Want[*service] - wantErr bool - }{ - { - name: "happy path", - fields: fields{ - portfolios: myPortfolio(t), - }, - args: args{ - req: connect.NewRequest(&portfoliov1.DeletePortfolioTransactionRequest{ - TransactionId: 1, - }), - }, - wantSvc: func(t *testing.T, s *service) bool { - list, _ := s.portfolios.List("mybank-myportfolio") - return assert.Equals(t, 0, len(list)) - }, - }, - } - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - svc := &service{ - portfolios: tt.fields.portfolios, - events: persistence.Relationship[*portfoliov1.PortfolioEvent](tt.fields.portfolios), - securities: tt.fields.securities, - } - _, err := svc.DeletePortfolioTransactions(tt.args.ctx, tt.args.req) - if (err != nil) != tt.wantErr { - t.Errorf("service.DeletePortfolioTransactions() error = %v, wantErr %v", err, tt.wantErr) - return - } - tt.wantSvc(t, svc) - }) - } -} - -func Test_service_ImportTransactions(t *testing.T) { - type fields struct { - portfolios persistence.StorageOperations[*portfoliov1.Portfolio] - securities portfoliov1connect.SecuritiesServiceClient - } - type args struct { - ctx context.Context - req *connect.Request[portfoliov1.ImportTransactionsRequest] - } - tests := []struct { - name string - fields fields - args args - wantSvc assert.Want[*service] - wantErr bool - }{ - { - name: "happy path", - fields: fields{ - portfolios: emptyPortfolio(t), - securities: &mockSecuritiesClient{}, - }, - args: args{ - req: connect.NewRequest(&portfoliov1.ImportTransactionsRequest{ - PortfolioId: "mybank-myportfolio", - FromCsv: `Date;Type;Value;Transaction Currency;Gross Amount;Currency Gross Amount;Exchange Rate;Fees;Taxes;Shares;ISIN;WKN;Ticker Symbol;Security Name;Note -2021-06-05T00:00;Buy;2.151,85;EUR;;;;10,25;0,00;20;US0378331005;865985;APC.F;Apple Inc.; -2021-06-05T00:00;Sell;-2.151,85;EUR;;;;10,25;0,00;20;US0378331005;865985;APC.F;Apple Inc.; -2021-06-18T00:00;Buy;912,66;EUR;;;;7,16;0,00;5;US09075V1026;A2PSR2;22UA.F;BioNTech SE;`, - }), - }, - wantSvc: func(t *testing.T, s *service) bool { - txs, err := s.events.List("mybank-myportfolio") - return true && - assert.NoError(t, err) && - assert.Equals(t, 3, len(txs)) - }, - }, - } - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - svc := &service{ - portfolios: tt.fields.portfolios, - events: persistence.Relationship[*portfoliov1.PortfolioEvent](tt.fields.portfolios), - securities: tt.fields.securities, - } - _, err := svc.ImportTransactions(tt.args.ctx, tt.args.req) - if (err != nil) != tt.wantErr { - t.Errorf("service.ImportTransactions() error = %v, wantErr %v", err, tt.wantErr) - return - } - tt.wantSvc(t, svc) - }) - } -} diff --git a/service/securities/securities.go b/service/securities/securities.go deleted file mode 100644 index 75c0db48..00000000 --- a/service/securities/securities.go +++ /dev/null @@ -1,114 +0,0 @@ -// Copyright 2023 Christian Banse -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. -// -// This file is part of The Money Gopher. - -package securities - -import ( - "context" - "slices" - - portfoliov1 "github.com/oxisto/money-gopher/gen" - "github.com/oxisto/money-gopher/service/internal/crud" - - "connectrpc.com/connect" - "google.golang.org/protobuf/types/known/emptypb" -) - -func (svc *service) CreateSecurity(ctx context.Context, req *connect.Request[portfoliov1.CreateSecurityRequest]) (res *connect.Response[portfoliov1.Security], err error) { - return crud.Create( - req.Msg.Security, - svc.securities, - func(obj *portfoliov1.Security) *portfoliov1.Security { - for _, ls := range obj.ListedOn { - svc.listedSecurities.Replace(ls) - } - - return obj - }, - ) -} - -func (svc *service) GetSecurity(ctx context.Context, req *connect.Request[portfoliov1.GetSecurityRequest]) (res *connect.Response[portfoliov1.Security], err error) { - return crud.Get( - req.Msg.Id, - svc.securities, - func(obj *portfoliov1.Security) *portfoliov1.Security { - obj.ListedOn, _ = svc.listedSecurities.List(obj.Id) - - return obj - }, - ) -} - -func (svc *service) ListSecurities(ctx context.Context, req *connect.Request[portfoliov1.ListSecuritiesRequest]) (res *connect.Response[portfoliov1.ListSecuritiesResponse], err error) { - return crud.List( - svc.securities, - func(res *connect.Response[portfoliov1.ListSecuritiesResponse], list []*portfoliov1.Security) error { - res.Msg.Securities = list - - for _, sec := range res.Msg.Securities { - sec.ListedOn, err = svc.listedSecurities.List(sec.Id) - if err != nil { - return err - } - } - - return nil - }, - ) -} - -func (svc *service) UpdateSecurity(ctx context.Context, req *connect.Request[portfoliov1.UpdateSecurityRequest]) (res *connect.Response[portfoliov1.Security], err error) { - return crud.Update( - req.Msg.Security.Id, - req.Msg.Security, - req.Msg.UpdateMask.Paths, - svc.securities, - func(obj *portfoliov1.Security) *portfoliov1.Security { - if slices.Contains(req.Msg.UpdateMask.Paths, "listed_on") { - for _, ls := range req.Msg.Security.ListedOn { - svc.listedSecurities.Replace(ls) - } - } - - return obj - }, - ) -} - -func (svc *service) DeleteSecurity(ctx context.Context, req *connect.Request[portfoliov1.DeleteSecurityRequest]) (res *connect.Response[emptypb.Empty], err error) { - return crud.Delete( - req.Msg.Id, - svc.securities, - ) -} - -func (svc *service) fetchSecurity(name string) (sec *portfoliov1.Security, err error) { - res, err := crud.Get( - name, - svc.securities, - func(obj *portfoliov1.Security) *portfoliov1.Security { - obj.ListedOn, _ = svc.listedSecurities.List(obj.Id) - - return obj - }, - ) - if err != nil { - return nil, err - } - - return res.Msg, nil -} diff --git a/service/securities/securities_test.go b/service/securities/securities_test.go deleted file mode 100644 index 0f6883a2..00000000 --- a/service/securities/securities_test.go +++ /dev/null @@ -1,229 +0,0 @@ -// Copyright 2023 Christian Banse -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. -// -// This file is part of The Money Gopher. - -package securities - -import ( - "context" - "testing" - - portfoliov1 "github.com/oxisto/money-gopher/gen" - "github.com/oxisto/money-gopher/gen/portfoliov1connect" - "github.com/oxisto/money-gopher/internal" - "github.com/oxisto/money-gopher/persistence" - - "connectrpc.com/connect" - "github.com/oxisto/assert" - "golang.org/x/text/currency" - "google.golang.org/protobuf/proto" - "google.golang.org/protobuf/testing/protocmp" - "google.golang.org/protobuf/types/known/emptypb" - "google.golang.org/protobuf/types/known/fieldmaskpb" -) - -func Test_service_ListSecurities(t *testing.T) { - type fields struct { - securities persistence.StorageOperations[*portfoliov1.Security] - } - type args struct { - ctx context.Context - req *connect.Request[portfoliov1.ListSecuritiesRequest] - } - tests := []struct { - name string - fields fields - args args - wantRes assert.Want[*connect.Response[portfoliov1.ListSecuritiesResponse]] - wantErr bool - }{ - { - name: "happy path", - fields: fields{ - securities: internal.NewTestDBOps(t, func(ops persistence.StorageOperations[*portfoliov1.Security]) { - assert.NoError(t, ops.Replace(&portfoliov1.Security{Id: "My Security"})) - rel := persistence.Relationship[*portfoliov1.ListedSecurity](ops) - assert.NoError(t, rel.Replace(&portfoliov1.ListedSecurity{SecurityId: "My Security", Ticker: "SEC", Currency: currency.EUR.String()})) - }), - }, - wantRes: func(t *testing.T, r *connect.Response[portfoliov1.ListSecuritiesResponse]) bool { - return true && - assert.Equals(t, "My Security", r.Msg.Securities[0].Id) && - assert.Equals(t, 1, len(r.Msg.Securities[0].ListedOn)) - }, - wantErr: false, - }, - } - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - svc := &service{ - securities: tt.fields.securities, - listedSecurities: persistence.Relationship[*portfoliov1.ListedSecurity](tt.fields.securities), - } - gotRes, err := svc.ListSecurities(tt.args.ctx, tt.args.req) - if (err != nil) != tt.wantErr { - t.Errorf("service.ListSecurities() error = %v, wantErr %v", err, tt.wantErr) - return - } - tt.wantRes(t, gotRes) - }) - } -} - -func Test_service_GetSecurity(t *testing.T) { - type fields struct { - securities persistence.StorageOperations[*portfoliov1.Security] - } - type args struct { - ctx context.Context - req *connect.Request[portfoliov1.GetSecurityRequest] - } - tests := []struct { - name string - fields fields - args args - wantRes assert.Want[*portfoliov1.Security] - wantErr bool - }{ - { - name: "happy path", - fields: fields{ - securities: internal.NewTestDBOps(t, func(ops persistence.StorageOperations[*portfoliov1.Security]) { - ops.Replace(&portfoliov1.Security{Id: "My Security"}) - rel := persistence.Relationship[*portfoliov1.ListedSecurity](ops) - assert.NoError(t, rel.Replace(&portfoliov1.ListedSecurity{SecurityId: "My Security", Ticker: "SEC", Currency: currency.EUR.String()})) - }), - }, - args: args{ - req: connect.NewRequest(&portfoliov1.GetSecurityRequest{Id: "My Security"}), - }, - wantRes: func(t *testing.T, s *portfoliov1.Security) bool { - return assert.Equals(t, &portfoliov1.Security{ - Id: "My Security", - ListedOn: []*portfoliov1.ListedSecurity{{SecurityId: "My Security", Ticker: "SEC", Currency: currency.EUR.String()}}, - }, s, protocmp.Transform()) - }, - wantErr: false, - }, - } - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - svc := &service{ - securities: tt.fields.securities, - listedSecurities: persistence.Relationship[*portfoliov1.ListedSecurity](tt.fields.securities), - } - gotRes, err := svc.GetSecurity(tt.args.ctx, tt.args.req) - if (err != nil) != tt.wantErr { - t.Errorf("service.GetSecurity() error = %v, wantErr %v", err, tt.wantErr) - return - } - tt.wantRes(t, gotRes.Msg) - }) - } -} - -func Test_service_UpdateSecurity(t *testing.T) { - type fields struct { - securities persistence.StorageOperations[*portfoliov1.Security] - } - type args struct { - ctx context.Context - req *connect.Request[portfoliov1.UpdateSecurityRequest] - } - tests := []struct { - name string - fields fields - args args - wantRes *connect.Response[portfoliov1.Security] - wantErr bool - }{ - { - name: "change display_name", - fields: fields{ - securities: internal.NewTestDBOps(t, func(ops persistence.StorageOperations[*portfoliov1.Security]) { - ops.Replace(&portfoliov1.Security{Id: "My Stock"}) - }), - }, - args: args{req: connect.NewRequest(&portfoliov1.UpdateSecurityRequest{ - Security: &portfoliov1.Security{Id: "My Stock", DisplayName: "Test"}, - UpdateMask: &fieldmaskpb.FieldMask{Paths: []string{"display_name"}}, - })}, - wantRes: connect.NewResponse(&portfoliov1.Security{Id: "My Stock", DisplayName: "Test"}), - wantErr: false, - }, - } - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - svc := &service{ - securities: tt.fields.securities, - } - gotRes, err := svc.UpdateSecurity(tt.args.ctx, tt.args.req) - if (err != nil) != tt.wantErr { - t.Errorf("service.UpdateSecurity() error = %v, wantErr %v", err, tt.wantErr) - return - } - if !proto.Equal(gotRes.Msg, tt.wantRes.Msg) { - t.Errorf("service.UpdateSecurity() = %v, want %v", gotRes, tt.wantRes) - } - }) - } -} - -func Test_service_DeleteSecurity(t *testing.T) { - type fields struct { - securities persistence.StorageOperations[*portfoliov1.Security] - UnimplementedSecuritiesServiceHandler portfoliov1connect.UnimplementedSecuritiesServiceHandler - } - type args struct { - ctx context.Context - req *connect.Request[portfoliov1.DeleteSecurityRequest] - } - tests := []struct { - name string - fields fields - args args - wantRes assert.Want[*emptypb.Empty] - wantErr bool - }{ - { - name: "happy path", - fields: fields{ - securities: internal.NewTestDBOps(t, func(ops persistence.StorageOperations[*portfoliov1.Security]) { - ops.Replace(&portfoliov1.Security{Id: "My Stock"}) - }), - }, - args: args{req: connect.NewRequest(&portfoliov1.DeleteSecurityRequest{ - Id: "My Stock", - })}, - wantRes: func(t *testing.T, e *emptypb.Empty) bool { - return assert.Equals(t, &emptypb.Empty{}, e, protocmp.Transform()) - }, - wantErr: false, - }, - } - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - svc := &service{ - securities: tt.fields.securities, - } - gotRes, err := svc.DeleteSecurity(tt.args.ctx, tt.args.req) - if (err != nil) != tt.wantErr { - t.Errorf("service.DeleteSecurityRequest() error = %v, wantErr %v", err, tt.wantErr) - return - } - tt.wantRes(t, gotRes.Msg) - }) - } -} diff --git a/service/securities/service.go b/service/securities/service.go deleted file mode 100644 index 09824da5..00000000 --- a/service/securities/service.go +++ /dev/null @@ -1,88 +0,0 @@ -// Copyright 2023 Christian Banse -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. -// -// This file is part of The Money Gopher. - -// package securities contains the code for the SecuritiesService implementation. -package securities - -import ( - "context" - "time" - - moneygopher "github.com/oxisto/money-gopher" - portfoliov1 "github.com/oxisto/money-gopher/gen" - "github.com/oxisto/money-gopher/gen/portfoliov1connect" - "github.com/oxisto/money-gopher/persistence" - - "golang.org/x/text/currency" - "google.golang.org/protobuf/types/known/timestamppb" -) - -type service struct { - securities persistence.StorageOperations[*portfoliov1.Security] - listedSecurities persistence.StorageOperations[*portfoliov1.ListedSecurity] - - portfoliov1connect.UnimplementedSecuritiesServiceHandler - db *persistence.DB -} - -// QuoteUpdater is an interface that allows to trigger an update of the quotes -// for the given securities. -type QuoteUpdater interface { - // UpdateQuotes triggers an update of the quotes for the given securities. - UpdateQuotes(ctx context.Context, IDs []string) error -} - -func NewService(db *persistence.DB) portfoliov1connect.SecuritiesServiceHandler { - securities := persistence.Ops[*portfoliov1.Security](db) - listedSecurities := persistence.Relationship[*portfoliov1.ListedSecurity](securities) - secs := []*portfoliov1.Security{ - { - Id: "US0378331005", - DisplayName: "Apple Inc.", - ListedOn: []*portfoliov1.ListedSecurity{ - { - SecurityId: "US0378331005", - Ticker: "APC.F", - Currency: currency.EUR.String(), - LatestQuote: portfoliov1.Value(15016), - LatestQuoteTimestamp: timestamppb.New(time.Date(2023, 4, 21, 0, 0, 0, 0, time.Local)), - }, - { - SecurityId: "US0378331005", - Ticker: "AAPL", - Currency: currency.USD.String(), - LatestQuote: portfoliov1.Value(16502), - LatestQuoteTimestamp: timestamppb.New(time.Date(2023, 4, 21, 0, 0, 0, 0, time.Local)), - }, - }, - QuoteProvider: moneygopher.Ref(QuoteProviderYF), - }, - } - for _, sec := range secs { - securities.Replace(sec) - - // TODO: in the future, we might do this automatically - for _, ls := range sec.ListedOn { - listedSecurities.Replace(ls) - } - } - - return &service{ - securities: securities, - listedSecurities: listedSecurities, - db: db, - } -} diff --git a/service/securities/service_test.go b/service/securities/service_test.go deleted file mode 100644 index a3cb92c9..00000000 --- a/service/securities/service_test.go +++ /dev/null @@ -1,45 +0,0 @@ -// Copyright 2023 Christian Banse -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. -// -// This file is part of The Money Gopher. - -package securities - -import ( - "testing" - - "github.com/oxisto/money-gopher/internal" - - "github.com/oxisto/assert" -) - -func TestNewService(t *testing.T) { - tests := []struct { - name string - want assert.Want[*service] - }{ - { - name: "Default", - want: func(t *testing.T, s *service) bool { - return true - }, - }, - } - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - got := NewService(internal.NewTestDB(t)) - tt.want(t, assert.Is[*service](t, got)) - }) - } -} diff --git a/sqlc.yaml b/sqlc.yaml index 88f9c706..5cf75e43 100644 --- a/sqlc.yaml +++ b/sqlc.yaml @@ -8,3 +8,30 @@ sql: package: "persistence" out: "persistence" emit_result_struct_pointers: true + overrides: + - column: "portfolio_events.type" + go_type: github.com/oxisto/money-gopher/portfolio/events.PortfolioEventType + - column: "portfolio_events.price" + go_type: + import: "github.com/oxisto/money-gopher/currency" + package: "currency" + type: "Currency" + pointer: true + - column: "portfolio_events.fees" + go_type: + import: "github.com/oxisto/money-gopher/currency" + package: "currency" + type: "Currency" + pointer: true + - column: "portfolio_events.taxes" + go_type: + import: "github.com/oxisto/money-gopher/currency" + package: "currency" + type: "Currency" + pointer: true + - column: "listed_securities.latest_quote" + go_type: + import: "github.com/oxisto/money-gopher/currency" + package: "currency" + type: "Currency" + pointer: true diff --git a/tools/tools.go b/tools/tools.go index ac12408e..85d7ad32 100644 --- a/tools/tools.go +++ b/tools/tools.go @@ -6,5 +6,6 @@ package tools import ( _ "github.com/99designs/gqlgen" _ "github.com/mfridman/tparse" + _ "github.com/mpyw/sqlc-restruct/cmd/sqlc-restruct" _ "github.com/sqlc-dev/sqlc/cmd/sqlc" ) From 3f4ce6d6700ff2730d65028d46e21950a079de42 Mon Sep 17 00:00:00 2001 From: Christian Banse Date: Wed, 1 Jan 2025 14:50:09 +0100 Subject: [PATCH 14/35] Package securities/quote also tests --- securities/quote/quote_test.go | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/securities/quote/quote_test.go b/securities/quote/quote_test.go index 253fed8b..1226f457 100644 --- a/securities/quote/quote_test.go +++ b/securities/quote/quote_test.go @@ -44,6 +44,10 @@ func (mockQuoteProvider) LatestQuote(_ context.Context, _ *persistence.ListedSec return currency.Value(100), time.Date(1, 0, 0, 0, 0, 0, 0, time.UTC), nil } +func init() { + RegisterQuoteProvider(QuoteProviderMock, &mockQP{}) +} + func Test_qu_updateQuote(t *testing.T) { type fields struct { db *persistence.DB @@ -81,7 +85,7 @@ func Test_qu_updateQuote(t *testing.T) { ls: &persistence.ListedSecurity{SecurityID: "My Security", Ticker: "SEC", Currency: "EUR"}, }, want: func(t *testing.T, ls *persistence.ListedSecurity) bool { - return assert.Equals(t, 100, int(ls.LatestQuote.Int64)) + return assert.Equals(t, 100, ls.LatestQuote.Amount) }, }, } From 309aaa7e84a8fe80835a930619efbccffc29db74 Mon Sep 17 00:00:00 2001 From: Christian Banse Date: Wed, 1 Jan 2025 15:12:51 +0100 Subject: [PATCH 15/35] CSV package works --- import/csv/csv_importer.go | 86 ++++++++++-------- import/csv/csv_importer_test.go | 155 +++++++++++++++++--------------- persistence/extra.go | 20 +++++ server/server.go | 43 ++------- 4 files changed, 161 insertions(+), 143 deletions(-) diff --git a/import/csv/csv_importer.go b/import/csv/csv_importer.go index 90ff9af1..c3d656d7 100644 --- a/import/csv/csv_importer.go +++ b/import/csv/csv_importer.go @@ -29,6 +29,7 @@ package csv import ( + "database/sql" "encoding/csv" "errors" "fmt" @@ -39,7 +40,6 @@ import ( "strings" "time" - moneygopher "github.com/oxisto/money-gopher" "github.com/oxisto/money-gopher/currency" "github.com/oxisto/money-gopher/persistence" "github.com/oxisto/money-gopher/portfolio/events" @@ -60,7 +60,11 @@ var ( // Import imports CSV records from a [io.Reader] containing portfolio // transactions. -func Import(r io.Reader, pname string) (txs []*persistence.PortfolioEvent, secs []*persistence.Security) { +func Import(r io.Reader, pname string) ( + txs []*persistence.PortfolioEvent, + secs []*persistence.Security, + lss []*persistence.ListedSecurity, +) { cr := csv.NewReader(r) cr.Comma = ';' @@ -69,7 +73,7 @@ func Import(r io.Reader, pname string) (txs []*persistence.PortfolioEvent, secs // Read until EOF for { - tx, sec, err := readLine(cr, pname) + tx, sec, ls, err := readLine(cr, pname) if errors.Is(err, io.EOF) { break } else if err != nil { @@ -80,17 +84,25 @@ func Import(r io.Reader, pname string) (txs []*persistence.PortfolioEvent, secs txs = append(txs, tx) secs = append(secs, sec) + lss = append(lss, ls...) } - // Compact securities + // Make (listed) securities unique secs = slices.CompactFunc(secs, func(a *persistence.Security, b *persistence.Security) bool { return a.ID == b.ID }) + lss = slices.CompactFunc(lss, func(a *persistence.ListedSecurity, b *persistence.ListedSecurity) bool { + return a.SecurityID == b.SecurityID && a.Ticker == b.Ticker + }) return } -func readLine(cr *csv.Reader, pname string) (tx *persistence.PortfolioEvent, sec *persistence.Security, err error) { +func readLine(cr *csv.Reader, pname string) ( + tx *persistence.PortfolioEvent, + sec *persistence.Security, + ls []*persistence.ListedSecurity, + err error) { var ( record []string value *currency.Currency @@ -98,69 +110,68 @@ func readLine(cr *csv.Reader, pname string) (tx *persistence.PortfolioEvent, sec record, err = cr.Read() if err != nil { - return nil, nil, fmt.Errorf("%w: %w", ErrReadingCSV, err) + return nil, nil, nil, fmt.Errorf("%w: %w", ErrReadingCSV, err) } tx = new(persistence.PortfolioEvent) tx.Time, err = txTime(record[0]) if err != nil { - return nil, nil, fmt.Errorf("%w: %w", ErrParsingTime, err) + return nil, nil, nil, fmt.Errorf("%w: %w", ErrParsingTime, err) } - tx.Type = int64(txType(record[1])) - if tx.Type == int64(events.PortfolioEventTypeUnknown) { - return nil, nil, ErrParsingType + tx.Type = txType(record[1]) + if tx.Type == events.PortfolioEventTypeUnknown { + return nil, nil, nil, ErrParsingType } value, err = parseFloatCurrency(record[2]) if err != nil { - return nil, nil, fmt.Errorf("%w: %w", ErrParsingValue, err) + return nil, nil, nil, fmt.Errorf("%w: %w", ErrParsingValue, err) } tx.Fees, err = parseFloatCurrency(record[7]) if err != nil { - return nil, nil, fmt.Errorf("%w: %w", ErrParsingFees, err) + return nil, nil, nil, fmt.Errorf("%w: %w", ErrParsingFees, err) } tx.Taxes, err = parseFloatCurrency(record[8]) if err != nil { - return nil, nil, fmt.Errorf("%w: %w", ErrParsingTaxes, err) + return nil, nil, nil, fmt.Errorf("%w: %w", ErrParsingTaxes, err) } tx.Amount, err = parseFloat64(record[9]) if err != nil { - return nil, nil, fmt.Errorf("%w: %w", ErrParsingAmount, err) + return nil, nil, nil, fmt.Errorf("%w: %w", ErrParsingAmount, err) } // Calculate the price if tx.Type == events.PortfolioEventTypeBuy || - tx.Type == events.PortfolioEventType { - tx.Price = portfoliov1.Divide(portfoliov1.Minus(value, tx.Fees), tx.Amount) - } else if tx.Type == portfoliov1.PortfolioEventType_PORTFOLIO_EVENT_TYPE_SELL || - tx.Type == portfoliov1.PortfolioEventType_PORTFOLIO_EVENT_TYPE_DELIVERY_OUTBOUND { - tx.Price = portfoliov1.Times(portfoliov1.Divide(portfoliov1.Minus(portfoliov1.Minus(value, tx.Fees), tx.Taxes), tx.Amount), -1) + tx.Type == events.PortfolioEventTypeDeliveryInbound { + tx.Price = currency.Divide(currency.Minus(value, tx.Fees), tx.Amount.Float64) + } else if tx.Type == events.PortfolioEventTypeSell || + tx.Type == events.PortfolioEventTypeDeliveryOutbound { + tx.Price = currency.Times(currency.Divide(currency.Minus(currency.Minus(value, tx.Fees), tx.Taxes), tx.Amount.Float64), -1) } - sec = new(portfoliov1.Security) - sec.Id = record[10] + sec = new(persistence.Security) + sec.ID = record[10] sec.DisplayName = record[13] - sec.ListedOn = []*portfoliov1.ListedSecurity{ - { - SecurityId: sec.Id, - Ticker: record[12], - Currency: lsCurrency(record[3], record[5]), - }, - } + + ls = append(ls, &persistence.ListedSecurity{ + SecurityID: sec.ID, + Ticker: record[12], + Currency: lsCurrency(record[3], record[5]), + }) // Default to YF, but only if we have a ticker symbol, otherwise, let's try ING - if len(sec.ListedOn) >= 0 && len(sec.ListedOn[0].Ticker) > 0 { - sec.QuoteProvider = moneygopher.Ref(quote.QuoteProviderYF) + if len(ls) >= 0 && len(ls[0].Ticker) > 0 { + sec.QuoteProvider = sql.NullString{String: quote.QuoteProviderYF, Valid: true} } else { - sec.QuoteProvider = moneygopher.Ref(quote.QuoteProviderING) + sec.QuoteProvider = sql.NullString{String: quote.QuoteProviderING, Valid: true} } - tx.PortfolioId = pname - tx.SecurityId = sec.Id + tx.PortfolioID = pname + tx.SecurityID = sec.ID tx.MakeUniqueID() return @@ -195,7 +206,11 @@ func txTime(s string) (t time.Time, err error) { return t, nil } -func parseFloat64(s string) (f float64, err error) { +func parseFloat64(s string) (nf sql.NullFloat64, err error) { + var ( + f float64 + ) + // We assume that the float is in German locale (this might not be true for // all users), so we need to convert it s = strings.ReplaceAll(s, ".", "") @@ -203,8 +218,9 @@ func parseFloat64(s string) (f float64, err error) { f, err = strconv.ParseFloat(s, 32) if err != nil { - return 0, err + return sql.NullFloat64{Valid: false}, err } + nf = sql.NullFloat64{Float64: f, Valid: true} return } diff --git a/import/csv/csv_importer_test.go b/import/csv/csv_importer_test.go index 4b6845cf..2a95cc5d 100644 --- a/import/csv/csv_importer_test.go +++ b/import/csv/csv_importer_test.go @@ -18,17 +18,18 @@ package csv import ( "bytes" + "database/sql" "encoding/csv" "io" "testing" "time" - moneygopher "github.com/oxisto/money-gopher" + "github.com/oxisto/money-gopher/currency" + "github.com/oxisto/money-gopher/persistence" + "github.com/oxisto/money-gopher/portfolio/events" "github.com/oxisto/money-gopher/securities/quote" "github.com/oxisto/assert" - "google.golang.org/protobuf/testing/protocmp" - "google.golang.org/protobuf/types/known/timestamppb" ) func TestImport(t *testing.T) { @@ -39,8 +40,9 @@ func TestImport(t *testing.T) { tests := []struct { name string args args - wantTxs assert.Want[[]*portfoliov1.PortfolioEvent] - wantSecs assert.Want[[]*portfoliov1.Security] + wantTxs assert.Want[[]*persistence.PortfolioEvent] + wantSecs assert.Want[[]*persistence.Security] + wantLss assert.Want[[]*persistence.ListedSecurity] }{ { name: "happy path", @@ -50,12 +52,15 @@ func TestImport(t *testing.T) { 2021-06-05T00:00;Sell;-2.151,85;EUR;;;;10,25;0,00;20;US0378331005;865985;APC.F;Apple Inc.; 2021-06-18T00:00;Delivery (Inbound);912,66;EUR;;;;7,16;0,00;5;US09075V1026;A2PSR2;22UA.F;BioNTech SE;`)), }, - wantTxs: func(t *testing.T, txs []*portfoliov1.PortfolioEvent) bool { + wantTxs: func(t *testing.T, txs []*persistence.PortfolioEvent) bool { return assert.Equals(t, 3, len(txs)) }, - wantSecs: func(t *testing.T, secs []*portfoliov1.Security) bool { + wantSecs: func(t *testing.T, secs []*persistence.Security) bool { return assert.Equals(t, 2, len(secs)) }, + wantLss: func(t *testing.T, lss []*persistence.ListedSecurity) bool { + return assert.Equals(t, 2, len(lss)) + }, }, { name: "error", @@ -63,19 +68,23 @@ func TestImport(t *testing.T) { r: bytes.NewReader([]byte(`Date;Type;Value;Transaction Currency;Gross Amount;Currency Gross Amount;Exchange Rate;Fees;Taxes;Shares;ISIN;WKN;Ticker Symbol;Security Name;Note this;will;be;an;error`)), }, - wantTxs: func(t *testing.T, txs []*portfoliov1.PortfolioEvent) bool { + wantTxs: func(t *testing.T, txs []*persistence.PortfolioEvent) bool { return assert.Equals(t, 0, len(txs)) }, - wantSecs: func(t *testing.T, secs []*portfoliov1.Security) bool { + wantSecs: func(t *testing.T, secs []*persistence.Security) bool { return assert.Equals(t, 0, len(secs)) }, + wantLss: func(t *testing.T, lss []*persistence.ListedSecurity) bool { + return assert.Equals(t, 0, len(lss)) + }, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - gotTxs, gotSecs := Import(tt.args.r, tt.args.pname) + gotTxs, gotSecs, gotLss := Import(tt.args.r, tt.args.pname) tt.wantTxs(t, gotTxs) tt.wantSecs(t, gotSecs) + tt.wantLss(t, gotLss) }) } } @@ -88,8 +97,9 @@ func Test_readLine(t *testing.T) { tests := []struct { name string args args - wantTx *portfoliov1.PortfolioEvent - wantSec *portfoliov1.Security + wantTx *persistence.PortfolioEvent + wantSec *persistence.Security + wantLs []*persistence.ListedSecurity wantErr assert.Want[error] }{ { @@ -101,26 +111,26 @@ func Test_readLine(t *testing.T) { return cr }(), }, - wantTx: &portfoliov1.PortfolioEvent{ - Id: "9e7b470b7566beca", - SecurityId: "US0378331005", - Type: portfoliov1.PortfolioEventType_PORTFOLIO_EVENT_TYPE_BUY, - Time: timestamppb.New(time.Date(2020, 1, 1, 0, 0, 0, 0, time.Local)), - Amount: 20, - Fees: portfoliov1.Value(1025), - Taxes: portfoliov1.Zero(), - Price: portfoliov1.Value(10708), - }, - wantSec: &portfoliov1.Security{ - Id: "US0378331005", + wantTx: &persistence.PortfolioEvent{ + ID: "9e7b470b7566beca", + SecurityID: "US0378331005", + Type: events.PortfolioEventTypeBuy, + Time: time.Date(2020, 1, 1, 0, 0, 0, 0, time.Local), + Amount: sql.NullFloat64{Float64: 20, Valid: true}, + Fees: currency.Value(1025), + Taxes: currency.Zero(), + Price: currency.Value(10708), + }, + wantSec: &persistence.Security{ + ID: "US0378331005", DisplayName: "Apple Inc.", - QuoteProvider: moneygopher.Ref(quote.QuoteProviderYF), - ListedOn: []*portfoliov1.ListedSecurity{ - { - SecurityId: "US0378331005", - Ticker: "APC.F", - Currency: "EUR", - }, + QuoteProvider: sql.NullString{String: quote.QuoteProviderYF, Valid: true}, + }, + wantLs: []*persistence.ListedSecurity{ + { + SecurityID: "US0378331005", + Ticker: "APC.F", + Currency: "EUR", }, }, }, @@ -133,26 +143,26 @@ func Test_readLine(t *testing.T) { return cr }(), }, - wantTx: &portfoliov1.PortfolioEvent{ - Id: "1070dafc882785a0", - SecurityId: "US00827B1061", - Type: portfoliov1.PortfolioEventType_PORTFOLIO_EVENT_TYPE_BUY, - Time: timestamppb.New(time.Date(2022, 1, 1, 9, 0, 0, 0, time.Local)), - Amount: 20, - Price: portfoliov1.Value(6040), - Fees: portfoliov1.Zero(), - Taxes: portfoliov1.Zero(), - }, - wantSec: &portfoliov1.Security{ - Id: "US00827B1061", + wantTx: &persistence.PortfolioEvent{ + ID: "1070dafc882785a0", + SecurityID: "US00827B1061", + Type: events.PortfolioEventTypeBuy, + Time: time.Date(2022, 1, 1, 9, 0, 0, 0, time.Local), + Amount: sql.NullFloat64{Float64: 20, Valid: true}, + Price: currency.Value(6040), + Fees: currency.Zero(), + Taxes: currency.Zero(), + }, + wantSec: &persistence.Security{ + ID: "US00827B1061", DisplayName: "Affirm Holdings Inc.", - QuoteProvider: moneygopher.Ref(quote.QuoteProviderYF), - ListedOn: []*portfoliov1.ListedSecurity{ - { - SecurityId: "US00827B1061", - Ticker: "AFRM", - Currency: "USD", - }, + QuoteProvider: sql.NullString{String: quote.QuoteProviderYF, Valid: true}, + }, + wantLs: []*persistence.ListedSecurity{ + { + SecurityID: "US00827B1061", + Ticker: "AFRM", + Currency: "USD", }, }, }, @@ -165,26 +175,26 @@ func Test_readLine(t *testing.T) { return cr }(), }, - wantTx: &portfoliov1.PortfolioEvent{ - Id: "8bb43fed65b35685", - SecurityId: "DE0005557508", - Type: portfoliov1.PortfolioEventType_PORTFOLIO_EVENT_TYPE_SELL, - Time: timestamppb.New(time.Date(2022, 1, 1, 8, 0, 6, 0, time.Local)), - Amount: 103, - Fees: portfoliov1.Zero(), - Taxes: portfoliov1.Value(1830), - Price: portfoliov1.Value(1552), - }, - wantSec: &portfoliov1.Security{ - Id: "DE0005557508", + wantTx: &persistence.PortfolioEvent{ + ID: "8bb43fed65b35685", + SecurityID: "DE0005557508", + Type: events.PortfolioEventTypeSell, + Time: time.Date(2022, 1, 1, 8, 0, 6, 0, time.Local), + Amount: sql.NullFloat64{Float64: 103, Valid: true}, + Fees: currency.Zero(), + Taxes: currency.Value(1830), + Price: currency.Value(1552), + }, + wantSec: &persistence.Security{ + ID: "DE0005557508", DisplayName: "Deutsche Telekom AG", - QuoteProvider: moneygopher.Ref(quote.QuoteProviderYF), - ListedOn: []*portfoliov1.ListedSecurity{ - { - SecurityId: "DE0005557508", - Ticker: "DTE.F", - Currency: "EUR", - }, + QuoteProvider: sql.NullString{String: quote.QuoteProviderYF, Valid: true}, + }, + wantLs: []*persistence.ListedSecurity{ + { + SecurityID: "DE0005557508", + Ticker: "DTE.F", + Currency: "EUR", }, }, }, @@ -269,13 +279,14 @@ func Test_readLine(t *testing.T) { } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - gotTx, gotSec, err := readLine(tt.args.cr, tt.args.pname) + gotTx, gotSec, gotLs, err := readLine(tt.args.cr, tt.args.pname) if err != nil { tt.wantErr(t, err) return } - assert.Equals(t, tt.wantTx, gotTx, protocmp.Transform()) - assert.Equals(t, tt.wantSec, gotSec, protocmp.Transform()) + assert.Equals(t, tt.wantTx, gotTx) + assert.Equals(t, tt.wantSec, gotSec) + assert.Equals(t, tt.wantLs, gotLs) }) } } diff --git a/persistence/extra.go b/persistence/extra.go index 1587ea53..51f57381 100644 --- a/persistence/extra.go +++ b/persistence/extra.go @@ -2,9 +2,29 @@ package persistence import ( "context" + "hash/fnv" + "strconv" + "time" ) // ListedAs returns the listed securities for the security. func (s *Security) ListedAs(ctx context.Context, db *DB) ([]*ListedSecurity, error) { return db.ListListedSecuritiesBySecurityID(ctx, s.ID) + +} + +// MakeUniqueID creates a unique ID for the portfolio event based on a hash containing: +// - security ID +// - portfolio ID +// - date +// - amount +func (tx *PortfolioEvent) MakeUniqueID() { + h := fnv.New64a() + h.Write([]byte(tx.SecurityID)) + h.Write([]byte(tx.PortfolioID)) + h.Write([]byte(tx.Time.Format(time.DateTime))) + h.Write([]byte(strconv.FormatInt(int64(tx.Type), 10))) + h.Write([]byte(strconv.FormatInt(int64(tx.Amount.Float64), 10))) + + tx.ID = strconv.FormatUint(h.Sum64(), 16) } diff --git a/server/server.go b/server/server.go index 39642c7e..18d87582 100644 --- a/server/server.go +++ b/server/server.go @@ -27,9 +27,8 @@ import ( "github.com/99designs/gqlgen/graphql/playground" "github.com/oxisto/money-gopher/graph" "github.com/oxisto/money-gopher/persistence" + "github.com/oxisto/money-gopher/securities/quote" - "connectrpc.com/connect" - "connectrpc.com/vanguard" "github.com/lmittmann/tint" oauth2 "github.com/oxisto/oauth2go" "github.com/oxisto/oauth2go/login" @@ -51,8 +50,7 @@ type Options struct { // StartServer starts the server. func StartServer(pdb *persistence.DB, opts Options) (err error) { var ( - authSrv *oauth2.AuthorizationServer - transcoder *vanguard.Transcoder + authSrv *oauth2.AuthorizationServer ) authSrv = oauth2.NewServer( @@ -70,39 +68,12 @@ func StartServer(pdb *persistence.DB, opts Options) (err error) { ) go authSrv.ListenAndServe() - interceptors := connect.WithInterceptors( - NewSimpleLoggingInterceptor(), - NewAuthInterceptor(), - ) - - portfolioService := vanguard.NewService( - portfoliov1connect.NewPortfolioServiceHandler(portfolio.NewService( - portfolio.Options{ - DB: pdb, - SecuritiesClient: portfoliov1connect.NewSecuritiesServiceClient(http.DefaultClient, portfolio.DefaultSecuritiesServiceURL), - }, - ), interceptors)) - svc := securities.NewService(pdb) - securitiesService := vanguard.NewService( - portfoliov1connect.NewSecuritiesServiceHandler(svc, interceptors), - ) - - transcoder, err = vanguard.NewTranscoder([]*vanguard.Service{ - portfolioService, - securitiesService, - }, vanguard.WithCodec(func(tr vanguard.TypeResolver) vanguard.Codec { - codec := vanguard.NewJSONCodec(tr) - codec.MarshalOptions.EmitDefaultValues = true - return codec - })) - if err != nil { - slog.Error("transcoder failed", tint.Err(err)) - return err - } + // Create a quote updater + qu := quote.NewQuoteUpdater(pdb) + // Configure serve mux mux := http.NewServeMux() - mux.Handle("/", transcoder) - ConfigureGraphQL(mux, pdb, svc.(securities.QuoteUpdater)) + ConfigureGraphQL(mux, pdb, qu) err = http.ListenAndServe( ":8080", @@ -117,7 +88,7 @@ func StartServer(pdb *persistence.DB, opts Options) (err error) { func ConfigureGraphQL( mux *http.ServeMux, db *persistence.DB, - qu securities.QuoteUpdater, + qu quote.QuoteUpdater, ) (err error) { srv := handler.New(graph.NewExecutableSchema(graph.Config{Resolvers: &graph.Resolver{ DB: db, From b10069c577fd1cd2e4b01e82cea9f1dd561a02c0 Mon Sep 17 00:00:00 2001 From: Christian Banse Date: Wed, 1 Jan 2025 15:17:43 +0100 Subject: [PATCH 16/35] package finance test works --- finance/calculation.go | 26 +++++------ finance/calculation_test.go | 44 ++++++++++--------- finance/snapshot.go | 11 ++--- graph/schema.resolvers.go | 3 +- import/csv/csv_importer.go | 13 ++---- import/csv/csv_importer_test.go | 6 +-- persistence/extra.go | 2 +- persistence/models.go | 2 +- .../sql/migrations/0002_create_portfolio.sql | 2 +- 9 files changed, 51 insertions(+), 58 deletions(-) diff --git a/finance/calculation.go b/finance/calculation.go index 20174860..47e057f7 100644 --- a/finance/calculation.go +++ b/finance/calculation.go @@ -72,10 +72,10 @@ func (c *calculation) Apply(tx *persistence.PortfolioEvent) { // Increase the amount of shares and the fees by the value stored in the // transaction - c.Fees.PlusAssign(tx.Fees()) - c.Amount += tx.Amount.Float64 + c.Fees.PlusAssign(tx.Fees) + c.Amount += tx.Amount - total = currency.Times(tx.Price(), tx.Amount.Float64).Plus(tx.Fees()).Plus(tx.Taxes()) + total = currency.Times(tx.Price, tx.Amount).Plus(tx.Fees).Plus(tx.Taxes) // Decrease our cash c.Cash.MinusAssign(total) @@ -85,10 +85,10 @@ func (c *calculation) Apply(tx *persistence.PortfolioEvent) { // need to store this information to reduce the amount in the items // later when a sell transaction occurs. c.fifo = append(c.fifo, &fifoTx{ - amount: tx.Amount.Float64, - ppu: tx.Price(), - value: currency.Times(tx.Price(), tx.Amount.Float64), - fees: tx.Fees(), + amount: tx.Amount, + ppu: tx.Price, + value: currency.Times(tx.Price, tx.Amount), + fees: tx.Fees, }) case events.PortfolioEventTypeDeliveryOutbound: fallthrough @@ -100,17 +100,17 @@ func (c *calculation) Apply(tx *persistence.PortfolioEvent) { // Increase the fees and taxes by the value stored in the // transaction - c.Fees.PlusAssign(tx.Fees()) - c.Taxes.PlusAssign(tx.Taxes()) + c.Fees.PlusAssign(tx.Fees) + c.Taxes.PlusAssign(tx.Taxes) - total = currency.Times(tx.Price(), tx.Amount.Float64).Plus(tx.Fees()).Plus(tx.Taxes()) + total = currency.Times(tx.Price, tx.Amount).Plus(tx.Fees).Plus(tx.Taxes) // Increase our cash c.Cash.PlusAssign(total) // Store the amount of shares sold in this variable, since we later need // to decrease it while looping through the FIFO list - sold = tx.Amount.Float64 + sold = tx.Amount // Calculate the remaining shares (if any) c.Amount -= sold @@ -153,10 +153,10 @@ func (c *calculation) Apply(tx *persistence.PortfolioEvent) { } case events.PortfolioEventTypeDepositCash: // Add to the cash - c.Cash.PlusAssign(tx.Price()) + c.Cash.PlusAssign(tx.Price) case events.PortfolioEventTypeWithdrawCash: // Remove from the cash - c.Cash.MinusAssign(tx.Price()) + c.Cash.MinusAssign(tx.Price) } } diff --git a/finance/calculation_test.go b/finance/calculation_test.go index f71d26c8..50a7745e 100644 --- a/finance/calculation_test.go +++ b/finance/calculation_test.go @@ -21,6 +21,7 @@ import ( "testing" "github.com/oxisto/assert" + "github.com/oxisto/money-gopher/currency" "github.com/oxisto/money-gopher/persistence" "github.com/oxisto/money-gopher/portfolio/events" ) @@ -40,64 +41,65 @@ func TestNewCalculation(t *testing.T) { txs: []*persistence.PortfolioEvent{ { Type: events.PortfolioEventTypeDepositCash, - Price: persistence.Value(500000), + Price: currency.Value(500000), }, { Type: events.PortfolioEventTypeBuy, Amount: 5, - Price: persistence.Value(18110), - Fees: persistence.Value(716), + Price: currency.Value(18110), + Fees: currency.Value(716), }, { Type: events.PortfolioEventTypeSell, Amount: 2, - Price: persistence.Value(30430), - Fees: persistence.Value(642), - Taxes: persistence.Value(1632), + Price: currency.Value(30430), + Fees: currency.Value(642), + Taxes: currency.Value(1632), }, { Type: events.PortfolioEventTypeBuy, Amount: 5, - Price: persistence.Value(29000), - Fees: persistence.Value(853), + Price: currency.Value(29000), + Fees: currency.Value(853), }, { Type: events.PortfolioEventTypeSell, Amount: 3, - Price: persistence.Value(22000), - Fees: persistence.Value(845), + Price: currency.Value(22000), + Fees: currency.Value(845), }, { Type: events.PortfolioEventTypeBuy, Amount: 5, - Price: persistence.Value(20330), - Fees: persistence.Value(744), + Price: currency.Value(20330), + Fees: currency.Value(744), }, { Type: events.PortfolioEventTypeBuy, Amount: 5, - Price: persistence.Value(19645), - Fees: persistence.Value(736), + Price: currency.Value(19645), + Fees: currency.Value(736), }, { Type: events.PortfolioEventTypeBuy, Amount: 10, - Price: persistence.Value(14655), - Fees: persistence.Value(856), + Price: currency.Value(14655), + Fees: currency.Value(856), }, }, }, want: func(t *testing.T, c *calculation) bool { return true && assert.Equals(t, 25, c.Amount) && - assert.Equals(t, 491425, int(c.NetValue().Value)) && - assert.Equals(t, 494614, int(c.GrossValue().Value)) && - assert.Equals(t, 19657, int(c.NetPrice().Value)) && - assert.Equals(t, 19785, int(c.GrossPrice().Value)) && - assert.Equals(t, 44099, int(c.Cash.Value)) + assert.Equals(t, 491425, int(c.NetValue().Amount)) && + assert.Equals(t, 494614, int(c.GrossValue().Amount)) && + assert.Equals(t, 19657, int(c.NetPrice().Amount)) && + assert.Equals(t, 19785, int(c.GrossPrice().Amount)) && + assert.Equals(t, 44099, int(c.Cash.Amount)) }, }, } + for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { got := NewCalculation(tt.args.txs) diff --git a/finance/snapshot.go b/finance/snapshot.go index dda23095..f779e58f 100644 --- a/finance/snapshot.go +++ b/finance/snapshot.go @@ -114,7 +114,7 @@ func BuildSnapshot( // Calculate loss and gains pos.ProfitOrLoss = currency.Minus(pos.MarketValue, pos.PurchaseValue) - pos.Gains = float64(currency.Minus(pos.MarketValue, pos.PurchaseValue).Value) / float64(pos.PurchaseValue.Value) + pos.Gains = float64(currency.Minus(pos.MarketValue, pos.PurchaseValue).Amount) / float64(pos.PurchaseValue.Amount) // Add to total value(s) snap.TotalPurchaseValue.PlusAssign(pos.PurchaseValue) @@ -126,7 +126,7 @@ func BuildSnapshot( } // Calculate total gains - snap.TotalGains = float64(currency.Minus(snap.TotalMarketValue, snap.TotalPurchaseValue).Value) / float64(snap.TotalPurchaseValue.Value) + snap.TotalGains = float64(currency.Minus(snap.TotalMarketValue, snap.TotalPurchaseValue).Amount) / float64(snap.TotalPurchaseValue.Amount) // Calculate total portfolio value snap.TotalPortfolioValue = snap.TotalMarketValue.Plus(snap.Cash) @@ -173,12 +173,9 @@ func marketPrice( ) *currency.Currency { ls, _ := provider.ListListedSecuritiesBySecurityID(context.Background(), name) - if ls == nil || !ls[0].LatestQuote.Valid { + if ls == nil || ls[0].LatestQuote == nil { return netPrice } else { - return ¤cy.Currency{ - Value: int32(ls[0].LatestQuote.Int64), - Symbol: ls[0].Currency, - } + return ls[0].LatestQuote } } diff --git a/graph/schema.resolvers.go b/graph/schema.resolvers.go index e726ba51..f685ff2d 100644 --- a/graph/schema.resolvers.go +++ b/graph/schema.resolvers.go @@ -14,7 +14,6 @@ import ( "github.com/oxisto/money-gopher/finance" "github.com/oxisto/money-gopher/models" "github.com/oxisto/money-gopher/persistence" - "github.com/oxisto/money-gopher/securities/quote" ) // Security is the resolver for the security field. @@ -109,7 +108,7 @@ func (r *mutationResolver) UpdateSecurity(ctx context.Context, id string, input // TriggerQuoteUpdate is the resolver for the triggerQuoteUpdate field. func (r *mutationResolver) TriggerQuoteUpdate(ctx context.Context, securityIDs []string) (b bool, err error) { - err = quote.UpdateQuotes(ctx, securityIDs, r.DB) + err = r.QuoteUpdater.UpdateQuotes(ctx, securityIDs) if err != nil { return false, err } diff --git a/import/csv/csv_importer.go b/import/csv/csv_importer.go index c3d656d7..5b6a7fa0 100644 --- a/import/csv/csv_importer.go +++ b/import/csv/csv_importer.go @@ -147,10 +147,10 @@ func readLine(cr *csv.Reader, pname string) ( // Calculate the price if tx.Type == events.PortfolioEventTypeBuy || tx.Type == events.PortfolioEventTypeDeliveryInbound { - tx.Price = currency.Divide(currency.Minus(value, tx.Fees), tx.Amount.Float64) + tx.Price = currency.Divide(currency.Minus(value, tx.Fees), tx.Amount) } else if tx.Type == events.PortfolioEventTypeSell || tx.Type == events.PortfolioEventTypeDeliveryOutbound { - tx.Price = currency.Times(currency.Divide(currency.Minus(currency.Minus(value, tx.Fees), tx.Taxes), tx.Amount.Float64), -1) + tx.Price = currency.Times(currency.Divide(currency.Minus(currency.Minus(value, tx.Fees), tx.Taxes), tx.Amount), -1) } sec = new(persistence.Security) @@ -206,11 +206,7 @@ func txTime(s string) (t time.Time, err error) { return t, nil } -func parseFloat64(s string) (nf sql.NullFloat64, err error) { - var ( - f float64 - ) - +func parseFloat64(s string) (f float64, err error) { // We assume that the float is in German locale (this might not be true for // all users), so we need to convert it s = strings.ReplaceAll(s, ".", "") @@ -218,9 +214,8 @@ func parseFloat64(s string) (nf sql.NullFloat64, err error) { f, err = strconv.ParseFloat(s, 32) if err != nil { - return sql.NullFloat64{Valid: false}, err + return 0, err } - nf = sql.NullFloat64{Float64: f, Valid: true} return } diff --git a/import/csv/csv_importer_test.go b/import/csv/csv_importer_test.go index 2a95cc5d..d8d6424b 100644 --- a/import/csv/csv_importer_test.go +++ b/import/csv/csv_importer_test.go @@ -116,7 +116,7 @@ func Test_readLine(t *testing.T) { SecurityID: "US0378331005", Type: events.PortfolioEventTypeBuy, Time: time.Date(2020, 1, 1, 0, 0, 0, 0, time.Local), - Amount: sql.NullFloat64{Float64: 20, Valid: true}, + Amount: 20, Fees: currency.Value(1025), Taxes: currency.Zero(), Price: currency.Value(10708), @@ -148,7 +148,7 @@ func Test_readLine(t *testing.T) { SecurityID: "US00827B1061", Type: events.PortfolioEventTypeBuy, Time: time.Date(2022, 1, 1, 9, 0, 0, 0, time.Local), - Amount: sql.NullFloat64{Float64: 20, Valid: true}, + Amount: 20, Price: currency.Value(6040), Fees: currency.Zero(), Taxes: currency.Zero(), @@ -180,7 +180,7 @@ func Test_readLine(t *testing.T) { SecurityID: "DE0005557508", Type: events.PortfolioEventTypeSell, Time: time.Date(2022, 1, 1, 8, 0, 6, 0, time.Local), - Amount: sql.NullFloat64{Float64: 103, Valid: true}, + Amount: 103, Fees: currency.Zero(), Taxes: currency.Value(1830), Price: currency.Value(1552), diff --git a/persistence/extra.go b/persistence/extra.go index 51f57381..982e53ec 100644 --- a/persistence/extra.go +++ b/persistence/extra.go @@ -24,7 +24,7 @@ func (tx *PortfolioEvent) MakeUniqueID() { h.Write([]byte(tx.PortfolioID)) h.Write([]byte(tx.Time.Format(time.DateTime))) h.Write([]byte(strconv.FormatInt(int64(tx.Type), 10))) - h.Write([]byte(strconv.FormatInt(int64(tx.Amount.Float64), 10))) + h.Write([]byte(strconv.FormatInt(int64(tx.Amount), 10))) tx.ID = strconv.FormatUint(h.Sum64(), 16) } diff --git a/persistence/models.go b/persistence/models.go index 6495d65b..48908db7 100644 --- a/persistence/models.go +++ b/persistence/models.go @@ -48,7 +48,7 @@ type PortfolioEvent struct { Time time.Time PortfolioID string SecurityID string - Amount sql.NullFloat64 + Amount float64 Price *currency.Currency Fees *currency.Currency Taxes *currency.Currency diff --git a/persistence/sql/migrations/0002_create_portfolio.sql b/persistence/sql/migrations/0002_create_portfolio.sql index 3391b197..6410ebba 100644 --- a/persistence/sql/migrations/0002_create_portfolio.sql +++ b/persistence/sql/migrations/0002_create_portfolio.sql @@ -14,7 +14,7 @@ CREATE TABLE time DATETIME NOT NULL, portfolio_id TEXT NOT NULL, security_id TEXT NOT NULL, - amount REAL, + amount REAL NOT NULL, price JSONB, fees JSONB, taxes JSONB From 14af0db1f5c0799ed13bbe8732e45bf7d368bed2 Mon Sep 17 00:00:00 2001 From: Christian Banse Date: Wed, 1 Jan 2025 21:48:43 +0100 Subject: [PATCH 17/35] More graphql conversion --- cli/commands/account.go | 6 +- cli/commands/portfolio.go | 151 ++++++++++++------------ cli/commands/securities_test.go | 14 +-- graph/schema.resolvers_test.go | 77 ++++++++++++ internal/testdata/securities.go | 29 +++++ securities/quote/quote_provider_test.go | 28 +++++ securities/quote/quote_test.go | 21 ---- 7 files changed, 216 insertions(+), 110 deletions(-) create mode 100644 graph/schema.resolvers_test.go create mode 100644 internal/testdata/securities.go create mode 100644 securities/quote/quote_provider_test.go diff --git a/cli/commands/account.go b/cli/commands/account.go index c0e704c7..d053abae 100644 --- a/cli/commands/account.go +++ b/cli/commands/account.go @@ -18,11 +18,9 @@ package commands import ( "context" - "fmt" mcli "github.com/oxisto/money-gopher/cli" - "connectrpc.com/connect" "github.com/urfave/cli/v3" ) @@ -46,7 +44,7 @@ var BankAccountCmd = &cli.Command{ // CreateBankAccount creates a new bank account. func CreateBankAccount(ctx context.Context, cmd *cli.Command) error { - s := mcli.FromContext(ctx) + /*s := mcli.FromContext(ctx) res, err := s.PortfolioClient.CreateBankAccount( context.Background(), connect.NewRequest(&portfoliov1.CreateBankAccountRequest{ @@ -60,6 +58,6 @@ func CreateBankAccount(ctx context.Context, cmd *cli.Command) error { return err } - fmt.Fprint(cmd.Writer, res.Msg) + fmt.Fprint(cmd.Writer, res.Msg)*/ return nil } diff --git a/cli/commands/portfolio.go b/cli/commands/portfolio.go index 11504b16..07b5635e 100644 --- a/cli/commands/portfolio.go +++ b/cli/commands/portfolio.go @@ -18,18 +18,11 @@ package commands import ( "context" - "fmt" - "io" - "os" - "strings" - "time" mcli "github.com/oxisto/money-gopher/cli" - "connectrpc.com/connect" "github.com/fatih/color" "github.com/urfave/cli/v3" - "google.golang.org/protobuf/types/known/timestamppb" ) // PortfolioCmd is the command for portfolio related commands. @@ -97,54 +90,55 @@ var PortfolioCmd = &cli.Command{ // ListPortfolio lists all portfolios. func ListPortfolio(ctx context.Context, cmd *cli.Command) error { - s := mcli.FromContext(ctx) - res, err := s.PortfolioClient.ListPortfolios( - context.Background(), - connect.NewRequest(&portfoliov1.ListPortfoliosRequest{}), - ) - if err != nil { - return err - } else { - in := `This is a list of all portfolios. -` + /* + s := mcli.FromContext(ctx) + res, err := s.PortfolioClient.ListPortfolios( + context.Background(), + connect.NewRequest(&portfoliov1.ListPortfoliosRequest{}), + ) + if err != nil { + return err + } else { + in := `This is a list of all portfolios. + ` - for _, portfolio := range res.Msg.Portfolios { - snapshot, _ := s.PortfolioClient.GetPortfolioSnapshot( - context.Background(), - connect.NewRequest(&portfoliov1.GetPortfolioSnapshotRequest{ - PortfolioId: portfolio.Id, - }), - ) + for _, portfolio := range res.Msg.Portfolios { + snapshot, _ := s.PortfolioClient.GetPortfolioSnapshot( + context.Background(), + connect.NewRequest(&portfoliov1.GetPortfolioSnapshotRequest{ + PortfolioId: portfolio.Id, + }), + ) - in += fmt.Sprintf(` -| %-*s | -| %s | %s | -| %-*s | %*s | -| %-*s | %*s | -`, - 15+15+3, color.New(color.FgWhite, color.Bold).Sprint(portfolio.DisplayName), - strings.Repeat("-", 15), - strings.Repeat("-", 15), - 15, "Market Value", - 15, snapshot.Msg.TotalMarketValue.Pretty(), - 15, "Performance", - 15, fmt.Sprintf("%s € (%s %%)", - greenOrRed(float64(snapshot.Msg.TotalProfitOrLoss.Value/100)), - greenOrRed(snapshot.Msg.TotalGains*100), - ), - ) - } + in += fmt.Sprintf(` + | %-*s | + | %s | %s | + | %-*s | %*s | + | %-*s | %*s | + `, + 15+15+3, color.New(color.FgWhite, color.Bold).Sprint(portfolio.DisplayName), + strings.Repeat("-", 15), + strings.Repeat("-", 15), + 15, "Market Value", + 15, snapshot.Msg.TotalMarketValue.Pretty(), + 15, "Performance", + 15, fmt.Sprintf("%s € (%s %%)", + greenOrRed(float64(snapshot.Msg.TotalProfitOrLoss.Value/100)), + greenOrRed(snapshot.Msg.TotalGains*100), + ), + ) + } - //out, _ := glamour.Render(in, "dark") - fmt.Println(in) - } + //out, _ := glamour.Render(in, "dark") + fmt.Println(in) + }*/ return nil } // CreatePortfolio creates a new portfolio. func CreatePortfolio(ctx context.Context, cmd *cli.Command) error { - s := mcli.FromContext(ctx) + /*s := mcli.FromContext(ctx) res, err := s.PortfolioClient.CreatePortfolio( context.Background(), connect.NewRequest(&portfoliov1.CreatePortfolioRequest{ @@ -159,13 +153,13 @@ func CreatePortfolio(ctx context.Context, cmd *cli.Command) error { return err } - fmt.Println(res.Msg) + fmt.Println(res.Msg)*/ return nil } // ShowPortfolio shows details about a portfolio. func ShowPortfolio(ctx context.Context, cmd *cli.Command) error { - s := mcli.FromContext(ctx) + /*s := mcli.FromContext(ctx) res, err := s.PortfolioClient.GetPortfolioSnapshot( context.Background(), connect.NewRequest(&portfoliov1.GetPortfolioSnapshotRequest{ @@ -177,7 +171,7 @@ func ShowPortfolio(ctx context.Context, cmd *cli.Command) error { return err } - fmt.Println(res.Msg) + fmt.Println(res.Msg)*/ return nil } @@ -191,35 +185,38 @@ func greenOrRed(f float64) string { // CreateTransaction creates a transaction. func CreateTransaction(ctx context.Context, cmd *cli.Command) error { - s := mcli.FromContext(ctx) - var req = connect.NewRequest(&portfoliov1.CreatePortfolioTransactionRequest{ - Transaction: &portfoliov1.PortfolioEvent{ - PortfolioId: cmd.String("portfolio-id"), - SecurityId: cmd.String("security-id"), - Type: eventTypeFrom(cmd.String("type")), - Amount: cmd.Float("amount"), - Time: timeOrNow(cmd.Timestamp("time")), - Price: portfoliov1.Value(int32(cmd.Float("price") * 100)), - Fees: portfoliov1.Value(int32(cmd.Float("fees") * 100)), - Taxes: portfoliov1.Value(int32(cmd.Float("taxes") * 100)), - }, - }) + /* - res, err := s.PortfolioClient.CreatePortfolioTransaction(context.Background(), req) - if err != nil { - return err - } + s := mcli.FromContext(ctx) + var req = connect.NewRequest(&portfoliov1.CreatePortfolioTransactionRequest{ + Transaction: &portfoliov1.PortfolioEvent{ + PortfolioId: cmd.String("portfolio-id"), + SecurityId: cmd.String("security-id"), + Type: eventTypeFrom(cmd.String("type")), + Amount: cmd.Float("amount"), + Time: timeOrNow(cmd.Timestamp("time")), + Price: portfoliov1.Value(int32(cmd.Float("price") * 100)), + Fees: portfoliov1.Value(int32(cmd.Float("fees") * 100)), + Taxes: portfoliov1.Value(int32(cmd.Float("taxes") * 100)), + }, + }) - fmt.Printf("Successfully created a %s transaction (%s) for security %s in %s.\n", - color.CyanString(cmd.String("type")), - color.GreenString(res.Msg.Id), - color.CyanString(res.Msg.SecurityId), - color.CyanString(res.Msg.PortfolioId), - ) + res, err := s.PortfolioClient.CreatePortfolioTransaction(context.Background(), req) + if err != nil { + return err + } + + fmt.Printf("Successfully created a %s transaction (%s) for security %s in %s.\n", + color.CyanString(cmd.String("type")), + color.GreenString(res.Msg.Id), + color.CyanString(res.Msg.SecurityId), + color.CyanString(res.Msg.PortfolioId), + )*/ return nil } +/* func eventTypeFrom(typ string) portfoliov1.PortfolioEventType { if typ == "buy" { return portfoliov1.PortfolioEventType_PORTFOLIO_EVENT_TYPE_BUY @@ -240,11 +237,11 @@ func timeOrNow(t time.Time) *timestamppb.Timestamp { } return timestamppb.New(t) -} +}*/ // ImportTransactions imports transactions from a CSV file func ImportTransactions(ctx context.Context, cmd *cli.Command) error { - s := mcli.FromContext(ctx) + /*s := mcli.FromContext(ctx) // Read from args[1] f, err := os.Open(cmd.String("csv-file")) if err != nil { @@ -268,13 +265,13 @@ func ImportTransactions(ctx context.Context, cmd *cli.Command) error { return err } - fmt.Println(res.Msg) + fmt.Println(res.Msg)*/ return nil } // PredictPortfolios predicts the portfolios for shell completion. func PredictPortfolios(ctx context.Context, cmd *cli.Command) { - s := mcli.FromContext(ctx) + /*s := mcli.FromContext(ctx) res, err := s.PortfolioClient.ListPortfolios( context.Background(), connect.NewRequest(&portfoliov1.ListPortfoliosRequest{}), @@ -285,5 +282,5 @@ func PredictPortfolios(ctx context.Context, cmd *cli.Command) { for _, p := range res.Msg.Portfolios { fmt.Fprintf(cmd.Root().Writer, "%s:%s\n", p.Id, p.DisplayName) - } + }*/ } diff --git a/cli/commands/securities_test.go b/cli/commands/securities_test.go index b0d8073b..ea62ae47 100644 --- a/cli/commands/securities_test.go +++ b/cli/commands/securities_test.go @@ -21,8 +21,9 @@ import ( "context" "testing" - moneygopher "github.com/oxisto/money-gopher" "github.com/oxisto/money-gopher/internal" + "github.com/oxisto/money-gopher/internal/persistencetest" + "github.com/oxisto/money-gopher/internal/testdata" "github.com/oxisto/money-gopher/internal/testing/clitest" "github.com/oxisto/money-gopher/internal/testing/servertest" "github.com/oxisto/money-gopher/persistence" @@ -32,12 +33,9 @@ import ( ) func TestUpdateQuote(t *testing.T) { - srv := servertest.NewServer(internal.NewTestDB(t, func(db *persistence.DB) { - ops := persistence.Ops[*portfoliov1.Security](db) - ops.Replace(&portfoliov1.Security{ - Id: "mysecurity", - QuoteProvider: moneygopher.Ref("mock"), - }) + srv := servertest.NewServer(persistencetest.NewTestDB(t, func(db *persistence.DB) { + _, err := db.CreateSecurity(context.Background(), testdata.TestCreateSecurityParams) + assert.NoError(t, err) })) defer srv.Close() @@ -56,7 +54,7 @@ func TestUpdateQuote(t *testing.T) { ctx: clitest.NewSessionContext(t, srv), cmd: clitest.MockCommand(t, SecuritiesCmd.Command("update-quote").Flags, - "--security-ids", "mysecurity", + "--security-ids", testdata.TestCreateSecurityParams.ID, ), }, }, diff --git a/graph/schema.resolvers_test.go b/graph/schema.resolvers_test.go new file mode 100644 index 00000000..c781abc1 --- /dev/null +++ b/graph/schema.resolvers_test.go @@ -0,0 +1,77 @@ +package graph + +import ( + "context" + "testing" + + "github.com/oxisto/assert" + "github.com/oxisto/money-gopher/internal/persistencetest" + "github.com/oxisto/money-gopher/models" + "github.com/oxisto/money-gopher/persistence" +) + +func Test_mutationResolver_CreateSecurity(t *testing.T) { + type fields struct { + Resolver *Resolver + } + type args struct { + ctx context.Context + input models.SecurityInput + } + tests := []struct { + name string + fields fields + args args + want *persistence.Security + wantDB assert.Want[*persistence.DB] + wantErr bool + }{ + { + name: "happy path", + fields: fields{ + Resolver: &Resolver{ + DB: persistencetest.NewTestDB(t), + }, + }, + args: args{ + ctx: context.TODO(), + input: models.SecurityInput{ + ID: "DE1234567890", + DisplayName: "My Security", + ListedAs: []*models.ListedSecurityInput{ + { + Ticker: "TICK", + Currency: "USD", + }, + }, + }, + }, + want: &persistence.Security{ + ID: "DE1234567890", + DisplayName: "My Security", + }, + wantDB: func(t *testing.T, db *persistence.DB) bool { + _, err := db.Queries.GetSecurity(context.Background(), "DE1234567890") + assert.NoError(t, err) + + ls, err := db.Queries.ListListedSecuritiesBySecurityID(context.Background(), "DE1234567890") + return len(ls) == 1 && assert.NoError(t, err) + }, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + r := &mutationResolver{ + Resolver: tt.fields.Resolver, + } + got, err := r.CreateSecurity(tt.args.ctx, tt.args.input) + if (err != nil) != tt.wantErr { + t.Errorf("mutationResolver.CreateSecurity() error = %v, wantErr %v", err, tt.wantErr) + return + } + assert.Equals(t, tt.want, got) + tt.wantDB(t, tt.fields.Resolver.DB) + }) + } +} diff --git a/internal/testdata/securities.go b/internal/testdata/securities.go new file mode 100644 index 00000000..da7b9274 --- /dev/null +++ b/internal/testdata/securities.go @@ -0,0 +1,29 @@ +package testdata + +import ( + "database/sql" + + "github.com/oxisto/money-gopher/persistence" +) + +// TestSecurity is a test security. +var TestSecurity = &persistence.Security{ + ID: "DE1234567890", + DisplayName: "My Security", + QuoteProvider: sql.NullString{String: "mock", Valid: true}, +} + +// TestListedSecurity is a listed security for [TestSecurity] that has a ticker +// "TICK" and currency "USD". +var TestListedSecurity = &persistence.ListedSecurity{ + SecurityID: TestSecurity.ID, + Ticker: "TICK", + Currency: "USD", +} + +// TestCreateSecurityParams is a test security creation parameter. +var TestCreateSecurityParams = persistence.CreateSecurityParams{ + ID: TestSecurity.ID, + DisplayName: TestSecurity.DisplayName, + QuoteProvider: TestSecurity.QuoteProvider, +} diff --git a/securities/quote/quote_provider_test.go b/securities/quote/quote_provider_test.go new file mode 100644 index 00000000..cea24b2d --- /dev/null +++ b/securities/quote/quote_provider_test.go @@ -0,0 +1,28 @@ +package quote + +import ( + "context" + "time" + + "github.com/oxisto/money-gopher/currency" + "github.com/oxisto/money-gopher/persistence" +) + +const QuoteProviderMock = "mock" + +type mockQP struct { +} + +func (m *mockQP) LatestQuote(ctx context.Context, ls *persistence.ListedSecurity) (quote *currency.Currency, t time.Time, err error) { + return currency.Value(100), time.Now(), nil +} + +type mockQuoteProvider struct{} + +func (mockQuoteProvider) LatestQuote(_ context.Context, _ *persistence.ListedSecurity) (quote *currency.Currency, t time.Time, err error) { + return currency.Value(100), time.Date(1, 0, 0, 0, 0, 0, 0, time.UTC), nil +} + +func init() { + RegisterQuoteProvider(QuoteProviderMock, &mockQP{}) +} diff --git a/securities/quote/quote_test.go b/securities/quote/quote_test.go index 1226f457..ea6e6d18 100644 --- a/securities/quote/quote_test.go +++ b/securities/quote/quote_test.go @@ -20,34 +20,13 @@ import ( "context" "database/sql" "testing" - "time" - "github.com/oxisto/money-gopher/currency" "github.com/oxisto/money-gopher/internal" "github.com/oxisto/money-gopher/persistence" "github.com/oxisto/assert" ) -const QuoteProviderMock = "mock" - -type mockQP struct { -} - -func (m *mockQP) LatestQuote(ctx context.Context, ls *persistence.ListedSecurity) (quote *currency.Currency, t time.Time, err error) { - return currency.Value(100), time.Now(), nil -} - -type mockQuoteProvider struct{} - -func (mockQuoteProvider) LatestQuote(_ context.Context, _ *persistence.ListedSecurity) (quote *currency.Currency, t time.Time, err error) { - return currency.Value(100), time.Date(1, 0, 0, 0, 0, 0, 0, time.UTC), nil -} - -func init() { - RegisterQuoteProvider(QuoteProviderMock, &mockQP{}) -} - func Test_qu_updateQuote(t *testing.T) { type fields struct { db *persistence.DB From 93f9bffc67218c185cbfd4940b8fd9a2dd8f4603 Mon Sep 17 00:00:00 2001 From: Christian Banse Date: Thu, 2 Jan 2025 01:04:01 +0100 Subject: [PATCH 18/35] Triggering quotes --- cli/commands/securities.go | 8 +- cli/commands/securities_test.go | 16 ++- go.mod | 5 +- go.sum | 8 -- graph/generated.go | 180 ++++++++++++++++++-------------- graph/schema.graphqls | 4 +- graph/schema.resolvers.go | 28 +++-- money-gopher.code-workspace | 1 + securities/quote/quote.go | 30 ++++-- 9 files changed, 167 insertions(+), 113 deletions(-) diff --git a/cli/commands/securities.go b/cli/commands/securities.go index 0f612479..38c0e64a 100644 --- a/cli/commands/securities.go +++ b/cli/commands/securities.go @@ -19,8 +19,10 @@ package commands import ( "context" + "fmt" mcli "github.com/oxisto/money-gopher/cli" + "github.com/oxisto/money-gopher/currency" "github.com/shurcooL/graphql" "github.com/urfave/cli/v3" @@ -115,7 +117,9 @@ func UpdateQuote(ctx context.Context, cmd *cli.Command) (err error) { s := mcli.FromContext(ctx) var query struct { - TriggerQuoteUpdate bool `graphql:"triggerQuoteUpdate(securityIDs: $IDs)" json:"security"` + TriggerQuoteUpdate []struct { + LatestQuote *currency.Currency `json:"latestQuote"` + } `graphql:"triggerQuoteUpdate(securityIDs: $IDs)" json:"security"` } var ids []graphql.String @@ -130,6 +134,8 @@ func UpdateQuote(ctx context.Context, cmd *cli.Command) (err error) { return err } + fmt.Fprintln(cmd.Writer, query.TriggerQuoteUpdate) + return err } diff --git a/cli/commands/securities_test.go b/cli/commands/securities_test.go index ea62ae47..2111e462 100644 --- a/cli/commands/securities_test.go +++ b/cli/commands/securities_test.go @@ -47,6 +47,7 @@ func TestUpdateQuote(t *testing.T) { name string args args wantErr bool + wantRec assert.Want[*clitest.CommandRecorder] }{ { name: "happy path", @@ -57,14 +58,25 @@ func TestUpdateQuote(t *testing.T) { "--security-ids", testdata.TestCreateSecurityParams.ID, ), }, + wantRec: func(t *testing.T, rec *clitest.CommandRecorder) bool { + return assert.Equals(t, `{ + "security": { + "id": "US0378331005"`, rec.String(), + ) + }, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { + rec := clitest.Record(tt.args.cmd) + if err := UpdateQuote(tt.args.ctx, tt.args.cmd); (err != nil) != tt.wantErr { t.Errorf("UpdateQuote() error = %v, wantErr %v", err, tt.wantErr) } + if tt.wantRec != nil { + tt.wantRec(t, rec) + } }) } } @@ -152,12 +164,10 @@ func TestListSecurities(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - rec := clitest.NewCommandRecorder() - tt.args.cmd.Writer = rec + rec := clitest.Record(tt.args.cmd) if err := ListSecurities(tt.args.ctx, tt.args.cmd); (err != nil) != tt.wantErr { t.Errorf("ListSecurities() error = %v, wantErr %v", err, tt.wantErr) } - if tt.wantRec != nil { tt.wantRec(t, rec) } diff --git a/go.mod b/go.mod index 9153c3e5..308e2165 100644 --- a/go.mod +++ b/go.mod @@ -4,7 +4,6 @@ go 1.23.4 require ( connectrpc.com/connect v1.16.2 - connectrpc.com/vanguard v0.3.0 github.com/99designs/gqlgen v0.17.61 github.com/MicahParks/keyfunc/v3 v3.3.5 github.com/fatih/color v1.18.0 @@ -20,6 +19,7 @@ require ( github.com/urfave/cli/v3 v3.0.0-beta1 github.com/vektah/gqlparser/v2 v2.5.20 golang.org/x/net v0.33.0 + golang.org/x/sync v0.10.0 google.golang.org/protobuf v1.36.1 ) @@ -37,11 +37,8 @@ require ( go.uber.org/multierr v1.11.0 // indirect golang.org/x/crypto v0.31.0 // indirect golang.org/x/oauth2 v0.22.0 // indirect - golang.org/x/sync v0.10.0 // indirect golang.org/x/sys v0.28.0 // indirect golang.org/x/time v0.5.0 // indirect - google.golang.org/genproto/googleapis/api v0.0.0-20241230172942-26aa7a208def // indirect - google.golang.org/genproto/googleapis/rpc v0.0.0-20241223144023-3abc09e42ca8 // indirect ) replace github.com/99designs/gqlgen v0.17.61 => github.com/oxisto/gqlgen v0.17.62-0.20241227140449-4bf1c5c27bad diff --git a/go.sum b/go.sum index 7ca54c31..16dee170 100644 --- a/go.sum +++ b/go.sum @@ -1,7 +1,5 @@ connectrpc.com/connect v1.16.2 h1:ybd6y+ls7GOlb7Bh5C8+ghA6SvCBajHwxssO2CGFjqE= connectrpc.com/connect v1.16.2/go.mod h1:n2kgwskMHXC+lVqb18wngEpF95ldBHXjZYJussz5FRc= -connectrpc.com/vanguard v0.3.0 h1:prUKFm8rYDwvpvnOSoqdUowPMK0tRA0pbSrQoMd6Zng= -connectrpc.com/vanguard v0.3.0/go.mod h1:nxQ7+N6qhBiQczqGwdTw4oCqx1rDryIt20cEdECqToM= github.com/MicahParks/jwkset v0.5.19 h1:XZCsgJv05DBCvxEHYEHlSafqiuVn5ESG0VRB331Fxhw= github.com/MicahParks/jwkset v0.5.19/go.mod h1:q8ptTGn/Z9c4MwbcfeCDssADeVQb3Pk7PnVxrvi+2QY= github.com/MicahParks/keyfunc/v3 v3.3.5 h1:7ceAJLUAldnoueHDNzF8Bx06oVcQ5CfJnYwNt1U3YYo= @@ -91,12 +89,6 @@ golang.org/x/text v0.21.0 h1:zyQAAkrwaneQ066sspRyJaG9VNi/YJ1NfzcGB3hZ/qo= golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ= golang.org/x/time v0.5.0 h1:o7cqy6amK/52YcAKIPlM3a+Fpj35zvRj2TP+e1xFSfk= golang.org/x/time v0.5.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM= -google.golang.org/genproto/googleapis/api v0.0.0-20241230172942-26aa7a208def h1:0Km0hi+g2KXbXL0+riZzSCKz23f4MmwicuEb00JeonI= -google.golang.org/genproto/googleapis/api v0.0.0-20241230172942-26aa7a208def/go.mod h1:u2DoMSpCXjrzqLdobRccQMc9wrnMAJ1DLng0a2yqM2Q= -google.golang.org/genproto/googleapis/rpc v0.0.0-20241223144023-3abc09e42ca8 h1:TqExAhdPaB60Ux47Cn0oLV07rGnxZzIsaRhQaqS666A= -google.golang.org/genproto/googleapis/rpc v0.0.0-20241223144023-3abc09e42ca8/go.mod h1:lcTa1sDdWEIHMWlITnIczmw5w60CF9ffkb8Z+DVmmjA= -google.golang.org/grpc v1.67.1 h1:zWnc1Vrcno+lHZCOofnIMvycFcc0QRGIzm9dhnDX68E= -google.golang.org/grpc v1.67.1/go.mod h1:1gLDyUQU7CTLJI90u3nXZ9ekeghjeM7pTDZlqFNg2AA= google.golang.org/protobuf v1.36.1 h1:yBPeRvTftaleIgM3PZ/WBIZ7XM/eEYAaEyCwvyjq/gk= google.golang.org/protobuf v1.36.1/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= diff --git a/graph/generated.go b/graph/generated.go index 54c38c50..00f56eea 100644 --- a/graph/generated.go +++ b/graph/generated.go @@ -60,8 +60,8 @@ type ComplexityRoot struct { } Currency struct { + Amount func(childComplexity int) int Symbol func(childComplexity int) int - Value func(childComplexity int) int } ListedSecurity struct { @@ -133,13 +133,13 @@ type ComplexityRoot struct { type ListedSecurityResolver interface { Security(ctx context.Context, obj *persistence.ListedSecurity) (*persistence.Security, error) - LatestQuote(ctx context.Context, obj *persistence.ListedSecurity) (*currency.Currency, error) + LatestQuoteTimestamp(ctx context.Context, obj *persistence.ListedSecurity) (*string, error) } type MutationResolver interface { CreateSecurity(ctx context.Context, input models.SecurityInput) (*persistence.Security, error) UpdateSecurity(ctx context.Context, id string, input models.SecurityInput) (*persistence.Security, error) - TriggerQuoteUpdate(ctx context.Context, securityIDs []string) (bool, error) + TriggerQuoteUpdate(ctx context.Context, securityIDs []string) ([]*persistence.ListedSecurity, error) } type PortfolioResolver interface { BankAccount(ctx context.Context, obj *persistence.Portfolio) (*persistence.BankAccount, error) @@ -195,19 +195,19 @@ func (e *executableSchema) Complexity(typeName, field string, childComplexity in return e.complexity.BankAccount.ID(childComplexity), true - case "Currency.symbol": - if e.complexity.Currency.Symbol == nil { + case "Currency.amount": + if e.complexity.Currency.Amount == nil { break } - return e.complexity.Currency.Symbol(childComplexity), true + return e.complexity.Currency.Amount(childComplexity), true - case "Currency.value": - if e.complexity.Currency.Value == nil { + case "Currency.symbol": + if e.complexity.Currency.Symbol == nil { break } - return e.complexity.Currency.Value(childComplexity), true + return e.complexity.Currency.Symbol(childComplexity), true case "ListedSecurity.currency": if e.complexity.ListedSecurity.Currency == nil { @@ -980,8 +980,8 @@ func (ec *executionContext) fieldContext_BankAccount_displayName(_ context.Conte return fc, nil } -func (ec *executionContext) _Currency_value(ctx context.Context, field graphql.CollectedField, obj *currency.Currency) (ret graphql.Marshaler) { - fc, err := ec.fieldContext_Currency_value(ctx, field) +func (ec *executionContext) _Currency_amount(ctx context.Context, field graphql.CollectedField, obj *currency.Currency) (ret graphql.Marshaler) { + fc, err := ec.fieldContext_Currency_amount(ctx, field) if err != nil { return graphql.Null } @@ -994,7 +994,7 @@ func (ec *executionContext) _Currency_value(ctx context.Context, field graphql.C }() resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (any, error) { ctx = rctx // use context from middleware stack in children - return obj.Value, nil + return obj.Amount, nil }) if err != nil { ec.Error(ctx, err) @@ -1011,7 +1011,7 @@ func (ec *executionContext) _Currency_value(ctx context.Context, field graphql.C return ec.marshalNInt2int32(ctx, field.Selections, res) } -func (ec *executionContext) fieldContext_Currency_value(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { +func (ec *executionContext) fieldContext_Currency_amount(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { fc = &graphql.FieldContext{ Object: "Currency", Field: field, @@ -1224,7 +1224,7 @@ func (ec *executionContext) _ListedSecurity_latestQuote(ctx context.Context, fie }() resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (any, error) { ctx = rctx // use context from middleware stack in children - return ec.resolvers.ListedSecurity().LatestQuote(rctx, obj) + return obj.LatestQuote, nil }) if err != nil { ec.Error(ctx, err) @@ -1242,12 +1242,12 @@ func (ec *executionContext) fieldContext_ListedSecurity_latestQuote(_ context.Co fc = &graphql.FieldContext{ Object: "ListedSecurity", Field: field, - IsMethod: true, - IsResolver: true, + IsMethod: false, + IsResolver: false, Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { switch field.Name { - case "value": - return ec.fieldContext_Currency_value(ctx, field) + case "amount": + return ec.fieldContext_Currency_amount(ctx, field) case "symbol": return ec.fieldContext_Currency_symbol(ctx, field) } @@ -1454,9 +1454,9 @@ func (ec *executionContext) _Mutation_triggerQuoteUpdate(ctx context.Context, fi } return graphql.Null } - res := resTmp.(bool) + res := resTmp.([]*persistence.ListedSecurity) fc.Result = res - return ec.marshalNBoolean2bool(ctx, field.Selections, res) + return ec.marshalNListedSecurity2ᚕᚖgithubᚗcomᚋoxistoᚋmoneyᚑgopherᚋpersistenceᚐListedSecurity(ctx, field.Selections, res) } func (ec *executionContext) fieldContext_Mutation_triggerQuoteUpdate(ctx context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { @@ -1466,7 +1466,19 @@ func (ec *executionContext) fieldContext_Mutation_triggerQuoteUpdate(ctx context IsMethod: true, IsResolver: true, Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { - return nil, errors.New("field of type Boolean does not have child fields") + switch field.Name { + case "ticker": + return ec.fieldContext_ListedSecurity_ticker(ctx, field) + case "currency": + return ec.fieldContext_ListedSecurity_currency(ctx, field) + case "security": + return ec.fieldContext_ListedSecurity_security(ctx, field) + case "latestQuote": + return ec.fieldContext_ListedSecurity_latestQuote(ctx, field) + case "latestQuoteTimestamp": + return ec.fieldContext_ListedSecurity_latestQuoteTimestamp(ctx, field) + } + return nil, fmt.Errorf("no field named %q was found under type ListedSecurity", field.Name) }, } defer func() { @@ -2021,8 +2033,8 @@ func (ec *executionContext) fieldContext_PortfolioPosition_purchaseValue(_ conte IsResolver: false, Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { switch field.Name { - case "value": - return ec.fieldContext_Currency_value(ctx, field) + case "amount": + return ec.fieldContext_Currency_amount(ctx, field) case "symbol": return ec.fieldContext_Currency_symbol(ctx, field) } @@ -2071,8 +2083,8 @@ func (ec *executionContext) fieldContext_PortfolioPosition_purchasePrice(_ conte IsResolver: false, Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { switch field.Name { - case "value": - return ec.fieldContext_Currency_value(ctx, field) + case "amount": + return ec.fieldContext_Currency_amount(ctx, field) case "symbol": return ec.fieldContext_Currency_symbol(ctx, field) } @@ -2121,8 +2133,8 @@ func (ec *executionContext) fieldContext_PortfolioPosition_marketValue(_ context IsResolver: false, Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { switch field.Name { - case "value": - return ec.fieldContext_Currency_value(ctx, field) + case "amount": + return ec.fieldContext_Currency_amount(ctx, field) case "symbol": return ec.fieldContext_Currency_symbol(ctx, field) } @@ -2171,8 +2183,8 @@ func (ec *executionContext) fieldContext_PortfolioPosition_marketPrice(_ context IsResolver: false, Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { switch field.Name { - case "value": - return ec.fieldContext_Currency_value(ctx, field) + case "amount": + return ec.fieldContext_Currency_amount(ctx, field) case "symbol": return ec.fieldContext_Currency_symbol(ctx, field) } @@ -2221,8 +2233,8 @@ func (ec *executionContext) fieldContext_PortfolioPosition_totalFees(_ context.C IsResolver: false, Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { switch field.Name { - case "value": - return ec.fieldContext_Currency_value(ctx, field) + case "amount": + return ec.fieldContext_Currency_amount(ctx, field) case "symbol": return ec.fieldContext_Currency_symbol(ctx, field) } @@ -2271,8 +2283,8 @@ func (ec *executionContext) fieldContext_PortfolioPosition_profitOrLoss(_ contex IsResolver: false, Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { switch field.Name { - case "value": - return ec.fieldContext_Currency_value(ctx, field) + case "amount": + return ec.fieldContext_Currency_amount(ctx, field) case "symbol": return ec.fieldContext_Currency_symbol(ctx, field) } @@ -2517,8 +2529,8 @@ func (ec *executionContext) fieldContext_PortfolioSnapshot_totalPurchaseValue(_ IsResolver: false, Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { switch field.Name { - case "value": - return ec.fieldContext_Currency_value(ctx, field) + case "amount": + return ec.fieldContext_Currency_amount(ctx, field) case "symbol": return ec.fieldContext_Currency_symbol(ctx, field) } @@ -2567,8 +2579,8 @@ func (ec *executionContext) fieldContext_PortfolioSnapshot_totalMarketValue(_ co IsResolver: false, Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { switch field.Name { - case "value": - return ec.fieldContext_Currency_value(ctx, field) + case "amount": + return ec.fieldContext_Currency_amount(ctx, field) case "symbol": return ec.fieldContext_Currency_symbol(ctx, field) } @@ -2617,8 +2629,8 @@ func (ec *executionContext) fieldContext_PortfolioSnapshot_totalProfitOrLoss(_ c IsResolver: false, Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { switch field.Name { - case "value": - return ec.fieldContext_Currency_value(ctx, field) + case "amount": + return ec.fieldContext_Currency_amount(ctx, field) case "symbol": return ec.fieldContext_Currency_symbol(ctx, field) } @@ -2708,8 +2720,8 @@ func (ec *executionContext) fieldContext_PortfolioSnapshot_totalPortfolioValue(_ IsResolver: false, Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { switch field.Name { - case "value": - return ec.fieldContext_Currency_value(ctx, field) + case "amount": + return ec.fieldContext_Currency_amount(ctx, field) case "symbol": return ec.fieldContext_Currency_symbol(ctx, field) } @@ -2758,8 +2770,8 @@ func (ec *executionContext) fieldContext_PortfolioSnapshot_cash(_ context.Contex IsResolver: false, Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { switch field.Name { - case "value": - return ec.fieldContext_Currency_value(ctx, field) + case "amount": + return ec.fieldContext_Currency_amount(ctx, field) case "symbol": return ec.fieldContext_Currency_symbol(ctx, field) } @@ -5227,8 +5239,8 @@ func (ec *executionContext) _Currency(ctx context.Context, sel ast.SelectionSet, switch field.Name { case "__typename": out.Values[i] = graphql.MarshalString("Currency") - case "value": - out.Values[i] = ec._Currency_value(ctx, field, obj) + case "amount": + out.Values[i] = ec._Currency_amount(ctx, field, obj) if out.Values[i] == graphql.Null { out.Invalids++ } @@ -5318,38 +5330,7 @@ func (ec *executionContext) _ListedSecurity(ctx context.Context, sel ast.Selecti out.Concurrently(i, func(ctx context.Context) graphql.Marshaler { return innerFunc(ctx, out) }) case "latestQuote": - field := field - - innerFunc := func(ctx context.Context, _ *graphql.FieldSet) (res graphql.Marshaler) { - defer func() { - if r := recover(); r != nil { - ec.Error(ctx, ec.Recover(ctx, r)) - } - }() - res = ec._ListedSecurity_latestQuote(ctx, field, obj) - return res - } - - if field.Deferrable != nil { - dfs, ok := deferred[field.Deferrable.Label] - di := 0 - if ok { - dfs.AddField(field) - di = len(dfs.Values) - 1 - } else { - dfs = graphql.NewFieldSet([]graphql.CollectedField{field}) - deferred[field.Deferrable.Label] = dfs - } - dfs.Concurrently(di, func(ctx context.Context) graphql.Marshaler { - return innerFunc(ctx, dfs) - }) - - // don't run the out.Concurrently() call below - out.Values[i] = graphql.Null - continue - } - - out.Concurrently(i, func(ctx context.Context) graphql.Marshaler { return innerFunc(ctx, out) }) + out.Values[i] = ec._ListedSecurity_latestQuote(ctx, field, obj) case "latestQuoteTimestamp": field := field @@ -6548,6 +6529,44 @@ func (ec *executionContext) marshalNInt2int32(ctx context.Context, sel ast.Selec return res } +func (ec *executionContext) marshalNListedSecurity2ᚕᚖgithubᚗcomᚋoxistoᚋmoneyᚑgopherᚋpersistenceᚐListedSecurity(ctx context.Context, sel ast.SelectionSet, v []*persistence.ListedSecurity) graphql.Marshaler { + ret := make(graphql.Array, len(v)) + var wg sync.WaitGroup + isLen1 := len(v) == 1 + if !isLen1 { + wg.Add(len(v)) + } + for i := range v { + i := i + fc := &graphql.FieldContext{ + Index: &i, + Result: &v[i], + } + ctx := graphql.WithFieldContext(ctx, fc) + f := func(i int) { + defer func() { + if r := recover(); r != nil { + ec.Error(ctx, ec.Recover(ctx, r)) + ret = nil + } + }() + if !isLen1 { + defer wg.Done() + } + ret[i] = ec.marshalOListedSecurity2ᚖgithubᚗcomᚋoxistoᚋmoneyᚑgopherᚋpersistenceᚐListedSecurity(ctx, sel, v[i]) + } + if isLen1 { + f(i) + } else { + go f(i) + } + + } + wg.Wait() + + return ret +} + func (ec *executionContext) marshalNListedSecurity2ᚖgithubᚗcomᚋoxistoᚋmoneyᚑgopherᚋpersistenceᚐListedSecurity(ctx context.Context, sel ast.SelectionSet, v *persistence.ListedSecurity) graphql.Marshaler { if v == nil { if !graphql.HasFieldError(ctx, graphql.GetFieldContext(ctx)) { @@ -7181,6 +7200,13 @@ func (ec *executionContext) marshalOListedSecurity2ᚕᚖgithubᚗcomᚋoxisto return ret } +func (ec *executionContext) marshalOListedSecurity2ᚖgithubᚗcomᚋoxistoᚋmoneyᚑgopherᚋpersistenceᚐListedSecurity(ctx context.Context, sel ast.SelectionSet, v *persistence.ListedSecurity) graphql.Marshaler { + if v == nil { + return graphql.Null + } + return ec._ListedSecurity(ctx, sel, v) +} + func (ec *executionContext) unmarshalOListedSecurityInput2ᚕᚖgithubᚗcomᚋoxistoᚋmoneyᚑgopherᚋmodelsᚐListedSecurityInputᚄ(ctx context.Context, v any) ([]*models.ListedSecurityInput, error) { if v == nil { return nil, nil diff --git a/graph/schema.graphqls b/graph/schema.graphqls index b211cd54..da403f04 100644 --- a/graph/schema.graphqls +++ b/graph/schema.graphqls @@ -8,7 +8,7 @@ directive @goEnum(value: String) on ENUM_VALUE scalar Date type Currency { - value: Int! + amount: Int! symbol: String! } @@ -139,7 +139,7 @@ type Mutation { Triggers a quote update for the given security IDs. If no security IDs are provided, all securities will be updated. """ - triggerQuoteUpdate(securityIDs: [String!]): Boolean! + triggerQuoteUpdate(securityIDs: [String!]): [ListedSecurity]! } type Query { diff --git a/graph/schema.resolvers.go b/graph/schema.resolvers.go index f685ff2d..a8cc871e 100644 --- a/graph/schema.resolvers.go +++ b/graph/schema.resolvers.go @@ -10,7 +10,6 @@ import ( "slices" "time" - "github.com/oxisto/money-gopher/currency" "github.com/oxisto/money-gopher/finance" "github.com/oxisto/money-gopher/models" "github.com/oxisto/money-gopher/persistence" @@ -21,11 +20,6 @@ func (r *listedSecurityResolver) Security(ctx context.Context, obj *persistence. panic(fmt.Errorf("not implemented: Security - security")) } -// LatestQuote is the resolver for the latestQuote field. -func (r *listedSecurityResolver) LatestQuote(ctx context.Context, obj *persistence.ListedSecurity) (*currency.Currency, error) { - panic(fmt.Errorf("not implemented: LatestQuote - latestQuote")) -} - // LatestQuoteTimestamp is the resolver for the latestQuoteTimestamp field. func (r *listedSecurityResolver) LatestQuoteTimestamp(ctx context.Context, obj *persistence.ListedSecurity) (*string, error) { panic(fmt.Errorf("not implemented: LatestQuoteTimestamp - latestQuoteTimestamp")) @@ -107,13 +101,13 @@ func (r *mutationResolver) UpdateSecurity(ctx context.Context, id string, input } // TriggerQuoteUpdate is the resolver for the triggerQuoteUpdate field. -func (r *mutationResolver) TriggerQuoteUpdate(ctx context.Context, securityIDs []string) (b bool, err error) { - err = r.QuoteUpdater.UpdateQuotes(ctx, securityIDs) +func (r *mutationResolver) TriggerQuoteUpdate(ctx context.Context, securityIDs []string) (updated []*persistence.ListedSecurity, err error) { + updated, err = r.QuoteUpdater.UpdateQuotes(ctx, securityIDs) if err != nil { - return false, err + return nil, err } - return true, nil + return } // BankAccount is the resolver for the bankAccount field. @@ -210,3 +204,17 @@ type portfolioResolver struct{ *Resolver } type portfolioEventResolver struct{ *Resolver } type queryResolver struct{ *Resolver } type securityResolver struct{ *Resolver } + +// !!! WARNING !!! +// The code below was going to be deleted when updating resolvers. It has been copied here so you have +// one last chance to move it out of harms way if you want. There are two reasons this happens: +// - When renaming or deleting a resolver the old code will be put in here. You can safely delete +// it when you're done. +// - You have helper methods in this file. Move them out to keep these resolver files clean. +/* + func (r *currencyResolver) Value(ctx context.Context, obj *currency.Currency) (int, error) { + panic(fmt.Errorf("not implemented: Value - value")) +} +func (r *Resolver) Currency() CurrencyResolver { return ¤cyResolver{r} } +type currencyResolver struct{ *Resolver } +*/ diff --git a/money-gopher.code-workspace b/money-gopher.code-workspace index 247cd820..fad13b65 100644 --- a/money-gopher.code-workspace +++ b/money-gopher.code-workspace @@ -15,6 +15,7 @@ "connectrpc", "DBTX", "emptypb", + "errgroup", "fatih", "fieldmaskpb", "headlessui", diff --git a/securities/quote/quote.go b/securities/quote/quote.go index 668c3a64..2a101893 100644 --- a/securities/quote/quote.go +++ b/securities/quote/quote.go @@ -27,6 +27,7 @@ import ( "github.com/oxisto/money-gopher/currency" "github.com/oxisto/money-gopher/persistence" + "golang.org/x/sync/errgroup" "github.com/lmittmann/tint" ) @@ -34,7 +35,7 @@ import ( // QuoteProvider is an interface that retrieves quotes for a [ListedSecurity]. They // can either be historical quotes or the latest quote. type QuoteUpdater interface { - UpdateQuotes(ctx context.Context, IDs []string) (err error) + UpdateQuotes(ctx context.Context, IDs []string) (updated []*persistence.ListedSecurity, err error) } // qu is the internal default implementation of the [QuoteUpdater] interface. @@ -50,7 +51,8 @@ func NewQuoteUpdater(db *persistence.DB) QuoteUpdater { } // UpdateQuotes triggers an update of the quotes for the given securities' IDs. -func (qu *qu) UpdateQuotes(ctx context.Context, secIDs []string) (err error) { +// It will return the updated listed securities. +func (qu *qu) UpdateQuotes(ctx context.Context, secIDs []string) (updated []*persistence.ListedSecurity, err error) { var ( secs []*persistence.Security listed []*persistence.ListedSecurity @@ -65,7 +67,7 @@ func (qu *qu) UpdateQuotes(ctx context.Context, secIDs []string) (err error) { secs, err = qu.db.ListSecuritiesByIDs(ctx, secIDs) } if err != nil { - return err + return nil, err } for _, sec := range secs { @@ -76,25 +78,37 @@ func (qu *qu) UpdateQuotes(ctx context.Context, secIDs []string) (err error) { qp, ok = providers[sec.QuoteProvider.String] if !ok { + slog.Error("Quote provider not found", "provider", sec.QuoteProvider.String) return } listed, err = sec.ListedAs(ctx, qu.db) if err != nil { - return err + return nil, err } // Trigger update from quote provider in separate go-routine - // TODO(oxisto): Use sync/errgroup instead + g, _ := errgroup.WithContext(ctx) for _, ls := range listed { - go func() { + ls := ls + g.Go(func() error { slog.Debug("Triggering quote update", "security", ls, "provider", sec.QuoteProvider) err = qu.updateQuote(qp, ls) if err != nil { - slog.Error("An error occurred during quote update", tint.Err(err), "ls", ls) + return err } - }() + + updated = append(updated, ls) + + return nil + }) + } + + err = g.Wait() + if err != nil { + slog.Error("An error occurred during quote update", tint.Err(err)) + return nil, err } } From 5d7b77aecc3b915cd9909d94f9eeff399a74422b Mon Sep 17 00:00:00 2001 From: Christian Banse Date: Sat, 4 Jan 2025 23:56:37 +0100 Subject: [PATCH 19/35] More test are ok --- cli/commands/securities.go | 5 ++-- cli/commands/securities_test.go | 23 +++++++++++++--- graph/schema.resolvers_test.go | 2 +- internal/testdata/securities.go | 10 ++++++- .../{ => testing}/persistencetest/queries.go | 0 internal/testing/quotetest/quotetest.go | 24 +++++++++++++++++ money-gopher.code-workspace | 1 + persistence/securities.sql_test.go | 2 +- securities/quote/quote_provider_test.go | 27 ------------------- securities/quote/quote_test.go | 25 ++++------------- 10 files changed, 62 insertions(+), 57 deletions(-) rename internal/{ => testing}/persistencetest/queries.go (100%) create mode 100644 internal/testing/quotetest/quotetest.go diff --git a/cli/commands/securities.go b/cli/commands/securities.go index 38c0e64a..ef9f9986 100644 --- a/cli/commands/securities.go +++ b/cli/commands/securities.go @@ -19,7 +19,6 @@ package commands import ( "context" - "fmt" mcli "github.com/oxisto/money-gopher/cli" "github.com/oxisto/money-gopher/currency" @@ -119,7 +118,7 @@ func UpdateQuote(ctx context.Context, cmd *cli.Command) (err error) { var query struct { TriggerQuoteUpdate []struct { LatestQuote *currency.Currency `json:"latestQuote"` - } `graphql:"triggerQuoteUpdate(securityIDs: $IDs)" json:"security"` + } `graphql:"triggerQuoteUpdate(securityIDs: $IDs)" json:"updated"` } var ids []graphql.String @@ -134,7 +133,7 @@ func UpdateQuote(ctx context.Context, cmd *cli.Command) (err error) { return err } - fmt.Fprintln(cmd.Writer, query.TriggerQuoteUpdate) + s.WriteJSON(cmd.Writer, query) return err } diff --git a/cli/commands/securities_test.go b/cli/commands/securities_test.go index 2111e462..17af0a32 100644 --- a/cli/commands/securities_test.go +++ b/cli/commands/securities_test.go @@ -21,12 +21,15 @@ import ( "context" "testing" + "github.com/oxisto/money-gopher/currency" "github.com/oxisto/money-gopher/internal" - "github.com/oxisto/money-gopher/internal/persistencetest" "github.com/oxisto/money-gopher/internal/testdata" "github.com/oxisto/money-gopher/internal/testing/clitest" + "github.com/oxisto/money-gopher/internal/testing/persistencetest" + "github.com/oxisto/money-gopher/internal/testing/quotetest" "github.com/oxisto/money-gopher/internal/testing/servertest" "github.com/oxisto/money-gopher/persistence" + "github.com/oxisto/money-gopher/securities/quote" "github.com/oxisto/assert" "github.com/urfave/cli/v3" @@ -36,6 +39,11 @@ func TestUpdateQuote(t *testing.T) { srv := servertest.NewServer(persistencetest.NewTestDB(t, func(db *persistence.DB) { _, err := db.CreateSecurity(context.Background(), testdata.TestCreateSecurityParams) assert.NoError(t, err) + + _, err = db.UpsertListedSecurity(context.Background(), testdata.TestUpsertListedSecurityParams) + assert.NoError(t, err) + + quote.RegisterQuoteProvider(quotetest.QuoteProviderStatic, quotetest.NewStaticQuoteProvider(currency.Value(100))) })) defer srv.Close() @@ -60,8 +68,16 @@ func TestUpdateQuote(t *testing.T) { }, wantRec: func(t *testing.T, rec *clitest.CommandRecorder) bool { return assert.Equals(t, `{ - "security": { - "id": "US0378331005"`, rec.String(), + "updated": [ + { + "latestQuote": { + "value": 100, + "symbol": "EUR" + } + } + ] +} +`, rec.String(), ) }, }, @@ -70,7 +86,6 @@ func TestUpdateQuote(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { rec := clitest.Record(tt.args.cmd) - if err := UpdateQuote(tt.args.ctx, tt.args.cmd); (err != nil) != tt.wantErr { t.Errorf("UpdateQuote() error = %v, wantErr %v", err, tt.wantErr) } diff --git a/graph/schema.resolvers_test.go b/graph/schema.resolvers_test.go index c781abc1..11863bd7 100644 --- a/graph/schema.resolvers_test.go +++ b/graph/schema.resolvers_test.go @@ -5,7 +5,7 @@ import ( "testing" "github.com/oxisto/assert" - "github.com/oxisto/money-gopher/internal/persistencetest" + "github.com/oxisto/money-gopher/internal/testing/persistencetest" "github.com/oxisto/money-gopher/models" "github.com/oxisto/money-gopher/persistence" ) diff --git a/internal/testdata/securities.go b/internal/testdata/securities.go index da7b9274..9a4c6f12 100644 --- a/internal/testdata/securities.go +++ b/internal/testdata/securities.go @@ -3,6 +3,7 @@ package testdata import ( "database/sql" + "github.com/oxisto/money-gopher/internal/testing/quotetest" "github.com/oxisto/money-gopher/persistence" ) @@ -10,7 +11,7 @@ import ( var TestSecurity = &persistence.Security{ ID: "DE1234567890", DisplayName: "My Security", - QuoteProvider: sql.NullString{String: "mock", Valid: true}, + QuoteProvider: sql.NullString{String: quotetest.QuoteProviderStatic, Valid: true}, } // TestListedSecurity is a listed security for [TestSecurity] that has a ticker @@ -27,3 +28,10 @@ var TestCreateSecurityParams = persistence.CreateSecurityParams{ DisplayName: TestSecurity.DisplayName, QuoteProvider: TestSecurity.QuoteProvider, } + +// TestUpsertListedSecurityParams is a test listed security upsert parameter. +var TestUpsertListedSecurityParams = persistence.UpsertListedSecurityParams{ + SecurityID: TestSecurity.ID, + Ticker: TestListedSecurity.Ticker, + Currency: TestListedSecurity.Currency, +} diff --git a/internal/persistencetest/queries.go b/internal/testing/persistencetest/queries.go similarity index 100% rename from internal/persistencetest/queries.go rename to internal/testing/persistencetest/queries.go diff --git a/internal/testing/quotetest/quotetest.go b/internal/testing/quotetest/quotetest.go new file mode 100644 index 00000000..56b8fb36 --- /dev/null +++ b/internal/testing/quotetest/quotetest.go @@ -0,0 +1,24 @@ +package quotetest + +import ( + "context" + "time" + + "github.com/oxisto/money-gopher/currency" + "github.com/oxisto/money-gopher/persistence" +) + +const QuoteProviderStatic = "static" + +type StaticQuoteProvider struct { + Quote *currency.Currency +} + +// NewStaticQuoteProvider creates a new static quote provider that always returns the same quote. +func NewStaticQuoteProvider(quote *currency.Currency) *StaticQuoteProvider { + return &StaticQuoteProvider{Quote: quote} +} + +func (p *StaticQuoteProvider) LatestQuote(ctx context.Context, ls *persistence.ListedSecurity) (quote *currency.Currency, t time.Time, err error) { + return p.Quote, time.Now(), nil +} diff --git a/money-gopher.code-workspace b/money-gopher.code-workspace index fad13b65..a11d0e2e 100644 --- a/money-gopher.code-workspace +++ b/money-gopher.code-workspace @@ -41,6 +41,7 @@ "protobuf", "protocmp", "protoreflect", + "quotetest", "rgossiaux", "secmap", "secres", diff --git a/persistence/securities.sql_test.go b/persistence/securities.sql_test.go index ff46c985..bb9f03d8 100644 --- a/persistence/securities.sql_test.go +++ b/persistence/securities.sql_test.go @@ -5,7 +5,7 @@ import ( "testing" "github.com/oxisto/assert" - "github.com/oxisto/money-gopher/internal/persistencetest" + "github.com/oxisto/money-gopher/internal/testing/persistencetest" "github.com/oxisto/money-gopher/persistence" ) diff --git a/securities/quote/quote_provider_test.go b/securities/quote/quote_provider_test.go index cea24b2d..3e2b3602 100644 --- a/securities/quote/quote_provider_test.go +++ b/securities/quote/quote_provider_test.go @@ -1,28 +1 @@ package quote - -import ( - "context" - "time" - - "github.com/oxisto/money-gopher/currency" - "github.com/oxisto/money-gopher/persistence" -) - -const QuoteProviderMock = "mock" - -type mockQP struct { -} - -func (m *mockQP) LatestQuote(ctx context.Context, ls *persistence.ListedSecurity) (quote *currency.Currency, t time.Time, err error) { - return currency.Value(100), time.Now(), nil -} - -type mockQuoteProvider struct{} - -func (mockQuoteProvider) LatestQuote(_ context.Context, _ *persistence.ListedSecurity) (quote *currency.Currency, t time.Time, err error) { - return currency.Value(100), time.Date(1, 0, 0, 0, 0, 0, 0, time.UTC), nil -} - -func init() { - RegisterQuoteProvider(QuoteProviderMock, &mockQP{}) -} diff --git a/securities/quote/quote_test.go b/securities/quote/quote_test.go index ea6e6d18..c6f6f169 100644 --- a/securities/quote/quote_test.go +++ b/securities/quote/quote_test.go @@ -1,19 +1,3 @@ -// Copyright 2023 Christian Banse -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. -// -// This file is part of The Money Gopher. - package quote import ( @@ -21,10 +5,11 @@ import ( "database/sql" "testing" + "github.com/oxisto/assert" + "github.com/oxisto/money-gopher/currency" "github.com/oxisto/money-gopher/internal" + "github.com/oxisto/money-gopher/internal/testing/quotetest" "github.com/oxisto/money-gopher/persistence" - - "github.com/oxisto/assert" ) func Test_qu_updateQuote(t *testing.T) { @@ -48,7 +33,7 @@ func Test_qu_updateQuote(t *testing.T) { db: internal.NewTestDB(t, func(db *persistence.DB) { _, err := db.CreateSecurity(context.Background(), persistence.CreateSecurityParams{ ID: "My Security", - QuoteProvider: sql.NullString{String: QuoteProviderMock, Valid: true}, + QuoteProvider: sql.NullString{String: quotetest.QuoteProviderStatic, Valid: true}, }) assert.NoError(t, err) _, err = db.UpsertListedSecurity(context.Background(), persistence.UpsertListedSecurityParams{ @@ -60,7 +45,7 @@ func Test_qu_updateQuote(t *testing.T) { }), }, args: args{ - qp: &mockQuoteProvider{}, + qp: quotetest.NewStaticQuoteProvider(currency.Value(100)), ls: &persistence.ListedSecurity{SecurityID: "My Security", Ticker: "SEC", Currency: "EUR"}, }, want: func(t *testing.T, ls *persistence.ListedSecurity) bool { From 62a2174151ddced66aa531e4050cf51868827fa0 Mon Sep 17 00:00:00 2001 From: Christian Banse Date: Sun, 5 Jan 2025 01:41:32 +0100 Subject: [PATCH 20/35] more tests ok --- cli/commands/securities.go | 26 +++++++++----- cli/commands/securities_test.go | 61 +++++++++++++++++---------------- 2 files changed, 48 insertions(+), 39 deletions(-) diff --git a/cli/commands/securities.go b/cli/commands/securities.go index ef9f9986..c36b8694 100644 --- a/cli/commands/securities.go +++ b/cli/commands/securities.go @@ -19,6 +19,7 @@ package commands import ( "context" + "fmt" mcli "github.com/oxisto/money-gopher/cli" "github.com/oxisto/money-gopher/currency" @@ -143,7 +144,9 @@ func UpdateAllQuotes(ctx context.Context, cmd *cli.Command) (err error) { s := mcli.FromContext(ctx) var query struct { - TriggerQuoteUpdate bool `graphql:"triggerQuoteUpdate(securityIDs: $IDs)" json:"security"` + TriggerQuoteUpdate []struct { + LatestQuote *currency.Currency `json:"latestQuote"` + } `graphql:"triggerQuoteUpdate(securityIDs: $IDs)" json:"updated"` } err = s.GraphQL.Mutate(context.Background(), &query, map[string]interface{}{ @@ -158,16 +161,21 @@ func UpdateAllQuotes(ctx context.Context, cmd *cli.Command) (err error) { // PredictSecurities predicts the securities for shell completion. func PredictSecurities(ctx context.Context, cmd *cli.Command) { - /*s := mcli.FromContext(ctx) - res, err := s.SecuritiesClient.ListSecurities( - context.Background(), - connect.NewRequest(&portfoliov1.ListSecuritiesRequest{}), - ) + s := mcli.FromContext(ctx) + + var query struct { + Securities []struct { + ID string `json:"id"` + DisplayName string `json:"displayName"` + } `json:"securities"` + } + + err := s.GraphQL.Query(context.Background(), &query, nil) if err != nil { return } - for _, p := range res.Msg.Securities { - fmt.Fprintf(cmd.Root().Writer, "%s:%s\n", p.Id, p.DisplayName) - }*/ + for _, p := range query.Securities { + fmt.Fprintf(cmd.Writer, "%s:%s\n", p.ID, p.DisplayName) + } } diff --git a/cli/commands/securities_test.go b/cli/commands/securities_test.go index 17af0a32..ca50ae66 100644 --- a/cli/commands/securities_test.go +++ b/cli/commands/securities_test.go @@ -22,7 +22,6 @@ import ( "testing" "github.com/oxisto/money-gopher/currency" - "github.com/oxisto/money-gopher/internal" "github.com/oxisto/money-gopher/internal/testdata" "github.com/oxisto/money-gopher/internal/testing/clitest" "github.com/oxisto/money-gopher/internal/testing/persistencetest" @@ -97,7 +96,15 @@ func TestUpdateQuote(t *testing.T) { } func TestUpdateAllQuotes(t *testing.T) { - srv := servertest.NewServer(internal.NewTestDB(t)) + srv := servertest.NewServer(persistencetest.NewTestDB(t, func(db *persistence.DB) { + _, err := db.CreateSecurity(context.Background(), testdata.TestCreateSecurityParams) + assert.NoError(t, err) + + _, err = db.UpsertListedSecurity(context.Background(), testdata.TestUpsertListedSecurityParams) + assert.NoError(t, err) + + quote.RegisterQuoteProvider(quotetest.QuoteProviderStatic, quotetest.NewStaticQuoteProvider(currency.Value(100))) + })) defer srv.Close() type args struct { @@ -127,18 +134,8 @@ func TestUpdateAllQuotes(t *testing.T) { } func TestListSecurities(t *testing.T) { - srv := servertest.NewServer(internal.NewTestDB(t, func(db *persistence.DB) { - _, err := db.CreateSecurity(context.Background(), persistence.CreateSecurityParams{ - ID: "1234", - DisplayName: "One Two Three Four", - }) - assert.NoError(t, err) - - _, err = db.UpsertListedSecurity(context.Background(), persistence.UpsertListedSecurityParams{ - SecurityID: "1234", - Ticker: "ONE", - Currency: "USD", - }) + srv := servertest.NewServer(persistencetest.NewTestDB(t, func(db *persistence.DB) { + _, err := db.CreateSecurity(context.Background(), testdata.TestCreateSecurityParams) assert.NoError(t, err) })) defer srv.Close() @@ -163,12 +160,8 @@ func TestListSecurities(t *testing.T) { return assert.Equals(t, `{ "securities": [ { - "id": "1234", - "displayName": "One Two Three Four" - }, - { - "id": "US0378331005", - "displayName": "Apple Inc." + "id": "DE1234567890", + "displayName": "My Security" } ] } @@ -191,7 +184,15 @@ func TestListSecurities(t *testing.T) { } func TestShowSecurity(t *testing.T) { - srv := servertest.NewServer(internal.NewTestDB(t)) + srv := servertest.NewServer(persistencetest.NewTestDB(t, func(db *persistence.DB) { + _, err := db.CreateSecurity(context.Background(), testdata.TestCreateSecurityParams) + assert.NoError(t, err) + + _, err = db.UpsertListedSecurity(context.Background(), testdata.TestUpsertListedSecurityParams) + assert.NoError(t, err) + + quote.RegisterQuoteProvider(quotetest.QuoteProviderStatic, quotetest.NewStaticQuoteProvider(currency.Value(100))) + })) defer srv.Close() type args struct { @@ -208,19 +209,16 @@ func TestShowSecurity(t *testing.T) { name: "happy path", args: args{ ctx: clitest.NewSessionContext(t, srv), - cmd: clitest.MockCommand(t, SecuritiesCmd.Command("show").Flags, "--security-id", "US0378331005"), + cmd: clitest.MockCommand(t, SecuritiesCmd.Command("show").Flags, "--security-id", "DE1234567890"), }, wantRec: func(t *testing.T, rec *clitest.CommandRecorder) bool { return assert.Equals(t, `{ "security": { - "id": "US0378331005", - "displayName": "Apple Inc.", + "id": "DE1234567890", + "displayName": "My Security", "listedAs": [ { - "ticker": "AAPL" - }, - { - "ticker": "APC.F" + "ticker": "TICK" } ] } @@ -245,7 +243,10 @@ func TestShowSecurity(t *testing.T) { } func TestPredictSecurities(t *testing.T) { - srv := servertest.NewServer(internal.NewTestDB(t)) + srv := servertest.NewServer(persistencetest.NewTestDB(t, func(db *persistence.DB) { + _, err := db.CreateSecurity(context.Background(), testdata.TestCreateSecurityParams) + assert.NoError(t, err) + })) defer srv.Close() type args struct { @@ -264,7 +265,7 @@ func TestPredictSecurities(t *testing.T) { cmd: &cli.Command{}, }, wantRec: func(t *testing.T, rec *clitest.CommandRecorder) bool { - return assert.Equals(t, "US0378331005:Apple Inc.\n", rec.String()) + return assert.Equals(t, "DE1234567890:My Security\n", rec.String()) }, }, } From fd849dadbd8d0342d783e510f59ecf4dc8b69738 Mon Sep 17 00:00:00 2001 From: Christian Banse Date: Sun, 5 Jan 2025 01:58:13 +0100 Subject: [PATCH 21/35] All tests work --- cli/commands/portfolio.go | 22 ++-- cli/commands/portfolio_test.go | 17 ++- finance/snapshot.go | 2 +- graph/generated.go | 155 +++++++++++++++++++++++++ graph/schema.graphqls | 8 ++ graph/schema.resolvers.go | 19 +-- models/models_gen.go | 5 + persistence/portfolios.sql.go | 20 ++++ persistence/sql/queries/portfolios.sql | 8 +- 9 files changed, 230 insertions(+), 26 deletions(-) diff --git a/cli/commands/portfolio.go b/cli/commands/portfolio.go index 07b5635e..aa496199 100644 --- a/cli/commands/portfolio.go +++ b/cli/commands/portfolio.go @@ -18,6 +18,7 @@ package commands import ( "context" + "fmt" mcli "github.com/oxisto/money-gopher/cli" @@ -271,16 +272,21 @@ func ImportTransactions(ctx context.Context, cmd *cli.Command) error { // PredictPortfolios predicts the portfolios for shell completion. func PredictPortfolios(ctx context.Context, cmd *cli.Command) { - /*s := mcli.FromContext(ctx) - res, err := s.PortfolioClient.ListPortfolios( - context.Background(), - connect.NewRequest(&portfoliov1.ListPortfoliosRequest{}), - ) + s := mcli.FromContext(ctx) + + var query struct { + Portfolios []struct { + ID string `json:"id"` + DisplayName string `json:"displayName"` + } `json:"portfolios"` + } + + err := s.GraphQL.Query(context.Background(), &query, nil) if err != nil { return } - for _, p := range res.Msg.Portfolios { - fmt.Fprintf(cmd.Root().Writer, "%s:%s\n", p.Id, p.DisplayName) - }*/ + for _, p := range query.Portfolios { + fmt.Fprintf(cmd.Writer, "%s:%s\n", p.ID, p.DisplayName) + } } diff --git a/cli/commands/portfolio_test.go b/cli/commands/portfolio_test.go index b0015ad5..acedb0a2 100644 --- a/cli/commands/portfolio_test.go +++ b/cli/commands/portfolio_test.go @@ -219,7 +219,20 @@ func TestImportTransactions(t *testing.T) { } func TestPredictPortfolios(t *testing.T) { - srv := servertest.NewServer(internal.NewTestDB(t)) + srv := servertest.NewServer(internal.NewTestDB(t, func(db *persistence.DB) { + _, err := db.Queries.CreateBankAccount(context.Background(), persistence.CreateBankAccountParams{ + ID: "mybank", + DisplayName: "My Bank", + }) + assert.NoError(t, err) + + _, err = db.Queries.CreatePortfolio(context.Background(), persistence.CreatePortfolioParams{ + ID: "mybank/myportfolio", + DisplayName: "My Portfolio", + BankAccountID: "mybank", + }) + assert.NoError(t, err) + })) defer srv.Close() type args struct { @@ -238,7 +251,7 @@ func TestPredictPortfolios(t *testing.T) { cmd: &cli.Command{}, }, wantRec: func(t *testing.T, r *clitest.CommandRecorder) bool { - return assert.Equals(t, "mybank-myportfolio:My Portfolio\n", r.String()) + return assert.Equals(t, "mybank/myportfolio:My Portfolio\n", r.String()) }, }, } diff --git a/finance/snapshot.go b/finance/snapshot.go index f779e58f..02f9cfb7 100644 --- a/finance/snapshot.go +++ b/finance/snapshot.go @@ -47,7 +47,7 @@ func BuildSnapshot( // Retrieve events events, err = provider.ListPortfolioEventsByPortfolioID(ctx, portfolioID) if err != nil { - return nil, connect.NewError(connect.CodeInternal, err) + return nil, err } // Set up the snapshot diff --git a/graph/generated.go b/graph/generated.go index 00f56eea..efe72537 100644 --- a/graph/generated.go +++ b/graph/generated.go @@ -73,6 +73,7 @@ type ComplexityRoot struct { } Mutation struct { + CreatePortfolio func(childComplexity int, input models.PortfolioInput) int CreateSecurity func(childComplexity int, input models.SecurityInput) int TriggerQuoteUpdate func(childComplexity int, securityIDs []string) int UpdateSecurity func(childComplexity int, id string, input models.SecurityInput) int @@ -139,6 +140,7 @@ type ListedSecurityResolver interface { type MutationResolver interface { CreateSecurity(ctx context.Context, input models.SecurityInput) (*persistence.Security, error) UpdateSecurity(ctx context.Context, id string, input models.SecurityInput) (*persistence.Security, error) + CreatePortfolio(ctx context.Context, input models.PortfolioInput) (*persistence.Portfolio, error) TriggerQuoteUpdate(ctx context.Context, securityIDs []string) ([]*persistence.ListedSecurity, error) } type PortfolioResolver interface { @@ -244,6 +246,18 @@ func (e *executableSchema) Complexity(typeName, field string, childComplexity in return e.complexity.ListedSecurity.Ticker(childComplexity), true + case "Mutation.createPortfolio": + if e.complexity.Mutation.CreatePortfolio == nil { + break + } + + args, err := ec.field_Mutation_createPortfolio_args(context.TODO(), rawArgs) + if err != nil { + return 0, false + } + + return e.complexity.Mutation.CreatePortfolio(childComplexity, args["input"].(models.PortfolioInput)), true + case "Mutation.createSecurity": if e.complexity.Mutation.CreateSecurity == nil { break @@ -542,6 +556,7 @@ func (e *executableSchema) Exec(ctx context.Context) graphql.ResponseHandler { ec := executionContext{opCtx, e, 0, 0, make(chan graphql.DeferredResult)} inputUnmarshalMap := graphql.BuildUnmarshalerMap( ec.unmarshalInputListedSecurityInput, + ec.unmarshalInputPortfolioInput, ec.unmarshalInputSecurityInput, ) first := true @@ -659,6 +674,29 @@ var parsedSchema = gqlparser.MustLoadSchema(sources...) // region ***************************** args.gotpl ***************************** +func (ec *executionContext) field_Mutation_createPortfolio_args(ctx context.Context, rawArgs map[string]any) (map[string]any, error) { + var err error + args := map[string]any{} + arg0, err := ec.field_Mutation_createPortfolio_argsInput(ctx, rawArgs) + if err != nil { + return nil, err + } + args["input"] = arg0 + return args, nil +} +func (ec *executionContext) field_Mutation_createPortfolio_argsInput( + ctx context.Context, + rawArgs map[string]any, +) (models.PortfolioInput, error) { + ctx = graphql.WithPathContext(ctx, graphql.NewPathWithField("input")) + if tmp, ok := rawArgs["input"]; ok { + return ec.unmarshalNPortfolioInput2githubᚗcomᚋoxistoᚋmoneyᚑgopherᚋmodelsᚐPortfolioInput(ctx, tmp) + } + + var zeroVal models.PortfolioInput + return zeroVal, nil +} + func (ec *executionContext) field_Mutation_createSecurity_args(ctx context.Context, rawArgs map[string]any) (map[string]any, error) { var err error args := map[string]any{} @@ -1428,6 +1466,73 @@ func (ec *executionContext) fieldContext_Mutation_updateSecurity(ctx context.Con return fc, nil } +func (ec *executionContext) _Mutation_createPortfolio(ctx context.Context, field graphql.CollectedField) (ret graphql.Marshaler) { + fc, err := ec.fieldContext_Mutation_createPortfolio(ctx, field) + if err != nil { + return graphql.Null + } + ctx = graphql.WithFieldContext(ctx, fc) + defer func() { + if r := recover(); r != nil { + ec.Error(ctx, ec.Recover(ctx, r)) + ret = graphql.Null + } + }() + resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (any, error) { + ctx = rctx // use context from middleware stack in children + return ec.resolvers.Mutation().CreatePortfolio(rctx, fc.Args["input"].(models.PortfolioInput)) + }) + if err != nil { + ec.Error(ctx, err) + return graphql.Null + } + if resTmp == nil { + if !graphql.HasFieldError(ctx, fc) { + ec.Errorf(ctx, "must not be null") + } + return graphql.Null + } + res := resTmp.(*persistence.Portfolio) + fc.Result = res + return ec.marshalNPortfolio2ᚖgithubᚗcomᚋoxistoᚋmoneyᚑgopherᚋpersistenceᚐPortfolio(ctx, field.Selections, res) +} + +func (ec *executionContext) fieldContext_Mutation_createPortfolio(ctx context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { + fc = &graphql.FieldContext{ + Object: "Mutation", + Field: field, + IsMethod: true, + IsResolver: true, + Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + switch field.Name { + case "id": + return ec.fieldContext_Portfolio_id(ctx, field) + case "displayName": + return ec.fieldContext_Portfolio_displayName(ctx, field) + case "bankAccount": + return ec.fieldContext_Portfolio_bankAccount(ctx, field) + case "snapshot": + return ec.fieldContext_Portfolio_snapshot(ctx, field) + case "events": + return ec.fieldContext_Portfolio_events(ctx, field) + } + return nil, fmt.Errorf("no field named %q was found under type Portfolio", field.Name) + }, + } + defer func() { + if r := recover(); r != nil { + err = ec.Recover(ctx, r) + ec.Error(ctx, err) + } + }() + ctx = graphql.WithFieldContext(ctx, fc) + if fc.Args, err = ec.field_Mutation_createPortfolio_args(ctx, field.ArgumentMap(ec.Variables)); err != nil { + ec.Error(ctx, err) + return fc, err + } + return fc, nil +} + func (ec *executionContext) _Mutation_triggerQuoteUpdate(ctx context.Context, field graphql.CollectedField) (ret graphql.Marshaler) { fc, err := ec.fieldContext_Mutation_triggerQuoteUpdate(ctx, field) if err != nil { @@ -5135,6 +5240,40 @@ func (ec *executionContext) unmarshalInputListedSecurityInput(ctx context.Contex return it, nil } +func (ec *executionContext) unmarshalInputPortfolioInput(ctx context.Context, obj any) (models.PortfolioInput, error) { + var it models.PortfolioInput + asMap := map[string]any{} + for k, v := range obj.(map[string]any) { + asMap[k] = v + } + + fieldsInOrder := [...]string{"id", "displayName"} + for _, k := range fieldsInOrder { + v, ok := asMap[k] + if !ok { + continue + } + switch k { + case "id": + ctx := graphql.WithPathContext(ctx, graphql.NewPathWithField("id")) + data, err := ec.unmarshalNString2string(ctx, v) + if err != nil { + return it, err + } + it.ID = data + case "displayName": + ctx := graphql.WithPathContext(ctx, graphql.NewPathWithField("displayName")) + data, err := ec.unmarshalNString2string(ctx, v) + if err != nil { + return it, err + } + it.DisplayName = data + } + } + + return it, nil +} + func (ec *executionContext) unmarshalInputSecurityInput(ctx context.Context, obj any) (models.SecurityInput, error) { var it models.SecurityInput asMap := map[string]any{} @@ -5420,6 +5559,13 @@ func (ec *executionContext) _Mutation(ctx context.Context, sel ast.SelectionSet) if out.Values[i] == graphql.Null { out.Invalids++ } + case "createPortfolio": + out.Values[i] = ec.OperationContext.RootResolverMiddleware(innerCtx, func(ctx context.Context) (res graphql.Marshaler) { + return ec._Mutation_createPortfolio(ctx, field) + }) + if out.Values[i] == graphql.Null { + out.Invalids++ + } case "triggerQuoteUpdate": out.Values[i] = ec.OperationContext.RootResolverMiddleware(innerCtx, func(ctx context.Context) (res graphql.Marshaler) { return ec._Mutation_triggerQuoteUpdate(ctx, field) @@ -6582,6 +6728,10 @@ func (ec *executionContext) unmarshalNListedSecurityInput2ᚖgithubᚗcomᚋoxis return &res, graphql.ErrorOnPath(ctx, err) } +func (ec *executionContext) marshalNPortfolio2githubᚗcomᚋoxistoᚋmoneyᚑgopherᚋpersistenceᚐPortfolio(ctx context.Context, sel ast.SelectionSet, v persistence.Portfolio) graphql.Marshaler { + return ec._Portfolio(ctx, sel, &v) +} + func (ec *executionContext) marshalNPortfolio2ᚕᚖgithubᚗcomᚋoxistoᚋmoneyᚑgopherᚋpersistenceᚐPortfolioᚄ(ctx context.Context, sel ast.SelectionSet, v []*persistence.Portfolio) graphql.Marshaler { ret := make(graphql.Array, len(v)) var wg sync.WaitGroup @@ -6719,6 +6869,11 @@ var ( } ) +func (ec *executionContext) unmarshalNPortfolioInput2githubᚗcomᚋoxistoᚋmoneyᚑgopherᚋmodelsᚐPortfolioInput(ctx context.Context, v any) (models.PortfolioInput, error) { + res, err := ec.unmarshalInputPortfolioInput(ctx, v) + return res, graphql.ErrorOnPath(ctx, err) +} + func (ec *executionContext) marshalNPortfolioPosition2ᚕᚖgithubᚗcomᚋoxistoᚋmoneyᚑgopherᚋmodelsᚐPortfolioPositionᚄ(ctx context.Context, sel ast.SelectionSet, v []*models.PortfolioPosition) graphql.Marshaler { ret := make(graphql.Array, len(v)) var wg sync.WaitGroup diff --git a/graph/schema.graphqls b/graph/schema.graphqls index da403f04..ed0fcbfa 100644 --- a/graph/schema.graphqls +++ b/graph/schema.graphqls @@ -127,6 +127,11 @@ input SecurityInput { listedAs: [ListedSecurityInput!] } +input PortfolioInput { + id: String! + displayName: String! +} + input ListedSecurityInput { ticker: String! currency: String! @@ -135,6 +140,9 @@ input ListedSecurityInput { type Mutation { createSecurity(input: SecurityInput!): Security! updateSecurity(id: ID!, input: SecurityInput!): Security! + + createPortfolio(input: PortfolioInput!): Portfolio! + """ Triggers a quote update for the given security IDs. If no security IDs are provided, all securities will be updated. diff --git a/graph/schema.resolvers.go b/graph/schema.resolvers.go index a8cc871e..ceb19e07 100644 --- a/graph/schema.resolvers.go +++ b/graph/schema.resolvers.go @@ -100,6 +100,11 @@ func (r *mutationResolver) UpdateSecurity(ctx context.Context, id string, input }) } +// CreatePortfolio is the resolver for the createPortfolio field. +func (r *mutationResolver) CreatePortfolio(ctx context.Context, input models.PortfolioInput) (*persistence.Portfolio, error) { + panic(fmt.Errorf("not implemented: CreatePortfolio - createPortfolio")) +} + // TriggerQuoteUpdate is the resolver for the triggerQuoteUpdate field. func (r *mutationResolver) TriggerQuoteUpdate(ctx context.Context, securityIDs []string) (updated []*persistence.ListedSecurity, err error) { updated, err = r.QuoteUpdater.UpdateQuotes(ctx, securityIDs) @@ -204,17 +209,3 @@ type portfolioResolver struct{ *Resolver } type portfolioEventResolver struct{ *Resolver } type queryResolver struct{ *Resolver } type securityResolver struct{ *Resolver } - -// !!! WARNING !!! -// The code below was going to be deleted when updating resolvers. It has been copied here so you have -// one last chance to move it out of harms way if you want. There are two reasons this happens: -// - When renaming or deleting a resolver the old code will be put in here. You can safely delete -// it when you're done. -// - You have helper methods in this file. Move them out to keep these resolver files clean. -/* - func (r *currencyResolver) Value(ctx context.Context, obj *currency.Currency) (int, error) { - panic(fmt.Errorf("not implemented: Value - value")) -} -func (r *Resolver) Currency() CurrencyResolver { return ¤cyResolver{r} } -type currencyResolver struct{ *Resolver } -*/ diff --git a/models/models_gen.go b/models/models_gen.go index 700e9e42..5f642105 100644 --- a/models/models_gen.go +++ b/models/models_gen.go @@ -15,6 +15,11 @@ type ListedSecurityInput struct { type Mutation struct { } +type PortfolioInput struct { + ID string `json:"id"` + DisplayName string `json:"displayName"` +} + type PortfolioPosition struct { Security *persistence.Security `json:"security"` Amount float64 `json:"amount"` diff --git a/persistence/portfolios.sql.go b/persistence/portfolios.sql.go index d80dfc54..32220a45 100644 --- a/persistence/portfolios.sql.go +++ b/persistence/portfolios.sql.go @@ -28,6 +28,26 @@ func (q *Queries) CreateBankAccount(ctx context.Context, arg CreateBankAccountPa return &i, err } +const createPortfolio = `-- name: CreatePortfolio :one +INSERT INTO + portfolios (id, display_name, bank_account_id) +VALUES + (?, ?, ?) RETURNING id, display_name, bank_account_id +` + +type CreatePortfolioParams struct { + ID string + DisplayName string + BankAccountID string +} + +func (q *Queries) CreatePortfolio(ctx context.Context, arg CreatePortfolioParams) (*Portfolio, error) { + row := q.db.QueryRowContext(ctx, createPortfolio, arg.ID, arg.DisplayName, arg.BankAccountID) + var i Portfolio + err := row.Scan(&i.ID, &i.DisplayName, &i.BankAccountID) + return &i, err +} + const getBankAccount = `-- name: GetBankAccount :one SELECT id, display_name diff --git a/persistence/sql/queries/portfolios.sql b/persistence/sql/queries/portfolios.sql index 4fd0dd99..ff22b67e 100644 --- a/persistence/sql/queries/portfolios.sql +++ b/persistence/sql/queries/portfolios.sql @@ -34,4 +34,10 @@ WHERE INSERT INTO bank_accounts (id, display_name) VALUES - (?, ?) RETURNING *; \ No newline at end of file + (?, ?) RETURNING *; + +-- name: CreatePortfolio :one +INSERT INTO + portfolios (id, display_name, bank_account_id) +VALUES + (?, ?, ?) RETURNING *; \ No newline at end of file From 6e426d41d0893bf0b6822df91cd692ec8fc7522c Mon Sep 17 00:00:00 2001 From: Christian Banse Date: Sun, 5 Jan 2025 18:56:51 +0100 Subject: [PATCH 22/35] Create accounts --- persistence/models.go | 11 +++++++++++ ...ate_portfolio.sql => 0002_create_accounts.sql} | 15 ++++++++++++++- portfolio/accounts/type.go | 15 +++++++++++++++ 3 files changed, 40 insertions(+), 1 deletion(-) rename persistence/sql/migrations/{0002_create_portfolio.sql => 0002_create_accounts.sql} (64%) create mode 100644 portfolio/accounts/type.go diff --git a/persistence/models.go b/persistence/models.go index 48908db7..e2470844 100644 --- a/persistence/models.go +++ b/persistence/models.go @@ -12,6 +12,16 @@ import ( "github.com/oxisto/money-gopher/portfolio/events" ) +// Accounts represents an account, such as a brokerage account or a bank +type Account struct { + // ID is the primary identifier for a brokerage account. + ID string + // DisplayName is the human-readable name of the brokerage account. + DisplayName string + // Type is the type of the account. + Type int64 +} + type BankAccount struct { // ID is the primary identifier for a bank account. ID string @@ -33,6 +43,7 @@ type ListedSecurity struct { LatestQuoteTimestamp sql.NullTime } +// Portfolios represent a collection of securities and other positions type Portfolio struct { // ID is the primary identifier for a portfolio. ID string diff --git a/persistence/sql/migrations/0002_create_portfolio.sql b/persistence/sql/migrations/0002_create_accounts.sql similarity index 64% rename from persistence/sql/migrations/0002_create_portfolio.sql rename to persistence/sql/migrations/0002_create_accounts.sql index 6410ebba..d310bdbf 100644 --- a/persistence/sql/migrations/0002_create_portfolio.sql +++ b/persistence/sql/migrations/0002_create_accounts.sql @@ -1,6 +1,8 @@ -- +goose Up CREATE TABLE IF NOT EXISTS portfolios ( + -- Portfolios represent a collection of securities and other positions + -- held by a user. id TEXT PRIMARY KEY, -- ID is the primary identifier for a portfolio. display_name TEXT NOT NULL, -- DisplayName is the human-readable name of the portfolio. bank_account_id TEXT NOT NULL, -- BankAccountID is the ID of the bank account that holds the portfolio. @@ -26,9 +28,20 @@ CREATE TABLE display_name TEXT NOT NULL -- DisplayName is the human-readable name of the bank account. ); +CREATE TABLE + IF NOT EXISTS accounts ( + -- Accounts represents an account, such as a brokerage account or a bank + -- account which comprise a portfolio. + id TEXT PRIMARY KEY, -- ID is the primary identifier for a brokerage account. + display_name TEXT NOT NULL, -- DisplayName is the human-readable name of the brokerage account. + type INTEGER NOT NULL -- Type is the type of the account. + ); + -- +goose Down DROP TABLE portfolios; DROP TABLE portfolio_events; -DROP TABLE bank_accounts; \ No newline at end of file +DROP TABLE bank_accounts; + +DROP TABLE accounts; \ No newline at end of file diff --git a/portfolio/accounts/type.go b/portfolio/accounts/type.go new file mode 100644 index 00000000..3be3f091 --- /dev/null +++ b/portfolio/accounts/type.go @@ -0,0 +1,15 @@ +package accounts + +// AccountType is the type of an account. +type AccountType int + +const ( + // AccountTypeBrokerage represents a brokerage account. + AccountTypeBrokerage AccountType = iota + 1 + // AccountTypeBank represents a bank account. + AccountTypeBank + // AccountTypeLoan represents a loan account. + AccountTypeLoan + // AccountTypeUnknown represents an unknown account type. + AccountTypeUnknown +) From 916c1f8d4f46384c7623d0e89df7d71340ac9c0f Mon Sep 17 00:00:00 2001 From: Christian Banse Date: Sun, 5 Jan 2025 20:16:13 +0100 Subject: [PATCH 23/35] Removed connect --- .github/workflows/build.yml | 4 - buf.gen.yaml | 10 - buf.lock | 8 - buf.openapi.gen.yaml | 6 - buf.yaml | 10 - finance/snapshot.go | 12 +- generate.go | 4 +- go.mod | 10 +- go.sum | 10 - graph/generated.go | 510 ++++++++++++++++++ graph/schema.graphqls | 9 + graph/schema.resolvers.go | 19 + mgo.proto | 338 ------------ money-gopher.code-workspace | 17 +- .../{portfolios.sql.go => accounts.sql.go} | 93 +++- persistence/models.go | 10 +- .../sql/migrations/0002_create_accounts.sql | 16 +- .../queries/{portfolios.sql => accounts.sql} | 22 + securities/quote/quote_provider_ing_test.go | 7 +- securities/quote/quote_provider_test.go | 1 - securities/quote/quote_provider_yf_test.go | 7 +- server/interceptors.go | 98 ---- sqlc.yaml | 2 + 23 files changed, 705 insertions(+), 518 deletions(-) delete mode 100644 buf.gen.yaml delete mode 100644 buf.lock delete mode 100644 buf.openapi.gen.yaml delete mode 100644 buf.yaml delete mode 100644 mgo.proto rename persistence/{portfolios.sql.go => accounts.sql.go} (65%) rename persistence/sql/queries/{portfolios.sql => accounts.sql} (67%) delete mode 100644 securities/quote/quote_provider_test.go delete mode 100644 server/interceptors.go diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index f9e9970b..8ee746c0 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -21,10 +21,6 @@ jobs: uses: actions/setup-node@v4 with: node-version: 23 - - name: Set up buf - uses: bufbuild/buf-setup-action@v1.48.0 - with: - github_token: ${{ secrets.GITHUB_TOKEN }} - name: Set up tools run: | go install github.com/mfridman/tparse diff --git a/buf.gen.yaml b/buf.gen.yaml deleted file mode 100644 index bccd7b2e..00000000 --- a/buf.gen.yaml +++ /dev/null @@ -1,10 +0,0 @@ -version: v1 -plugins: - - plugin: buf.build/connectrpc/go - out: gen - opt: - - paths=source_relative - - plugin: buf.build/protocolbuffers/go - out: gen - opt: - - paths=source_relative diff --git a/buf.lock b/buf.lock deleted file mode 100644 index 2b3f954f..00000000 --- a/buf.lock +++ /dev/null @@ -1,8 +0,0 @@ -# Generated by buf. DO NOT EDIT. -version: v1 -deps: - - remote: buf.build - owner: googleapis - repository: googleapis - commit: 8bc2c51e08c447cd8886cdea48a73e14 - digest: shake256:a969155953a5cedc5b2df5b42c368f2bc66ff8ce1804bc96e0f14ff2ee8a893687963058909df844d1643cdbc98ff099d2daa6bc9f9f5b8886c49afdc60e19af diff --git a/buf.openapi.gen.yaml b/buf.openapi.gen.yaml deleted file mode 100644 index 462cfbb5..00000000 --- a/buf.openapi.gen.yaml +++ /dev/null @@ -1,6 +0,0 @@ -version: v1 -plugins: - - plugin: buf.build/community/google-gnostic-openapi - out: . - opt: - - enum_type=string diff --git a/buf.yaml b/buf.yaml deleted file mode 100644 index a042d9b0..00000000 --- a/buf.yaml +++ /dev/null @@ -1,10 +0,0 @@ -version: v1 -name: buf.build/clouditor/api -deps: - - buf.build/googleapis/googleapis -breaking: - use: - - FILE -lint: - use: - - DEFAULT diff --git a/finance/snapshot.go b/finance/snapshot.go index 02f9cfb7..2ddbd645 100644 --- a/finance/snapshot.go +++ b/finance/snapshot.go @@ -7,7 +7,6 @@ import ( "slices" "time" - "connectrpc.com/connect" moneygopher "github.com/oxisto/money-gopher" "github.com/oxisto/money-gopher/currency" "github.com/oxisto/money-gopher/models" @@ -73,9 +72,7 @@ func BuildSnapshot( secs, err = provider.ListSecuritiesByIDs(context.Background(), ids) if err != nil { - return nil, connect.NewError(connect.CodeInternal, - fmt.Errorf("internal error while calling ListSecurities on securities service: %w", err), - ) + return nil, fmt.Errorf("internal error while calling ListSecurities on securities service: %w", err) } // Make a map out of the securities list so we can access it easier @@ -108,8 +105,8 @@ func BuildSnapshot( Amount: c.Amount, PurchaseValue: c.NetValue(), PurchasePrice: c.NetPrice(), - MarketValue: currency.Times(marketPrice(secmap, name, c.NetPrice(), provider), c.Amount), - MarketPrice: marketPrice(secmap, name, c.NetPrice(), provider), + MarketValue: currency.Times(marketPrice(name, c.NetPrice(), provider), c.Amount), + MarketPrice: marketPrice(name, c.NetPrice(), provider), } // Calculate loss and gains @@ -152,6 +149,8 @@ func eventsBefore(events []*persistence.PortfolioEvent, t time.Time) (out []*per // groupByPortfolio groups the events by their security ID. func groupByPortfolio(events []*persistence.PortfolioEvent) (m map[string][]*persistence.PortfolioEvent) { + m = make(map[string][]*persistence.PortfolioEvent) + for _, event := range events { name := event.SecurityID if name != "" { @@ -166,7 +165,6 @@ func groupByPortfolio(events []*persistence.PortfolioEvent) (m map[string][]*per } func marketPrice( - secmap map[string]*persistence.Security, name string, netPrice *currency.Currency, provider SnapshotDataProvider, diff --git a/generate.go b/generate.go index c45400e9..cb9d311c 100644 --- a/generate.go +++ b/generate.go @@ -15,8 +15,6 @@ // This file is part of The Money Gopher. //go:generate sqlc generate -//go:generate buf generate -//go:generate buf format -w -//go:generate buf generate --template buf.openapi.gen.yaml --path mgo.proto -o . +//go:generate gqlgen package moneygopher diff --git a/go.mod b/go.mod index 308e2165..5fc6ce07 100644 --- a/go.mod +++ b/go.mod @@ -3,11 +3,8 @@ module github.com/oxisto/money-gopher go 1.23.4 require ( - connectrpc.com/connect v1.16.2 github.com/99designs/gqlgen v0.17.61 - github.com/MicahParks/keyfunc/v3 v3.3.5 github.com/fatih/color v1.18.0 - github.com/golang-jwt/jwt/v5 v5.2.1 github.com/lmittmann/tint v1.0.6 github.com/mattn/go-colorable v0.1.13 github.com/mattn/go-isatty v0.0.20 @@ -20,15 +17,12 @@ require ( github.com/vektah/gqlparser/v2 v2.5.20 golang.org/x/net v0.33.0 golang.org/x/sync v0.10.0 - google.golang.org/protobuf v1.36.1 ) -require golang.org/x/text v0.21.0 // indirect - require ( - github.com/MicahParks/jwkset v0.5.19 // indirect github.com/agnivade/levenshtein v1.2.0 // indirect github.com/go-viper/mapstructure/v2 v2.2.1 // indirect + github.com/golang-jwt/jwt/v5 v5.2.1 // indirect github.com/google/go-cmp v0.6.0 // indirect github.com/google/uuid v1.6.0 // indirect github.com/mfridman/interpolate v0.0.2 // indirect @@ -38,7 +32,7 @@ require ( golang.org/x/crypto v0.31.0 // indirect golang.org/x/oauth2 v0.22.0 // indirect golang.org/x/sys v0.28.0 // indirect - golang.org/x/time v0.5.0 // indirect + golang.org/x/text v0.21.0 // indirect ) replace github.com/99designs/gqlgen v0.17.61 => github.com/oxisto/gqlgen v0.17.62-0.20241227140449-4bf1c5c27bad diff --git a/go.sum b/go.sum index 16dee170..9625f095 100644 --- a/go.sum +++ b/go.sum @@ -1,9 +1,3 @@ -connectrpc.com/connect v1.16.2 h1:ybd6y+ls7GOlb7Bh5C8+ghA6SvCBajHwxssO2CGFjqE= -connectrpc.com/connect v1.16.2/go.mod h1:n2kgwskMHXC+lVqb18wngEpF95ldBHXjZYJussz5FRc= -github.com/MicahParks/jwkset v0.5.19 h1:XZCsgJv05DBCvxEHYEHlSafqiuVn5ESG0VRB331Fxhw= -github.com/MicahParks/jwkset v0.5.19/go.mod h1:q8ptTGn/Z9c4MwbcfeCDssADeVQb3Pk7PnVxrvi+2QY= -github.com/MicahParks/keyfunc/v3 v3.3.5 h1:7ceAJLUAldnoueHDNzF8Bx06oVcQ5CfJnYwNt1U3YYo= -github.com/MicahParks/keyfunc/v3 v3.3.5/go.mod h1:SdCCyMJn/bYqWDvARspC6nCT8Sk74MjuAY22C7dCST8= github.com/PuerkitoBio/goquery v1.9.3 h1:mpJr/ikUA9/GNJB/DBZcGeFDXUtosHRyRrwh7KGdTG0= github.com/PuerkitoBio/goquery v1.9.3/go.mod h1:1ndLHPdTz+DyQPICCWYlYQMPl0oXZj0G6D4LCYA6u4U= github.com/agnivade/levenshtein v1.2.0 h1:U9L4IOT0Y3i0TIlUIDJ7rVUziKi/zPbrJGaFrtYH3SY= @@ -87,10 +81,6 @@ golang.org/x/sys v0.28.0 h1:Fksou7UEQUWlKvIdsqzJmUmCX3cZuD2+P3XyyzwMhlA= golang.org/x/sys v0.28.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/text v0.21.0 h1:zyQAAkrwaneQ066sspRyJaG9VNi/YJ1NfzcGB3hZ/qo= golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ= -golang.org/x/time v0.5.0 h1:o7cqy6amK/52YcAKIPlM3a+Fpj35zvRj2TP+e1xFSfk= -golang.org/x/time v0.5.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM= -google.golang.org/protobuf v1.36.1 h1:yBPeRvTftaleIgM3PZ/WBIZ7XM/eEYAaEyCwvyjq/gk= -google.golang.org/protobuf v1.36.1/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= modernc.org/gc/v3 v3.0.0-20240107210532-573471604cb6 h1:5D53IMaUuA5InSeMu9eJtlQXS2NxAhyWQvkKEgXZhHI= diff --git a/graph/generated.go b/graph/generated.go index efe72537..550df842 100644 --- a/graph/generated.go +++ b/graph/generated.go @@ -42,6 +42,7 @@ type Config struct { } type ResolverRoot interface { + Account() AccountResolver ListedSecurity() ListedSecurityResolver Mutation() MutationResolver Portfolio() PortfolioResolver @@ -54,6 +55,12 @@ type DirectiveRoot struct { } type ComplexityRoot struct { + Account struct { + DisplayName func(childComplexity int) int + ID func(childComplexity int) int + ReferenceAccount func(childComplexity int) int + } + BankAccount struct { DisplayName func(childComplexity int) int ID func(childComplexity int) int @@ -118,6 +125,8 @@ type ComplexityRoot struct { } Query struct { + Account func(childComplexity int, id string) int + Accounts func(childComplexity int) int Portfolio func(childComplexity int, id string) int Portfolios func(childComplexity int) int Securities func(childComplexity int) int @@ -132,6 +141,9 @@ type ComplexityRoot struct { } } +type AccountResolver interface { + ReferenceAccount(ctx context.Context, obj *persistence.Account) (*persistence.BankAccount, error) +} type ListedSecurityResolver interface { Security(ctx context.Context, obj *persistence.ListedSecurity) (*persistence.Security, error) @@ -158,6 +170,8 @@ type QueryResolver interface { Securities(ctx context.Context) ([]*persistence.Security, error) Portfolio(ctx context.Context, id string) (*persistence.Portfolio, error) Portfolios(ctx context.Context) ([]*persistence.Portfolio, error) + Account(ctx context.Context, id string) (*persistence.Account, error) + Accounts(ctx context.Context) ([]*persistence.Account, error) } type SecurityResolver interface { QuoteProvider(ctx context.Context, obj *persistence.Security) (*string, error) @@ -183,6 +197,27 @@ func (e *executableSchema) Complexity(typeName, field string, childComplexity in _ = ec switch typeName + "." + field { + case "Account.displayName": + if e.complexity.Account.DisplayName == nil { + break + } + + return e.complexity.Account.DisplayName(childComplexity), true + + case "Account.id": + if e.complexity.Account.ID == nil { + break + } + + return e.complexity.Account.ID(childComplexity), true + + case "Account.referenceAccount": + if e.complexity.Account.ReferenceAccount == nil { + break + } + + return e.complexity.Account.ReferenceAccount(childComplexity), true + case "BankAccount.displayName": if e.complexity.BankAccount.DisplayName == nil { break @@ -481,6 +516,25 @@ func (e *executableSchema) Complexity(typeName, field string, childComplexity in return e.complexity.PortfolioSnapshot.TotalPurchaseValue(childComplexity), true + case "Query.account": + if e.complexity.Query.Account == nil { + break + } + + args, err := ec.field_Query_account_args(context.TODO(), rawArgs) + if err != nil { + return 0, false + } + + return e.complexity.Query.Account(childComplexity, args["id"].(string)), true + + case "Query.accounts": + if e.complexity.Query.Accounts == nil { + break + } + + return e.complexity.Query.Accounts(childComplexity), true + case "Query.portfolio": if e.complexity.Query.Portfolio == nil { break @@ -830,6 +884,29 @@ func (ec *executionContext) field_Query___type_argsName( return zeroVal, nil } +func (ec *executionContext) field_Query_account_args(ctx context.Context, rawArgs map[string]any) (map[string]any, error) { + var err error + args := map[string]any{} + arg0, err := ec.field_Query_account_argsID(ctx, rawArgs) + if err != nil { + return nil, err + } + args["id"] = arg0 + return args, nil +} +func (ec *executionContext) field_Query_account_argsID( + ctx context.Context, + rawArgs map[string]any, +) (string, error) { + ctx = graphql.WithPathContext(ctx, graphql.NewPathWithField("id")) + if tmp, ok := rawArgs["id"]; ok { + return ec.unmarshalNString2string(ctx, tmp) + } + + var zeroVal string + return zeroVal, nil +} + func (ec *executionContext) field_Query_portfolio_args(ctx context.Context, rawArgs map[string]any) (map[string]any, error) { var err error args := map[string]any{} @@ -930,6 +1007,141 @@ func (ec *executionContext) field___Type_fields_argsIncludeDeprecated( // region **************************** field.gotpl ***************************** +func (ec *executionContext) _Account_id(ctx context.Context, field graphql.CollectedField, obj *persistence.Account) (ret graphql.Marshaler) { + fc, err := ec.fieldContext_Account_id(ctx, field) + if err != nil { + return graphql.Null + } + ctx = graphql.WithFieldContext(ctx, fc) + defer func() { + if r := recover(); r != nil { + ec.Error(ctx, ec.Recover(ctx, r)) + ret = graphql.Null + } + }() + resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (any, error) { + ctx = rctx // use context from middleware stack in children + return obj.ID, nil + }) + if err != nil { + ec.Error(ctx, err) + return graphql.Null + } + if resTmp == nil { + if !graphql.HasFieldError(ctx, fc) { + ec.Errorf(ctx, "must not be null") + } + return graphql.Null + } + res := resTmp.(string) + fc.Result = res + return ec.marshalNString2string(ctx, field.Selections, res) +} + +func (ec *executionContext) fieldContext_Account_id(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { + fc = &graphql.FieldContext{ + Object: "Account", + Field: field, + IsMethod: false, + IsResolver: false, + Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + return nil, errors.New("field of type String does not have child fields") + }, + } + return fc, nil +} + +func (ec *executionContext) _Account_displayName(ctx context.Context, field graphql.CollectedField, obj *persistence.Account) (ret graphql.Marshaler) { + fc, err := ec.fieldContext_Account_displayName(ctx, field) + if err != nil { + return graphql.Null + } + ctx = graphql.WithFieldContext(ctx, fc) + defer func() { + if r := recover(); r != nil { + ec.Error(ctx, ec.Recover(ctx, r)) + ret = graphql.Null + } + }() + resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (any, error) { + ctx = rctx // use context from middleware stack in children + return obj.DisplayName, nil + }) + if err != nil { + ec.Error(ctx, err) + return graphql.Null + } + if resTmp == nil { + if !graphql.HasFieldError(ctx, fc) { + ec.Errorf(ctx, "must not be null") + } + return graphql.Null + } + res := resTmp.(string) + fc.Result = res + return ec.marshalNString2string(ctx, field.Selections, res) +} + +func (ec *executionContext) fieldContext_Account_displayName(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { + fc = &graphql.FieldContext{ + Object: "Account", + Field: field, + IsMethod: false, + IsResolver: false, + Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + return nil, errors.New("field of type String does not have child fields") + }, + } + return fc, nil +} + +func (ec *executionContext) _Account_referenceAccount(ctx context.Context, field graphql.CollectedField, obj *persistence.Account) (ret graphql.Marshaler) { + fc, err := ec.fieldContext_Account_referenceAccount(ctx, field) + if err != nil { + return graphql.Null + } + ctx = graphql.WithFieldContext(ctx, fc) + defer func() { + if r := recover(); r != nil { + ec.Error(ctx, ec.Recover(ctx, r)) + ret = graphql.Null + } + }() + resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (any, error) { + ctx = rctx // use context from middleware stack in children + return ec.resolvers.Account().ReferenceAccount(rctx, obj) + }) + if err != nil { + ec.Error(ctx, err) + return graphql.Null + } + if resTmp == nil { + return graphql.Null + } + res := resTmp.(*persistence.BankAccount) + fc.Result = res + return ec.marshalOBankAccount2ᚖgithubᚗcomᚋoxistoᚋmoneyᚑgopherᚋpersistenceᚐBankAccount(ctx, field.Selections, res) +} + +func (ec *executionContext) fieldContext_Account_referenceAccount(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { + fc = &graphql.FieldContext{ + Object: "Account", + Field: field, + IsMethod: true, + IsResolver: true, + Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + switch field.Name { + case "id": + return ec.fieldContext_BankAccount_id(ctx, field) + case "displayName": + return ec.fieldContext_BankAccount_displayName(ctx, field) + } + return nil, fmt.Errorf("no field named %q was found under type BankAccount", field.Name) + }, + } + return fc, nil +} + func (ec *executionContext) _BankAccount_id(ctx context.Context, field graphql.CollectedField, obj *persistence.BankAccount) (ret graphql.Marshaler) { fc, err := ec.fieldContext_BankAccount_id(ctx, field) if err != nil { @@ -3122,6 +3334,118 @@ func (ec *executionContext) fieldContext_Query_portfolios(_ context.Context, fie return fc, nil } +func (ec *executionContext) _Query_account(ctx context.Context, field graphql.CollectedField) (ret graphql.Marshaler) { + fc, err := ec.fieldContext_Query_account(ctx, field) + if err != nil { + return graphql.Null + } + ctx = graphql.WithFieldContext(ctx, fc) + defer func() { + if r := recover(); r != nil { + ec.Error(ctx, ec.Recover(ctx, r)) + ret = graphql.Null + } + }() + resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (any, error) { + ctx = rctx // use context from middleware stack in children + return ec.resolvers.Query().Account(rctx, fc.Args["id"].(string)) + }) + if err != nil { + ec.Error(ctx, err) + return graphql.Null + } + if resTmp == nil { + return graphql.Null + } + res := resTmp.(*persistence.Account) + fc.Result = res + return ec.marshalOAccount2ᚖgithubᚗcomᚋoxistoᚋmoneyᚑgopherᚋpersistenceᚐAccount(ctx, field.Selections, res) +} + +func (ec *executionContext) fieldContext_Query_account(ctx context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { + fc = &graphql.FieldContext{ + Object: "Query", + Field: field, + IsMethod: true, + IsResolver: true, + Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + switch field.Name { + case "id": + return ec.fieldContext_Account_id(ctx, field) + case "displayName": + return ec.fieldContext_Account_displayName(ctx, field) + case "referenceAccount": + return ec.fieldContext_Account_referenceAccount(ctx, field) + } + return nil, fmt.Errorf("no field named %q was found under type Account", field.Name) + }, + } + defer func() { + if r := recover(); r != nil { + err = ec.Recover(ctx, r) + ec.Error(ctx, err) + } + }() + ctx = graphql.WithFieldContext(ctx, fc) + if fc.Args, err = ec.field_Query_account_args(ctx, field.ArgumentMap(ec.Variables)); err != nil { + ec.Error(ctx, err) + return fc, err + } + return fc, nil +} + +func (ec *executionContext) _Query_accounts(ctx context.Context, field graphql.CollectedField) (ret graphql.Marshaler) { + fc, err := ec.fieldContext_Query_accounts(ctx, field) + if err != nil { + return graphql.Null + } + ctx = graphql.WithFieldContext(ctx, fc) + defer func() { + if r := recover(); r != nil { + ec.Error(ctx, ec.Recover(ctx, r)) + ret = graphql.Null + } + }() + resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (any, error) { + ctx = rctx // use context from middleware stack in children + return ec.resolvers.Query().Accounts(rctx) + }) + if err != nil { + ec.Error(ctx, err) + return graphql.Null + } + if resTmp == nil { + if !graphql.HasFieldError(ctx, fc) { + ec.Errorf(ctx, "must not be null") + } + return graphql.Null + } + res := resTmp.([]*persistence.Account) + fc.Result = res + return ec.marshalNAccount2ᚕᚖgithubᚗcomᚋoxistoᚋmoneyᚑgopherᚋpersistenceᚐAccountᚄ(ctx, field.Selections, res) +} + +func (ec *executionContext) fieldContext_Query_accounts(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { + fc = &graphql.FieldContext{ + Object: "Query", + Field: field, + IsMethod: true, + IsResolver: true, + Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + switch field.Name { + case "id": + return ec.fieldContext_Account_id(ctx, field) + case "displayName": + return ec.fieldContext_Account_displayName(ctx, field) + case "referenceAccount": + return ec.fieldContext_Account_referenceAccount(ctx, field) + } + return nil, fmt.Errorf("no field named %q was found under type Account", field.Name) + }, + } + return fc, nil +} + func (ec *executionContext) _Query___type(ctx context.Context, field graphql.CollectedField) (ret graphql.Marshaler) { fc, err := ec.fieldContext_Query___type(ctx, field) if err != nil { @@ -5323,6 +5647,83 @@ func (ec *executionContext) unmarshalInputSecurityInput(ctx context.Context, obj // region **************************** object.gotpl **************************** +var accountImplementors = []string{"Account"} + +func (ec *executionContext) _Account(ctx context.Context, sel ast.SelectionSet, obj *persistence.Account) graphql.Marshaler { + fields := graphql.CollectFields(ec.OperationContext, sel, accountImplementors) + + out := graphql.NewFieldSet(fields) + deferred := make(map[string]*graphql.FieldSet) + for i, field := range fields { + switch field.Name { + case "__typename": + out.Values[i] = graphql.MarshalString("Account") + case "id": + out.Values[i] = ec._Account_id(ctx, field, obj) + if out.Values[i] == graphql.Null { + atomic.AddUint32(&out.Invalids, 1) + } + case "displayName": + out.Values[i] = ec._Account_displayName(ctx, field, obj) + if out.Values[i] == graphql.Null { + atomic.AddUint32(&out.Invalids, 1) + } + case "referenceAccount": + field := field + + innerFunc := func(ctx context.Context, _ *graphql.FieldSet) (res graphql.Marshaler) { + defer func() { + if r := recover(); r != nil { + ec.Error(ctx, ec.Recover(ctx, r)) + } + }() + res = ec._Account_referenceAccount(ctx, field, obj) + return res + } + + if field.Deferrable != nil { + dfs, ok := deferred[field.Deferrable.Label] + di := 0 + if ok { + dfs.AddField(field) + di = len(dfs.Values) - 1 + } else { + dfs = graphql.NewFieldSet([]graphql.CollectedField{field}) + deferred[field.Deferrable.Label] = dfs + } + dfs.Concurrently(di, func(ctx context.Context) graphql.Marshaler { + return innerFunc(ctx, dfs) + }) + + // don't run the out.Concurrently() call below + out.Values[i] = graphql.Null + continue + } + + out.Concurrently(i, func(ctx context.Context) graphql.Marshaler { return innerFunc(ctx, out) }) + default: + panic("unknown field " + strconv.Quote(field.Name)) + } + } + out.Dispatch(ctx) + if out.Invalids > 0 { + return graphql.Null + } + + atomic.AddInt32(&ec.deferred, int32(len(deferred))) + + for label, dfs := range deferred { + ec.processDeferredGroup(graphql.DeferredGroup{ + Label: label, + Path: graphql.GetPath(ctx), + FieldSet: dfs, + Context: ctx, + }) + } + + return out +} + var bankAccountImplementors = []string{"BankAccount"} func (ec *executionContext) _BankAccount(ctx context.Context, sel ast.SelectionSet, obj *persistence.BankAccount) graphql.Marshaler { @@ -6108,6 +6509,47 @@ func (ec *executionContext) _Query(ctx context.Context, sel ast.SelectionSet) gr func(ctx context.Context) graphql.Marshaler { return innerFunc(ctx, out) }) } + out.Concurrently(i, func(ctx context.Context) graphql.Marshaler { return rrm(innerCtx) }) + case "account": + field := field + + innerFunc := func(ctx context.Context, _ *graphql.FieldSet) (res graphql.Marshaler) { + defer func() { + if r := recover(); r != nil { + ec.Error(ctx, ec.Recover(ctx, r)) + } + }() + res = ec._Query_account(ctx, field) + return res + } + + rrm := func(ctx context.Context) graphql.Marshaler { + return ec.OperationContext.RootResolverMiddleware(ctx, + func(ctx context.Context) graphql.Marshaler { return innerFunc(ctx, out) }) + } + + out.Concurrently(i, func(ctx context.Context) graphql.Marshaler { return rrm(innerCtx) }) + case "accounts": + field := field + + innerFunc := func(ctx context.Context, fs *graphql.FieldSet) (res graphql.Marshaler) { + defer func() { + if r := recover(); r != nil { + ec.Error(ctx, ec.Recover(ctx, r)) + } + }() + res = ec._Query_accounts(ctx, field) + if res == graphql.Null { + atomic.AddUint32(&fs.Invalids, 1) + } + return res + } + + rrm := func(ctx context.Context) graphql.Marshaler { + return ec.OperationContext.RootResolverMiddleware(ctx, + func(ctx context.Context) graphql.Marshaler { return innerFunc(ctx, out) }) + } + out.Concurrently(i, func(ctx context.Context) graphql.Marshaler { return rrm(innerCtx) }) case "__type": out.Values[i] = ec.OperationContext.RootResolverMiddleware(innerCtx, func(ctx context.Context) (res graphql.Marshaler) { @@ -6576,6 +7018,60 @@ func (ec *executionContext) ___Type(ctx context.Context, sel ast.SelectionSet, o // region ***************************** type.gotpl ***************************** +func (ec *executionContext) marshalNAccount2ᚕᚖgithubᚗcomᚋoxistoᚋmoneyᚑgopherᚋpersistenceᚐAccountᚄ(ctx context.Context, sel ast.SelectionSet, v []*persistence.Account) graphql.Marshaler { + ret := make(graphql.Array, len(v)) + var wg sync.WaitGroup + isLen1 := len(v) == 1 + if !isLen1 { + wg.Add(len(v)) + } + for i := range v { + i := i + fc := &graphql.FieldContext{ + Index: &i, + Result: &v[i], + } + ctx := graphql.WithFieldContext(ctx, fc) + f := func(i int) { + defer func() { + if r := recover(); r != nil { + ec.Error(ctx, ec.Recover(ctx, r)) + ret = nil + } + }() + if !isLen1 { + defer wg.Done() + } + ret[i] = ec.marshalNAccount2ᚖgithubᚗcomᚋoxistoᚋmoneyᚑgopherᚋpersistenceᚐAccount(ctx, sel, v[i]) + } + if isLen1 { + f(i) + } else { + go f(i) + } + + } + wg.Wait() + + for _, e := range ret { + if e == graphql.Null { + return graphql.Null + } + } + + return ret +} + +func (ec *executionContext) marshalNAccount2ᚖgithubᚗcomᚋoxistoᚋmoneyᚑgopherᚋpersistenceᚐAccount(ctx context.Context, sel ast.SelectionSet, v *persistence.Account) graphql.Marshaler { + if v == nil { + if !graphql.HasFieldError(ctx, graphql.GetFieldContext(ctx)) { + ec.Errorf(ctx, "the requested element is null which the schema does not allow") + } + return graphql.Null + } + return ec._Account(ctx, sel, v) +} + func (ec *executionContext) marshalNBankAccount2githubᚗcomᚋoxistoᚋmoneyᚑgopherᚋpersistenceᚐBankAccount(ctx context.Context, sel ast.SelectionSet, v persistence.BankAccount) graphql.Marshaler { return ec._BankAccount(ctx, sel, &v) } @@ -7259,6 +7755,20 @@ func (ec *executionContext) marshalN__TypeKind2string(ctx context.Context, sel a return res } +func (ec *executionContext) marshalOAccount2ᚖgithubᚗcomᚋoxistoᚋmoneyᚑgopherᚋpersistenceᚐAccount(ctx context.Context, sel ast.SelectionSet, v *persistence.Account) graphql.Marshaler { + if v == nil { + return graphql.Null + } + return ec._Account(ctx, sel, v) +} + +func (ec *executionContext) marshalOBankAccount2ᚖgithubᚗcomᚋoxistoᚋmoneyᚑgopherᚋpersistenceᚐBankAccount(ctx context.Context, sel ast.SelectionSet, v *persistence.BankAccount) graphql.Marshaler { + if v == nil { + return graphql.Null + } + return ec._BankAccount(ctx, sel, v) +} + func (ec *executionContext) unmarshalOBoolean2bool(ctx context.Context, v any) (bool, error) { res, err := graphql.UnmarshalBoolean(v) return res, graphql.ErrorOnPath(ctx, err) diff --git a/graph/schema.graphqls b/graph/schema.graphqls index ed0fcbfa..911aa015 100644 --- a/graph/schema.graphqls +++ b/graph/schema.graphqls @@ -121,6 +121,12 @@ type BankAccount { displayName: String! } +type Account { + id: String! + displayName: String! + referenceAccount: BankAccount +} + input SecurityInput { id: String! displayName: String! @@ -156,4 +162,7 @@ type Query { portfolio(id: String!): Portfolio portfolios: [Portfolio!]! + + account(id: String!): Account + accounts: [Account!]! } diff --git a/graph/schema.resolvers.go b/graph/schema.resolvers.go index ceb19e07..4165b52d 100644 --- a/graph/schema.resolvers.go +++ b/graph/schema.resolvers.go @@ -15,6 +15,11 @@ import ( "github.com/oxisto/money-gopher/persistence" ) +// ReferenceAccount is the resolver for the referenceAccount field. +func (r *accountResolver) ReferenceAccount(ctx context.Context, obj *persistence.Account) (*persistence.BankAccount, error) { + panic(fmt.Errorf("not implemented: ReferenceAccount - referenceAccount")) +} + // Security is the resolver for the security field. func (r *listedSecurityResolver) Security(ctx context.Context, obj *persistence.ListedSecurity) (*persistence.Security, error) { panic(fmt.Errorf("not implemented: Security - security")) @@ -171,6 +176,16 @@ func (r *queryResolver) Portfolios(ctx context.Context) ([]*persistence.Portfoli return r.DB.ListPortfolios(ctx) } +// Account is the resolver for the account field. +func (r *queryResolver) Account(ctx context.Context, id string) (*persistence.Account, error) { + panic(fmt.Errorf("not implemented: Account - account")) +} + +// Accounts is the resolver for the accounts field. +func (r *queryResolver) Accounts(ctx context.Context) ([]*persistence.Account, error) { + panic(fmt.Errorf("not implemented: Accounts - accounts")) +} + // QuoteProvider is the resolver for the quoteProvider field. func (r *securityResolver) QuoteProvider(ctx context.Context, obj *persistence.Security) (*string, error) { if obj.QuoteProvider.Valid { @@ -185,6 +200,9 @@ func (r *securityResolver) ListedAs(ctx context.Context, obj *persistence.Securi return obj.ListedAs(ctx, r.DB) } +// Account returns AccountResolver implementation. +func (r *Resolver) Account() AccountResolver { return &accountResolver{r} } + // ListedSecurity returns ListedSecurityResolver implementation. func (r *Resolver) ListedSecurity() ListedSecurityResolver { return &listedSecurityResolver{r} } @@ -203,6 +221,7 @@ func (r *Resolver) Query() QueryResolver { return &queryResolver{r} } // Security returns SecurityResolver implementation. func (r *Resolver) Security() SecurityResolver { return &securityResolver{r} } +type accountResolver struct{ *Resolver } type listedSecurityResolver struct{ *Resolver } type mutationResolver struct{ *Resolver } type portfolioResolver struct{ *Resolver } diff --git a/mgo.proto b/mgo.proto deleted file mode 100644 index 3bfdaaf2..00000000 --- a/mgo.proto +++ /dev/null @@ -1,338 +0,0 @@ -syntax = "proto3"; - -package mgo.portfolio.v1; - -import "google/api/annotations.proto"; -import "google/api/field_behavior.proto"; -import "google/protobuf/empty.proto"; -import "google/protobuf/field_mask.proto"; -import "google/protobuf/timestamp.proto"; - -option go_package = "github.com/oxisto/money-gopher/gen;portfoliov1"; - -// Currency is a currency value in the lowest unit of the selected currency -// (e.g., cents for EUR/USD). -message Currency { - int32 value = 1 [(google.api.field_behavior) = REQUIRED]; - string symbol = 2 [(google.api.field_behavior) = REQUIRED]; -} - -message CreatePortfolioRequest { - Portfolio portfolio = 1 [(google.api.field_behavior) = REQUIRED]; -} - -message ListPortfoliosRequest {} -message ListPortfoliosResponse { - repeated Portfolio portfolios = 1 [(google.api.field_behavior) = REQUIRED]; -} - -message GetPortfolioRequest { - string id = 1 [(google.api.field_behavior) = REQUIRED]; -} - -message UpdatePortfolioRequest { - Portfolio portfolio = 1 [(google.api.field_behavior) = REQUIRED]; - google.protobuf.FieldMask updateMask = 2 [(google.api.field_behavior) = REQUIRED]; -} - -message DeletePortfolioRequest { - string id = 1 [(google.api.field_behavior) = REQUIRED]; -} - -message GetPortfolioSnapshotRequest { - // PortfolioId is the identifier of the portfolio we want to - // "snapshot". - string portfolio_id = 1 [(google.api.field_behavior) = REQUIRED]; - - // Time is the point in time of the requested snapshot. - google.protobuf.Timestamp time = 2 [(google.api.field_behavior) = REQUIRED]; -} - -message CreatePortfolioTransactionRequest { - PortfolioEvent transaction = 1 [(google.api.field_behavior) = REQUIRED]; -} - -message GetPortfolioTransactionRequest { - string id = 1 [(google.api.field_behavior) = REQUIRED]; -} - -message ListPortfolioTransactionsRequest { - string portfolio_id = 1 [(google.api.field_behavior) = REQUIRED]; -} - -message ListPortfolioTransactionsResponse { - repeated PortfolioEvent transactions = 1 [(google.api.field_behavior) = REQUIRED]; -} - -message UpdatePortfolioTransactionRequest { - PortfolioEvent transaction = 1 [(google.api.field_behavior) = REQUIRED]; - google.protobuf.FieldMask updateMask = 2 [(google.api.field_behavior) = REQUIRED]; -} - -message DeletePortfolioTransactionRequest { - int32 transaction_id = 1 [(google.api.field_behavior) = REQUIRED]; -} - -message ImportTransactionsRequest { - string portfolio_id = 1 [(google.api.field_behavior) = REQUIRED]; - string from_csv = 2 [(google.api.field_behavior) = REQUIRED]; -} - -message CreateBankAccountRequest { - BankAccount bank_account = 1 [(google.api.field_behavior) = REQUIRED]; -} - -message UpdateBankAccountRequest { - BankAccount account = 1 [(google.api.field_behavior) = REQUIRED]; - google.protobuf.FieldMask updateMask = 2 [(google.api.field_behavior) = REQUIRED]; -} - -message DeleteBankAccountRequest { - string id = 1 [(google.api.field_behavior) = REQUIRED]; -} - -message Portfolio { - string id = 1 [(google.api.field_behavior) = REQUIRED]; - - string display_name = 2 [(google.api.field_behavior) = REQUIRED]; - - // BankAccountId contains the id/identifier of the underlying bank - // account. - string bank_account_id = 3 [(google.api.field_behavior) = REQUIRED]; - - // Events contains all portfolio events, such as buy/sell transactions, - // dividends or other. They need to be ordered by time (ascending). - repeated PortfolioEvent events = 5; -} - -message BankAccount { - string id = 1 [(google.api.field_behavior) = REQUIRED]; - - string display_name = 2 [(google.api.field_behavior) = REQUIRED]; -} - -// PortfolioSnapshot represents a snapshot in time of the portfolio. It can for -// example be the current state of the portfolio but also represent the state of -// the portfolio at a certain time in the past. -message PortfolioSnapshot { - // Time is the time when this snapshot was taken. - google.protobuf.Timestamp time = 1 [(google.api.field_behavior) = REQUIRED]; - - // Positions holds the current positions within the snapshot and their value. - map positions = 2 [(google.api.field_behavior) = REQUIRED]; - - // FirstTransactionTime is the time of the first transaction with the - // snapshot. - optional google.protobuf.Timestamp first_transaction_time = 3 [(google.api.field_behavior) = REQUIRED]; - - // TotalPurchaseValue contains the total purchase value of all asset positions - Currency total_purchase_value = 10 [(google.api.field_behavior) = REQUIRED]; - - // TotalMarketValue contains the total market value of all asset positions - Currency total_market_value = 11 [(google.api.field_behavior) = REQUIRED]; - - // TotalProfitOrLoss contains the total absolute amount of profit or loss in - // this snapshot, based on asset value. - Currency total_profit_or_loss = 20 [(google.api.field_behavior) = REQUIRED]; - - // TotalGains contains the total relative amount of profit or loss in this - // snapshot, based on asset value. - double total_gains = 21 [(google.api.field_behavior) = REQUIRED]; - - // Cash contains the current amount of cash in the portfolio's bank - // account(s). - Currency cash = 22 [(google.api.field_behavior) = REQUIRED]; - - // TotalPortfolioValue contains the amount of cash plus the total market value - // of all assets. - Currency total_portfolio_value = 23 [(google.api.field_behavior) = REQUIRED]; -} - -message PortfolioPosition { - Security security = 1 [(google.api.field_behavior) = REQUIRED]; - - double amount = 2 [(google.api.field_behavior) = REQUIRED]; - - // PurchaseValue was the market value of this position when it was bought - // (net; exclusive of any fees). - Currency purchase_value = 5 [(google.api.field_behavior) = REQUIRED]; - - // PurchasePrice was the market price of this position when it was bought - // (net; exclusive of any fees). - Currency purchase_price = 6 [(google.api.field_behavior) = REQUIRED]; - - // MarketValue is the current market value of this position, as retrieved from - // the securities service. - Currency market_value = 10 [(google.api.field_behavior) = REQUIRED]; - - // MarketPrice is the current market price of this position, as retrieved from - // the securities service. - Currency market_price = 11 [(google.api.field_behavior) = REQUIRED]; - - // TotalFees is the total amount of fees accumulating in this position through - // various transactions. - Currency total_fees = 15 [(google.api.field_behavior) = REQUIRED]; - - // ProfitOrLoss contains the absolute amount of profit or loss in this - // position. - Currency profit_or_loss = 20 [(google.api.field_behavior) = REQUIRED]; - - // Gains contains the relative amount of profit or loss in this position. - double gains = 21 [(google.api.field_behavior) = REQUIRED]; -} - -enum PortfolioEventType { - PORTFOLIO_EVENT_TYPE_UNSPECIFIED = 0; - - PORTFOLIO_EVENT_TYPE_BUY = 1; - PORTFOLIO_EVENT_TYPE_SELL = 2; - PORTFOLIO_EVENT_TYPE_DELIVERY_INBOUND = 3; - PORTFOLIO_EVENT_TYPE_DELIVERY_OUTBOUND = 4; - - PORTFOLIO_EVENT_TYPE_DIVIDEND = 10; - PORTFOLIO_EVENT_TYPE_INTEREST = 11; - - PORTFOLIO_EVENT_TYPE_DEPOSIT_CASH = 20; - PORTFOLIO_EVENT_TYPE_WITHDRAW_CASH = 21; - - PORTFOLIO_EVENT_TYPE_ACCOUNT_FEES = 30; - PORTFOLIO_EVENT_TYPE_TAX_REFUND = 31; -} - -message PortfolioEvent { - string id = 1 [(google.api.field_behavior) = REQUIRED]; - PortfolioEventType type = 2 [(google.api.field_behavior) = REQUIRED]; - google.protobuf.Timestamp time = 3 [(google.api.field_behavior) = REQUIRED]; - string portfolio_id = 4 [(google.api.field_behavior) = REQUIRED]; - string security_id = 5 [(google.api.field_behavior) = REQUIRED]; - - double amount = 10 [(google.api.field_behavior) = REQUIRED]; - Currency price = 11 [(google.api.field_behavior) = REQUIRED]; - Currency fees = 12 [(google.api.field_behavior) = REQUIRED]; - Currency taxes = 13 [(google.api.field_behavior) = REQUIRED]; -} - -service PortfolioService { - rpc CreatePortfolio(CreatePortfolioRequest) returns (Portfolio) { - option (google.api.http) = { - post: "/v1/portfolios" - body: "portfolio" - }; - } - rpc ListPortfolios(ListPortfoliosRequest) returns (ListPortfoliosResponse) { - option idempotency_level = NO_SIDE_EFFECTS; - option (google.api.http) = {get: "/v1/portfolios"}; - } - rpc GetPortfolio(GetPortfolioRequest) returns (Portfolio) { - option idempotency_level = NO_SIDE_EFFECTS; - option (google.api.http) = {get: "/v1/portfolios/{id}"}; - } - rpc UpdatePortfolio(UpdatePortfolioRequest) returns (Portfolio); - rpc DeletePortfolio(DeletePortfolioRequest) returns (google.protobuf.Empty); - - rpc GetPortfolioSnapshot(GetPortfolioSnapshotRequest) returns (PortfolioSnapshot) { - option idempotency_level = NO_SIDE_EFFECTS; - option (google.api.http) = {get: "/v1/portfolios/{portfolio_id}/snapshot"}; - } - - rpc CreatePortfolioTransaction(CreatePortfolioTransactionRequest) returns (PortfolioEvent) { - option (google.api.http) = { - post: "/v1/portfolios/{transaction.portfolio_id}/transactions" - body: "transaction" - }; - } - rpc GetPortfolioTransaction(GetPortfolioTransactionRequest) returns (PortfolioEvent) { - option idempotency_level = NO_SIDE_EFFECTS; - option (google.api.http) = {get: "/v1/transactions/{id}"}; - } - rpc ListPortfolioTransactions(ListPortfolioTransactionsRequest) returns (ListPortfolioTransactionsResponse) { - option idempotency_level = NO_SIDE_EFFECTS; - option (google.api.http) = {get: "/v1/portfolios/{portfolio_id}/transactions"}; - } - rpc UpdatePortfolioTransaction(UpdatePortfolioTransactionRequest) returns (PortfolioEvent) { - option (google.api.http) = { - put: "/v1/transactions/{transaction.id}" - body: "transaction" - }; - } - rpc DeletePortfolioTransaction(DeletePortfolioTransactionRequest) returns (google.protobuf.Empty); - - rpc ImportTransactions(ImportTransactionsRequest) returns (google.protobuf.Empty); - - rpc CreateBankAccount(CreateBankAccountRequest) returns (BankAccount); - rpc UpdateBankAccount(UpdateBankAccountRequest) returns (BankAccount); - rpc DeleteBankAccount(DeleteBankAccountRequest) returns (google.protobuf.Empty); -} - -message Security { - // Id contains the unique resource ID. For a stock or bond, this should be - // an ISIN. - string id = 1 [(google.api.field_behavior) = REQUIRED]; - - // DisplayName contains the human readable id. - string display_name = 2 [(google.api.field_behavior) = REQUIRED]; - - repeated ListedSecurity listed_on = 4 [(google.api.field_behavior) = REQUIRED]; - - optional string quote_provider = 10 [(google.api.field_behavior) = REQUIRED]; -} - -message ListedSecurity { - string security_id = 1 [(google.api.field_behavior) = REQUIRED]; - string ticker = 3 [(google.api.field_behavior) = REQUIRED]; - string currency = 4 [(google.api.field_behavior) = REQUIRED]; - - optional Currency latest_quote = 5; - optional google.protobuf.Timestamp latest_quote_timestamp = 6; -} - -message ListSecuritiesRequest { - message Filter { - repeated string security_ids = 1; - } - - optional Filter filter = 5; -} - -message ListSecuritiesResponse { - repeated Security securities = 1 [(google.api.field_behavior) = REQUIRED]; -} - -message GetSecurityRequest { - string id = 1 [(google.api.field_behavior) = REQUIRED]; -} - -message CreateSecurityRequest { - Security security = 1 [(google.api.field_behavior) = REQUIRED]; -} - -message UpdateSecurityRequest { - Security security = 1 [(google.api.field_behavior) = REQUIRED]; - google.protobuf.FieldMask updateMask = 2 [(google.api.field_behavior) = REQUIRED]; -} - -message DeleteSecurityRequest { - string id = 1 [(google.api.field_behavior) = REQUIRED]; -} - -message TriggerQuoteUpdateRequest { - repeated string security_ids = 1; -} - -message TriggerQuoteUpdateResponse {} - -service SecuritiesService { - rpc ListSecurities(ListSecuritiesRequest) returns (ListSecuritiesResponse) { - option idempotency_level = NO_SIDE_EFFECTS; - option (google.api.http) = {get: "/v1/securities"}; - } - rpc GetSecurity(GetSecurityRequest) returns (Security) { - option idempotency_level = NO_SIDE_EFFECTS; - option (google.api.http) = {get: "/v1/securities/{id}"}; - } - rpc CreateSecurity(CreateSecurityRequest) returns (Security); - rpc UpdateSecurity(UpdateSecurityRequest) returns (Security); - rpc DeleteSecurity(DeleteSecurityRequest) returns (google.protobuf.Empty); - - rpc TriggerSecurityQuoteUpdate(TriggerQuoteUpdateRequest) returns (TriggerQuoteUpdateResponse); -} diff --git a/money-gopher.code-workspace b/money-gopher.code-workspace index a11d0e2e..8b61e426 100644 --- a/money-gopher.code-workspace +++ b/money-gopher.code-workspace @@ -9,6 +9,7 @@ ], "settings": { "cSpell.words": [ + "agnivade", "Banse", "bufbuild", "clitest", @@ -18,6 +19,8 @@ "errgroup", "fatih", "fieldmaskpb", + "gqlgen", + "gqlparser", "headlessui", "heroicons", "isatty", @@ -25,11 +28,14 @@ "jotaen", "kongcompletion", "lmittmann", + "mapstructure", + "mattn", "mcli", "mfridman", "modernc", "moneyd", "moneygopher", + "multierr", "mybank", "mycash", "myportfolio", @@ -38,20 +44,23 @@ "persistencetest", "portfoliotest", "portfoliov", - "protobuf", - "protocmp", - "protoreflect", + "pressly", "quotetest", "rgossiaux", "secmap", "secres", "servertest", + "sethvargo", + "shurcoo", "sidebaritems", + "sosodev", "sqlc", "steeze", "tailwindcss", "timestamppb", - "tparse" + "tparse", + "urfave", + "vektah" ], "editor.tabSize": 4, "typescript.tsdk": "ui-next/node_modules/typescript/lib", diff --git a/persistence/portfolios.sql.go b/persistence/accounts.sql.go similarity index 65% rename from persistence/portfolios.sql.go rename to persistence/accounts.sql.go index 32220a45..364f83ef 100644 --- a/persistence/portfolios.sql.go +++ b/persistence/accounts.sql.go @@ -1,14 +1,47 @@ // Code generated by sqlc. DO NOT EDIT. // versions: // sqlc v1.27.0 -// source: portfolios.sql +// source: accounts.sql package persistence import ( "context" + + "github.com/oxisto/money-gopher/portfolio/accounts" ) +const createAccount = `-- name: CreateAccount :one +INSERT INTO + accounts (id, display_name, type, reference_account_id) +VALUES + (?, ?, ?, ?) RETURNING id, display_name, type, reference_account_id +` + +type CreateAccountParams struct { + ID string + DisplayName string + Type accounts.AccountType + ReferenceAccountID interface{} +} + +func (q *Queries) CreateAccount(ctx context.Context, arg CreateAccountParams) (*Account, error) { + row := q.db.QueryRowContext(ctx, createAccount, + arg.ID, + arg.DisplayName, + arg.Type, + arg.ReferenceAccountID, + ) + var i Account + err := row.Scan( + &i.ID, + &i.DisplayName, + &i.Type, + &i.ReferenceAccountID, + ) + return &i, err +} + const createBankAccount = `-- name: CreateBankAccount :one INSERT INTO bank_accounts (id, display_name) @@ -48,6 +81,27 @@ func (q *Queries) CreatePortfolio(ctx context.Context, arg CreatePortfolioParams return &i, err } +const getAccount = `-- name: GetAccount :one +SELECT + id, display_name, type, reference_account_id +FROM + accounts +WHERE + id = ? +` + +func (q *Queries) GetAccount(ctx context.Context, id string) (*Account, error) { + row := q.db.QueryRowContext(ctx, getAccount, id) + var i Account + err := row.Scan( + &i.ID, + &i.DisplayName, + &i.Type, + &i.ReferenceAccountID, + ) + return &i, err +} + const getBankAccount = `-- name: GetBankAccount :one SELECT id, display_name @@ -80,6 +134,43 @@ func (q *Queries) GetPortfolio(ctx context.Context, id string) (*Portfolio, erro return &i, err } +const listAccounts = `-- name: ListAccounts :many +SELECT + id, display_name, type, reference_account_id +FROM + accounts +ORDER BY + id +` + +func (q *Queries) ListAccounts(ctx context.Context) ([]*Account, error) { + rows, err := q.db.QueryContext(ctx, listAccounts) + if err != nil { + return nil, err + } + defer rows.Close() + var items []*Account + for rows.Next() { + var i Account + if err := rows.Scan( + &i.ID, + &i.DisplayName, + &i.Type, + &i.ReferenceAccountID, + ); err != nil { + return nil, err + } + items = append(items, &i) + } + if err := rows.Close(); err != nil { + return nil, err + } + if err := rows.Err(); err != nil { + return nil, err + } + return items, nil +} + const listPortfolioEventsByPortfolioID = `-- name: ListPortfolioEventsByPortfolioID :many SELECT id, type, time, portfolio_id, security_id, amount, price, fees, taxes diff --git a/persistence/models.go b/persistence/models.go index e2470844..69ddc4f1 100644 --- a/persistence/models.go +++ b/persistence/models.go @@ -9,6 +9,7 @@ import ( "time" currency "github.com/oxisto/money-gopher/currency" + "github.com/oxisto/money-gopher/portfolio/accounts" "github.com/oxisto/money-gopher/portfolio/events" ) @@ -19,7 +20,8 @@ type Account struct { // DisplayName is the human-readable name of the brokerage account. DisplayName string // Type is the type of the account. - Type int64 + Type accounts.AccountType + ReferenceAccountID interface{} } type BankAccount struct { @@ -53,6 +55,12 @@ type Portfolio struct { BankAccountID string } +// PortfolioAccounts represents the relationship between portfolios and accounts. +type PortfolioAccount struct { + PortfolioID string + AccountID string +} + type PortfolioEvent struct { ID string Type events.PortfolioEventType diff --git a/persistence/sql/migrations/0002_create_accounts.sql b/persistence/sql/migrations/0002_create_accounts.sql index d310bdbf..7038422c 100644 --- a/persistence/sql/migrations/0002_create_accounts.sql +++ b/persistence/sql/migrations/0002_create_accounts.sql @@ -34,7 +34,19 @@ CREATE TABLE -- account which comprise a portfolio. id TEXT PRIMARY KEY, -- ID is the primary identifier for a brokerage account. display_name TEXT NOT NULL, -- DisplayName is the human-readable name of the brokerage account. - type INTEGER NOT NULL -- Type is the type of the account. + type INTEGER NOT NULL, -- Type is the type of the account. + reference_account_id INTEGER -- ReferenceAccountID is the ID of the account that this account is related to. For example, if this is a brokerage account, the reference account could be a bank account. + FOREIGN KEY (reference_account_id) REFERENCES accounts (id) ON DELETE RESTRICT + ); + +CREATE TABLE + IF NOT EXISTS portfolio_accounts ( + -- PortfolioAccounts represents the relationship between portfolios and accounts. + portfolio_id TEXT NOT NULL, + account_id TEXT NOT NULL, + FOREIGN KEY (portfolio_id) REFERENCES portfolios (id) ON DELETE RESTRICT, + FOREIGN KEY (account_id) REFERENCES accounts (id) ON DELETE RESTRICT, + PRIMARY KEY (account_id, portfolio_id) ); -- +goose Down @@ -42,6 +54,8 @@ DROP TABLE portfolios; DROP TABLE portfolio_events; +DROP TABLE portfolio_accounts; + DROP TABLE bank_accounts; DROP TABLE accounts; \ No newline at end of file diff --git a/persistence/sql/queries/portfolios.sql b/persistence/sql/queries/accounts.sql similarity index 67% rename from persistence/sql/queries/portfolios.sql rename to persistence/sql/queries/accounts.sql index ff22b67e..e776b90c 100644 --- a/persistence/sql/queries/portfolios.sql +++ b/persistence/sql/queries/accounts.sql @@ -22,6 +22,22 @@ FROM WHERE portfolio_id = ?; +-- name: ListAccounts :many +SELECT + * +FROM + accounts +ORDER BY + id; + +-- name: GetAccount :one +SELECT + * +FROM + accounts +WHERE + id = ?; + -- name: GetBankAccount :one SELECT * @@ -30,6 +46,12 @@ FROM WHERE id = ?; +-- name: CreateAccount :one +INSERT INTO + accounts (id, display_name, type, reference_account_id) +VALUES + (?, ?, ?, ?) RETURNING *; + -- name: CreateBankAccount :one INSERT INTO bank_accounts (id, display_name) diff --git a/securities/quote/quote_provider_ing_test.go b/securities/quote/quote_provider_ing_test.go index a97ba24d..f9a337be 100644 --- a/securities/quote/quote_provider_ing_test.go +++ b/securities/quote/quote_provider_ing_test.go @@ -27,7 +27,6 @@ import ( "github.com/oxisto/money-gopher/currency" "github.com/oxisto/money-gopher/persistence" - "google.golang.org/protobuf/testing/protocmp" "github.com/oxisto/assert" ) @@ -114,9 +113,9 @@ func Test_ing_LatestQuote(t *testing.T) { Client: tt.fields.Client, } gotQuote, gotTime, err := ing.LatestQuote(tt.args.ctx, tt.args.ls) - assert.Equals(t, true, tt.wantErr(t, err), protocmp.Transform()) - assert.Equals(t, tt.wantQuote, gotQuote, protocmp.Transform()) - assert.Equals(t, tt.wantTime.UTC(), gotTime.UTC(), protocmp.Transform()) + assert.Equals(t, true, tt.wantErr(t, err)) + assert.Equals(t, tt.wantQuote, gotQuote) + assert.Equals(t, tt.wantTime.UTC(), gotTime.UTC()) }) } } diff --git a/securities/quote/quote_provider_test.go b/securities/quote/quote_provider_test.go deleted file mode 100644 index 3e2b3602..00000000 --- a/securities/quote/quote_provider_test.go +++ /dev/null @@ -1 +0,0 @@ -package quote diff --git a/securities/quote/quote_provider_yf_test.go b/securities/quote/quote_provider_yf_test.go index 79e425f5..cf31f527 100644 --- a/securities/quote/quote_provider_yf_test.go +++ b/securities/quote/quote_provider_yf_test.go @@ -27,7 +27,6 @@ import ( "github.com/oxisto/money-gopher/currency" "github.com/oxisto/money-gopher/persistence" - "google.golang.org/protobuf/testing/protocmp" "github.com/oxisto/assert" ) @@ -148,9 +147,9 @@ func Test_yf_LatestQuote(t *testing.T) { Client: tt.fields.Client, } gotQuote, gotTime, err := yf.LatestQuote(tt.args.ctx, tt.args.ls) - assert.Equals(t, true, tt.wantErr(t, err), protocmp.Transform()) - assert.Equals(t, tt.wantQuote, gotQuote, protocmp.Transform()) - assert.Equals(t, tt.wantTime.UTC(), gotTime.UTC(), protocmp.Transform()) + assert.Equals(t, true, tt.wantErr(t, err)) + assert.Equals(t, tt.wantQuote, gotQuote) + assert.Equals(t, tt.wantTime.UTC(), gotTime.UTC()) }) } } diff --git a/server/interceptors.go b/server/interceptors.go deleted file mode 100644 index 5d61f777..00000000 --- a/server/interceptors.go +++ /dev/null @@ -1,98 +0,0 @@ -// Copyright 2024 Christian Banse -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. -// -// This file is part of The Money Gopher. - -package server - -import ( - "context" - "errors" - "log/slog" - "strings" - - "connectrpc.com/connect" - "github.com/MicahParks/keyfunc/v3" - "github.com/golang-jwt/jwt/v5" - "github.com/lmittmann/tint" -) - -// NewSimpleLoggingInterceptor returns a new simple logging interceptor. -func NewSimpleLoggingInterceptor() connect.UnaryInterceptorFunc { - interceptor := func(next connect.UnaryFunc) connect.UnaryFunc { - return connect.UnaryFunc(func( - ctx context.Context, - req connect.AnyRequest, - ) (connect.AnyResponse, error) { - slog.Debug("Handling RPC Request", - slog.Group("req", - "procedure", req.Spec().Procedure, - "httpmethod", req.HTTPMethod(), - )) - return next(ctx, req) - }) - } - - return connect.UnaryInterceptorFunc(interceptor) -} - -// NewAuthInterceptor returns a new auth interceptor. -func NewAuthInterceptor() connect.UnaryInterceptorFunc { - interceptor := func(next connect.UnaryFunc) connect.UnaryFunc { - k, err := keyfunc.NewDefault([]string{"http://localhost:8000/certs"}) - if err != nil { - slog.Error("Error while setting up JWKS", tint.Err(err)) - } - - return connect.UnaryFunc(func( - ctx context.Context, - req connect.AnyRequest, - ) (connect.AnyResponse, error) { - var ( - claims jwt.RegisteredClaims - auth string - token string - err error - ok bool - ) - auth = req.Header().Get("Authorization") - if auth == "" { - return nil, connect.NewError( - connect.CodeUnauthenticated, - errors.New("no token provided"), - ) - } - - token, ok = strings.CutPrefix(auth, "Bearer ") - if !ok { - return nil, connect.NewError( - connect.CodeUnauthenticated, - errors.New("no token provided"), - ) - } - - _, err = jwt.ParseWithClaims(token, &claims, k.Keyfunc) - if err != nil { - return nil, connect.NewError( - connect.CodeUnauthenticated, - err, - ) - } - - ctx = context.WithValue(ctx, "claims", claims) - return next(ctx, req) - }) - } - return connect.UnaryInterceptorFunc(interceptor) -} diff --git a/sqlc.yaml b/sqlc.yaml index 5cf75e43..216025ed 100644 --- a/sqlc.yaml +++ b/sqlc.yaml @@ -9,6 +9,8 @@ sql: out: "persistence" emit_result_struct_pointers: true overrides: + - column: "accounts.type" + go_type: github.com/oxisto/money-gopher/portfolio/accounts.AccountType - column: "portfolio_events.type" go_type: github.com/oxisto/money-gopher/portfolio/events.PortfolioEventType - column: "portfolio_events.price" From b1fff5c72a79ef84061bd37b6e612e7df850bc81 Mon Sep 17 00:00:00 2001 From: Christian Banse Date: Mon, 6 Jan 2025 11:35:30 +0100 Subject: [PATCH 24/35] Creating account works --- cli/commands/account.go | 59 ++-- cli/commands/account_test.go | 62 +++++ cli/commands/init.go | 2 +- cmd/mgo/mgo.go | 5 +- gqlgen.yml | 3 +- graph/generated.go | 251 ++++++++++++++++++ graph/schema.graphqls | 27 ++ graph/schema.resolvers.go | 13 +- models/models_gen.go | 7 + persistence/accounts.sql.go | 3 +- persistence/models.go | 5 +- .../sql/migrations/0002_create_accounts.sql | 2 +- portfolio/accounts/type.go | 58 ++++ 13 files changed, 465 insertions(+), 32 deletions(-) create mode 100644 cli/commands/account_test.go diff --git a/cli/commands/account.go b/cli/commands/account.go index d053abae..013afcfa 100644 --- a/cli/commands/account.go +++ b/cli/commands/account.go @@ -18,46 +18,63 @@ package commands import ( "context" + "fmt" mcli "github.com/oxisto/money-gopher/cli" + "github.com/oxisto/money-gopher/models" + "github.com/oxisto/money-gopher/portfolio/accounts" "github.com/urfave/cli/v3" ) -// BankAccountCmd is the command for bank account related commands. -var BankAccountCmd = &cli.Command{ - Name: "bank-account", - Usage: "Manage bank accounts", +// AccountCmd is the command for account related commands. +var AccountCmd = &cli.Command{ + Name: "account", + Usage: "Manage accounts", Before: mcli.InjectSession, Commands: []*cli.Command{ { Name: "create", - Usage: "Creates a new bank account", - Action: CreateBankAccount, + Usage: "Creates a new account", + Action: CreateAccount, Flags: []cli.Flag{ - &cli.StringFlag{Name: "id", Usage: "The identifier of the portfolio, e.g. mybank-myportfolio", Required: true}, - &cli.StringFlag{Name: "display-name", Usage: "The display name of the portfolio"}, + &cli.StringFlag{Name: "id", Usage: "The unique ID for the account", Required: true}, + &cli.StringFlag{Name: "display-name", Usage: "The display name of the account", Required: true}, + &cli.GenericFlag{Name: "type", Usage: "The type of bank account", Value: func() *accounts.AccountType { + var typ accounts.AccountType = accounts.AccountTypeBrokerage + return &typ + }()}, }, }, }, } -// CreateBankAccount creates a new bank account. -func CreateBankAccount(ctx context.Context, cmd *cli.Command) error { - /*s := mcli.FromContext(ctx) - res, err := s.PortfolioClient.CreateBankAccount( - context.Background(), - connect.NewRequest(&portfoliov1.CreateBankAccountRequest{ - BankAccount: &portfoliov1.BankAccount{ - Id: cmd.String("id"), - DisplayName: cmd.String("display-name"), - }, - }), - ) +// CreateAccount creates a new bank account. +func CreateAccount(ctx context.Context, cmd *cli.Command) (err error) { + s := mcli.FromContext(ctx) + + fmt.Printf("%+v", cmd.Generic("type")) + + var query struct { + CreateAccount struct { + ID string `json:"id"` + DisplayName string `json:"displayName"` + Type accounts.AccountType `json:"type"` + } `graphql:"createAccount(input: $input)" json:"account"` + } + + err = s.GraphQL.Mutate(context.Background(), &query, map[string]interface{}{ + "input": models.AccountInput{ + ID: cmd.String("id"), + DisplayName: cmd.String("display-name"), + Type: *cmd.Generic("type").(*accounts.AccountType), + }, + }) if err != nil { return err } - fmt.Fprint(cmd.Writer, res.Msg)*/ + fmt.Fprintln(cmd.Writer, query.CreateAccount) + return nil } diff --git a/cli/commands/account_test.go b/cli/commands/account_test.go new file mode 100644 index 00000000..61cd4743 --- /dev/null +++ b/cli/commands/account_test.go @@ -0,0 +1,62 @@ +// Copyright 2023 Christian Banse +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// +// This file is part of The Money Gopher. + +package commands + +import ( + "context" + "testing" + + "github.com/oxisto/money-gopher/internal" + "github.com/oxisto/money-gopher/internal/testing/clitest" + "github.com/oxisto/money-gopher/internal/testing/servertest" + "github.com/urfave/cli/v3" +) + +func TestCreateAccount(t *testing.T) { + srv := servertest.NewServer(internal.NewTestDB(t)) + defer srv.Close() + + type args struct { + ctx context.Context + cmd *cli.Command + } + tests := []struct { + name string + args args + wantErr bool + }{ + { + name: "happy path", + args: args{ + ctx: clitest.NewSessionContext(t, srv), + cmd: clitest.MockCommand(t, + AccountCmd.Command("create").Flags, + "--id", "myaccount", + "--display-name", "My Account", + "--type", "BANK", + ), + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if err := CreateAccount(tt.args.ctx, tt.args.cmd); (err != nil) != tt.wantErr { + t.Errorf("CreateAccount() error = %v, wantErr %v", err, tt.wantErr) + } + }) + } +} diff --git a/cli/commands/init.go b/cli/commands/init.go index 429d43ab..5128898c 100644 --- a/cli/commands/init.go +++ b/cli/commands/init.go @@ -31,7 +31,7 @@ var CLICmd = &cli.Command{ Commands: []*cli.Command{ PortfolioCmd, SecuritiesCmd, - BankAccountCmd, + AccountCmd, LoginCmd, }, } diff --git a/cmd/mgo/mgo.go b/cmd/mgo/mgo.go index f2061338..a2d1d689 100644 --- a/cmd/mgo/mgo.go +++ b/cmd/mgo/mgo.go @@ -18,15 +18,14 @@ package main import ( "context" - "log/slog" + "fmt" "os" - "github.com/lmittmann/tint" "github.com/oxisto/money-gopher/cli/commands" ) func main() { if err := commands.CLICmd.Run(context.Background(), os.Args); err != nil { - slog.Error("Error while running command", tint.Err(err)) + fmt.Println(err) } } diff --git a/gqlgen.yml b/gqlgen.yml index f750b9b6..03407afb 100644 --- a/gqlgen.yml +++ b/gqlgen.yml @@ -120,4 +120,5 @@ call_argument_directives_with_null: true autobind: - "github.com/oxisto/money-gopher/persistence" - "github.com/oxisto/money-gopher/portfolio/events" - - "github.com/oxisto/money-gopher/currency" \ No newline at end of file + - "github.com/oxisto/money-gopher/portfolio/accounts" + - "github.com/oxisto/money-gopher/currency" diff --git a/graph/generated.go b/graph/generated.go index 550df842..83f739f4 100644 --- a/graph/generated.go +++ b/graph/generated.go @@ -17,6 +17,7 @@ import ( "github.com/oxisto/money-gopher/currency" "github.com/oxisto/money-gopher/models" "github.com/oxisto/money-gopher/persistence" + "github.com/oxisto/money-gopher/portfolio/accounts" "github.com/oxisto/money-gopher/portfolio/events" gqlparser "github.com/vektah/gqlparser/v2" "github.com/vektah/gqlparser/v2/ast" @@ -59,6 +60,7 @@ type ComplexityRoot struct { DisplayName func(childComplexity int) int ID func(childComplexity int) int ReferenceAccount func(childComplexity int) int + Type func(childComplexity int) int } BankAccount struct { @@ -80,6 +82,7 @@ type ComplexityRoot struct { } Mutation struct { + CreateAccount func(childComplexity int, input models.AccountInput) int CreatePortfolio func(childComplexity int, input models.PortfolioInput) int CreateSecurity func(childComplexity int, input models.SecurityInput) int TriggerQuoteUpdate func(childComplexity int, securityIDs []string) int @@ -153,6 +156,7 @@ type MutationResolver interface { CreateSecurity(ctx context.Context, input models.SecurityInput) (*persistence.Security, error) UpdateSecurity(ctx context.Context, id string, input models.SecurityInput) (*persistence.Security, error) CreatePortfolio(ctx context.Context, input models.PortfolioInput) (*persistence.Portfolio, error) + CreateAccount(ctx context.Context, input models.AccountInput) (*persistence.Account, error) TriggerQuoteUpdate(ctx context.Context, securityIDs []string) ([]*persistence.ListedSecurity, error) } type PortfolioResolver interface { @@ -218,6 +222,13 @@ func (e *executableSchema) Complexity(typeName, field string, childComplexity in return e.complexity.Account.ReferenceAccount(childComplexity), true + case "Account.type": + if e.complexity.Account.Type == nil { + break + } + + return e.complexity.Account.Type(childComplexity), true + case "BankAccount.displayName": if e.complexity.BankAccount.DisplayName == nil { break @@ -281,6 +292,18 @@ func (e *executableSchema) Complexity(typeName, field string, childComplexity in return e.complexity.ListedSecurity.Ticker(childComplexity), true + case "Mutation.createAccount": + if e.complexity.Mutation.CreateAccount == nil { + break + } + + args, err := ec.field_Mutation_createAccount_args(context.TODO(), rawArgs) + if err != nil { + return 0, false + } + + return e.complexity.Mutation.CreateAccount(childComplexity, args["input"].(models.AccountInput)), true + case "Mutation.createPortfolio": if e.complexity.Mutation.CreatePortfolio == nil { break @@ -609,6 +632,7 @@ func (e *executableSchema) Exec(ctx context.Context) graphql.ResponseHandler { opCtx := graphql.GetOperationContext(ctx) ec := executionContext{opCtx, e, 0, 0, make(chan graphql.DeferredResult)} inputUnmarshalMap := graphql.BuildUnmarshalerMap( + ec.unmarshalInputAccountInput, ec.unmarshalInputListedSecurityInput, ec.unmarshalInputPortfolioInput, ec.unmarshalInputSecurityInput, @@ -728,6 +752,29 @@ var parsedSchema = gqlparser.MustLoadSchema(sources...) // region ***************************** args.gotpl ***************************** +func (ec *executionContext) field_Mutation_createAccount_args(ctx context.Context, rawArgs map[string]any) (map[string]any, error) { + var err error + args := map[string]any{} + arg0, err := ec.field_Mutation_createAccount_argsInput(ctx, rawArgs) + if err != nil { + return nil, err + } + args["input"] = arg0 + return args, nil +} +func (ec *executionContext) field_Mutation_createAccount_argsInput( + ctx context.Context, + rawArgs map[string]any, +) (models.AccountInput, error) { + ctx = graphql.WithPathContext(ctx, graphql.NewPathWithField("input")) + if tmp, ok := rawArgs["input"]; ok { + return ec.unmarshalNAccountInput2githubᚗcomᚋoxistoᚋmoneyᚑgopherᚋmodelsᚐAccountInput(ctx, tmp) + } + + var zeroVal models.AccountInput + return zeroVal, nil +} + func (ec *executionContext) field_Mutation_createPortfolio_args(ctx context.Context, rawArgs map[string]any) (map[string]any, error) { var err error args := map[string]any{} @@ -1095,6 +1142,50 @@ func (ec *executionContext) fieldContext_Account_displayName(_ context.Context, return fc, nil } +func (ec *executionContext) _Account_type(ctx context.Context, field graphql.CollectedField, obj *persistence.Account) (ret graphql.Marshaler) { + fc, err := ec.fieldContext_Account_type(ctx, field) + if err != nil { + return graphql.Null + } + ctx = graphql.WithFieldContext(ctx, fc) + defer func() { + if r := recover(); r != nil { + ec.Error(ctx, ec.Recover(ctx, r)) + ret = graphql.Null + } + }() + resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (any, error) { + ctx = rctx // use context from middleware stack in children + return obj.Type, nil + }) + if err != nil { + ec.Error(ctx, err) + return graphql.Null + } + if resTmp == nil { + if !graphql.HasFieldError(ctx, fc) { + ec.Errorf(ctx, "must not be null") + } + return graphql.Null + } + res := resTmp.(accounts.AccountType) + fc.Result = res + return ec.marshalNAccountType2githubᚗcomᚋoxistoᚋmoneyᚑgopherᚋportfolioᚋaccountsᚐAccountType(ctx, field.Selections, res) +} + +func (ec *executionContext) fieldContext_Account_type(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { + fc = &graphql.FieldContext{ + Object: "Account", + Field: field, + IsMethod: false, + IsResolver: false, + Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + return nil, errors.New("field of type AccountType does not have child fields") + }, + } + return fc, nil +} + func (ec *executionContext) _Account_referenceAccount(ctx context.Context, field graphql.CollectedField, obj *persistence.Account) (ret graphql.Marshaler) { fc, err := ec.fieldContext_Account_referenceAccount(ctx, field) if err != nil { @@ -1745,6 +1836,71 @@ func (ec *executionContext) fieldContext_Mutation_createPortfolio(ctx context.Co return fc, nil } +func (ec *executionContext) _Mutation_createAccount(ctx context.Context, field graphql.CollectedField) (ret graphql.Marshaler) { + fc, err := ec.fieldContext_Mutation_createAccount(ctx, field) + if err != nil { + return graphql.Null + } + ctx = graphql.WithFieldContext(ctx, fc) + defer func() { + if r := recover(); r != nil { + ec.Error(ctx, ec.Recover(ctx, r)) + ret = graphql.Null + } + }() + resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (any, error) { + ctx = rctx // use context from middleware stack in children + return ec.resolvers.Mutation().CreateAccount(rctx, fc.Args["input"].(models.AccountInput)) + }) + if err != nil { + ec.Error(ctx, err) + return graphql.Null + } + if resTmp == nil { + if !graphql.HasFieldError(ctx, fc) { + ec.Errorf(ctx, "must not be null") + } + return graphql.Null + } + res := resTmp.(*persistence.Account) + fc.Result = res + return ec.marshalNAccount2ᚖgithubᚗcomᚋoxistoᚋmoneyᚑgopherᚋpersistenceᚐAccount(ctx, field.Selections, res) +} + +func (ec *executionContext) fieldContext_Mutation_createAccount(ctx context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { + fc = &graphql.FieldContext{ + Object: "Mutation", + Field: field, + IsMethod: true, + IsResolver: true, + Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + switch field.Name { + case "id": + return ec.fieldContext_Account_id(ctx, field) + case "displayName": + return ec.fieldContext_Account_displayName(ctx, field) + case "type": + return ec.fieldContext_Account_type(ctx, field) + case "referenceAccount": + return ec.fieldContext_Account_referenceAccount(ctx, field) + } + return nil, fmt.Errorf("no field named %q was found under type Account", field.Name) + }, + } + defer func() { + if r := recover(); r != nil { + err = ec.Recover(ctx, r) + ec.Error(ctx, err) + } + }() + ctx = graphql.WithFieldContext(ctx, fc) + if fc.Args, err = ec.field_Mutation_createAccount_args(ctx, field.ArgumentMap(ec.Variables)); err != nil { + ec.Error(ctx, err) + return fc, err + } + return fc, nil +} + func (ec *executionContext) _Mutation_triggerQuoteUpdate(ctx context.Context, field graphql.CollectedField) (ret graphql.Marshaler) { fc, err := ec.fieldContext_Mutation_triggerQuoteUpdate(ctx, field) if err != nil { @@ -3374,6 +3530,8 @@ func (ec *executionContext) fieldContext_Query_account(ctx context.Context, fiel return ec.fieldContext_Account_id(ctx, field) case "displayName": return ec.fieldContext_Account_displayName(ctx, field) + case "type": + return ec.fieldContext_Account_type(ctx, field) case "referenceAccount": return ec.fieldContext_Account_referenceAccount(ctx, field) } @@ -3437,6 +3595,8 @@ func (ec *executionContext) fieldContext_Query_accounts(_ context.Context, field return ec.fieldContext_Account_id(ctx, field) case "displayName": return ec.fieldContext_Account_displayName(ctx, field) + case "type": + return ec.fieldContext_Account_type(ctx, field) case "referenceAccount": return ec.fieldContext_Account_referenceAccount(ctx, field) } @@ -5530,6 +5690,47 @@ func (ec *executionContext) fieldContext___Type_specifiedByURL(_ context.Context // region **************************** input.gotpl ***************************** +func (ec *executionContext) unmarshalInputAccountInput(ctx context.Context, obj any) (models.AccountInput, error) { + var it models.AccountInput + asMap := map[string]any{} + for k, v := range obj.(map[string]any) { + asMap[k] = v + } + + fieldsInOrder := [...]string{"id", "displayName", "type"} + for _, k := range fieldsInOrder { + v, ok := asMap[k] + if !ok { + continue + } + switch k { + case "id": + ctx := graphql.WithPathContext(ctx, graphql.NewPathWithField("id")) + data, err := ec.unmarshalNString2string(ctx, v) + if err != nil { + return it, err + } + it.ID = data + case "displayName": + ctx := graphql.WithPathContext(ctx, graphql.NewPathWithField("displayName")) + data, err := ec.unmarshalNString2string(ctx, v) + if err != nil { + return it, err + } + it.DisplayName = data + case "type": + ctx := graphql.WithPathContext(ctx, graphql.NewPathWithField("type")) + data, err := ec.unmarshalNAccountType2githubᚗcomᚋoxistoᚋmoneyᚑgopherᚋportfolioᚋaccountsᚐAccountType(ctx, v) + if err != nil { + return it, err + } + it.Type = data + } + } + + return it, nil +} + func (ec *executionContext) unmarshalInputListedSecurityInput(ctx context.Context, obj any) (models.ListedSecurityInput, error) { var it models.ListedSecurityInput asMap := map[string]any{} @@ -5668,6 +5869,11 @@ func (ec *executionContext) _Account(ctx context.Context, sel ast.SelectionSet, if out.Values[i] == graphql.Null { atomic.AddUint32(&out.Invalids, 1) } + case "type": + out.Values[i] = ec._Account_type(ctx, field, obj) + if out.Values[i] == graphql.Null { + atomic.AddUint32(&out.Invalids, 1) + } case "referenceAccount": field := field @@ -5967,6 +6173,13 @@ func (ec *executionContext) _Mutation(ctx context.Context, sel ast.SelectionSet) if out.Values[i] == graphql.Null { out.Invalids++ } + case "createAccount": + out.Values[i] = ec.OperationContext.RootResolverMiddleware(innerCtx, func(ctx context.Context) (res graphql.Marshaler) { + return ec._Mutation_createAccount(ctx, field) + }) + if out.Values[i] == graphql.Null { + out.Invalids++ + } case "triggerQuoteUpdate": out.Values[i] = ec.OperationContext.RootResolverMiddleware(innerCtx, func(ctx context.Context) (res graphql.Marshaler) { return ec._Mutation_triggerQuoteUpdate(ctx, field) @@ -7018,6 +7231,10 @@ func (ec *executionContext) ___Type(ctx context.Context, sel ast.SelectionSet, o // region ***************************** type.gotpl ***************************** +func (ec *executionContext) marshalNAccount2githubᚗcomᚋoxistoᚋmoneyᚑgopherᚋpersistenceᚐAccount(ctx context.Context, sel ast.SelectionSet, v persistence.Account) graphql.Marshaler { + return ec._Account(ctx, sel, &v) +} + func (ec *executionContext) marshalNAccount2ᚕᚖgithubᚗcomᚋoxistoᚋmoneyᚑgopherᚋpersistenceᚐAccountᚄ(ctx context.Context, sel ast.SelectionSet, v []*persistence.Account) graphql.Marshaler { ret := make(graphql.Array, len(v)) var wg sync.WaitGroup @@ -7072,6 +7289,40 @@ func (ec *executionContext) marshalNAccount2ᚖgithubᚗcomᚋoxistoᚋmoneyᚑg return ec._Account(ctx, sel, v) } +func (ec *executionContext) unmarshalNAccountInput2githubᚗcomᚋoxistoᚋmoneyᚑgopherᚋmodelsᚐAccountInput(ctx context.Context, v any) (models.AccountInput, error) { + res, err := ec.unmarshalInputAccountInput(ctx, v) + return res, graphql.ErrorOnPath(ctx, err) +} + +func (ec *executionContext) unmarshalNAccountType2githubᚗcomᚋoxistoᚋmoneyᚑgopherᚋportfolioᚋaccountsᚐAccountType(ctx context.Context, v any) (accounts.AccountType, error) { + tmp, err := graphql.UnmarshalString(v) + res := unmarshalNAccountType2githubᚗcomᚋoxistoᚋmoneyᚑgopherᚋportfolioᚋaccountsᚐAccountType[tmp] + return res, graphql.ErrorOnPath(ctx, err) +} + +func (ec *executionContext) marshalNAccountType2githubᚗcomᚋoxistoᚋmoneyᚑgopherᚋportfolioᚋaccountsᚐAccountType(ctx context.Context, sel ast.SelectionSet, v accounts.AccountType) graphql.Marshaler { + res := graphql.MarshalString(marshalNAccountType2githubᚗcomᚋoxistoᚋmoneyᚑgopherᚋportfolioᚋaccountsᚐAccountType[v]) + if res == graphql.Null { + if !graphql.HasFieldError(ctx, graphql.GetFieldContext(ctx)) { + ec.Errorf(ctx, "the requested element is null which the schema does not allow") + } + } + return res +} + +var ( + unmarshalNAccountType2githubᚗcomᚋoxistoᚋmoneyᚑgopherᚋportfolioᚋaccountsᚐAccountType = map[string]accounts.AccountType{ + "BROKERAGE": accounts.AccountTypeBrokerage, + "BANK": accounts.AccountTypeBank, + "LOAN": accounts.AccountTypeLoan, + } + marshalNAccountType2githubᚗcomᚋoxistoᚋmoneyᚑgopherᚋportfolioᚋaccountsᚐAccountType = map[accounts.AccountType]string{ + accounts.AccountTypeBrokerage: "BROKERAGE", + accounts.AccountTypeBank: "BANK", + accounts.AccountTypeLoan: "LOAN", + } +) + func (ec *executionContext) marshalNBankAccount2githubᚗcomᚋoxistoᚋmoneyᚑgopherᚋpersistenceᚐBankAccount(ctx context.Context, sel ast.SelectionSet, v persistence.BankAccount) graphql.Marshaler { return ec._BankAccount(ctx, sel, &v) } diff --git a/graph/schema.graphqls b/graph/schema.graphqls index 911aa015..0a08382a 100644 --- a/graph/schema.graphqls +++ b/graph/schema.graphqls @@ -53,6 +53,24 @@ enum PortfolioEventType ) } +enum AccountType + @goModel( + model: "github.com/oxisto/money-gopher/portfolio/accounts.AccountType" + ) { + BROKERAGE + @goEnum( + value: "github.com/oxisto/money-gopher/portfolio/accounts.AccountTypeBrokerage" + ) + BANK + @goEnum( + value: "github.com/oxisto/money-gopher/portfolio/accounts.AccountTypeBank" + ) + LOAN + @goEnum( + value: "github.com/oxisto/money-gopher/portfolio/accounts.AccountTypeLoan" + ) +} + type PortfolioEvent { time: Date! type: PortfolioEventType! @@ -124,6 +142,7 @@ type BankAccount { type Account { id: String! displayName: String! + type: AccountType! referenceAccount: BankAccount } @@ -138,6 +157,12 @@ input PortfolioInput { displayName: String! } +input AccountInput { + id: String! + displayName: String! + type: AccountType! +} + input ListedSecurityInput { ticker: String! currency: String! @@ -149,6 +174,8 @@ type Mutation { createPortfolio(input: PortfolioInput!): Portfolio! + createAccount(input: AccountInput!): Account! + """ Triggers a quote update for the given security IDs. If no security IDs are provided, all securities will be updated. diff --git a/graph/schema.resolvers.go b/graph/schema.resolvers.go index 4165b52d..f9ba55ee 100644 --- a/graph/schema.resolvers.go +++ b/graph/schema.resolvers.go @@ -110,6 +110,15 @@ func (r *mutationResolver) CreatePortfolio(ctx context.Context, input models.Por panic(fmt.Errorf("not implemented: CreatePortfolio - createPortfolio")) } +// CreateAccount is the resolver for the createAccount field. +func (r *mutationResolver) CreateAccount(ctx context.Context, input models.AccountInput) (*persistence.Account, error) { + return r.DB.CreateAccount(ctx, persistence.CreateAccountParams{ + ID: input.ID, + DisplayName: input.DisplayName, + Type: input.Type, + }) +} + // TriggerQuoteUpdate is the resolver for the triggerQuoteUpdate field. func (r *mutationResolver) TriggerQuoteUpdate(ctx context.Context, securityIDs []string) (updated []*persistence.ListedSecurity, err error) { updated, err = r.QuoteUpdater.UpdateQuotes(ctx, securityIDs) @@ -178,12 +187,12 @@ func (r *queryResolver) Portfolios(ctx context.Context) ([]*persistence.Portfoli // Account is the resolver for the account field. func (r *queryResolver) Account(ctx context.Context, id string) (*persistence.Account, error) { - panic(fmt.Errorf("not implemented: Account - account")) + return r.DB.GetAccount(ctx, id) } // Accounts is the resolver for the accounts field. func (r *queryResolver) Accounts(ctx context.Context) ([]*persistence.Account, error) { - panic(fmt.Errorf("not implemented: Accounts - accounts")) + return r.DB.ListAccounts(ctx) } // QuoteProvider is the resolver for the quoteProvider field. diff --git a/models/models_gen.go b/models/models_gen.go index 5f642105..5a4da2c7 100644 --- a/models/models_gen.go +++ b/models/models_gen.go @@ -5,8 +5,15 @@ package models import ( "github.com/oxisto/money-gopher/currency" "github.com/oxisto/money-gopher/persistence" + "github.com/oxisto/money-gopher/portfolio/accounts" ) +type AccountInput struct { + ID string `json:"id"` + DisplayName string `json:"displayName"` + Type accounts.AccountType `json:"type"` +} + type ListedSecurityInput struct { Ticker string `json:"ticker"` Currency string `json:"currency"` diff --git a/persistence/accounts.sql.go b/persistence/accounts.sql.go index 364f83ef..4d9cb46c 100644 --- a/persistence/accounts.sql.go +++ b/persistence/accounts.sql.go @@ -7,6 +7,7 @@ package persistence import ( "context" + "database/sql" "github.com/oxisto/money-gopher/portfolio/accounts" ) @@ -22,7 +23,7 @@ type CreateAccountParams struct { ID string DisplayName string Type accounts.AccountType - ReferenceAccountID interface{} + ReferenceAccountID sql.NullInt64 } func (q *Queries) CreateAccount(ctx context.Context, arg CreateAccountParams) (*Account, error) { diff --git a/persistence/models.go b/persistence/models.go index 69ddc4f1..d7fa07b8 100644 --- a/persistence/models.go +++ b/persistence/models.go @@ -20,8 +20,9 @@ type Account struct { // DisplayName is the human-readable name of the brokerage account. DisplayName string // Type is the type of the account. - Type accounts.AccountType - ReferenceAccountID interface{} + Type accounts.AccountType + // ReferenceAccountID is the ID of the account that this account is related to. For example, if this is a brokerage account, the reference account could be a bank account. + ReferenceAccountID sql.NullInt64 } type BankAccount struct { diff --git a/persistence/sql/migrations/0002_create_accounts.sql b/persistence/sql/migrations/0002_create_accounts.sql index 7038422c..d253d4d9 100644 --- a/persistence/sql/migrations/0002_create_accounts.sql +++ b/persistence/sql/migrations/0002_create_accounts.sql @@ -35,7 +35,7 @@ CREATE TABLE id TEXT PRIMARY KEY, -- ID is the primary identifier for a brokerage account. display_name TEXT NOT NULL, -- DisplayName is the human-readable name of the brokerage account. type INTEGER NOT NULL, -- Type is the type of the account. - reference_account_id INTEGER -- ReferenceAccountID is the ID of the account that this account is related to. For example, if this is a brokerage account, the reference account could be a bank account. + reference_account_id INTEGER, -- ReferenceAccountID is the ID of the account that this account is related to. For example, if this is a brokerage account, the reference account could be a bank account. FOREIGN KEY (reference_account_id) REFERENCES accounts (id) ON DELETE RESTRICT ); diff --git a/portfolio/accounts/type.go b/portfolio/accounts/type.go index 3be3f091..fc6c7ec9 100644 --- a/portfolio/accounts/type.go +++ b/portfolio/accounts/type.go @@ -1,5 +1,10 @@ package accounts +import ( + "encoding/json" + "fmt" +) + // AccountType is the type of an account. type AccountType int @@ -13,3 +18,56 @@ const ( // AccountTypeUnknown represents an unknown account type. AccountTypeUnknown ) + +func (t *AccountType) Get() any { + return t +} + +func (t *AccountType) Set(v string) error { + switch v { + case "BROKERAGE": + *t = AccountTypeBrokerage + case "BANK": + *t = AccountTypeBank + case "LOAN": + *t = AccountTypeLoan + case "UNKNOWN": + *t = AccountTypeUnknown + default: + return fmt.Errorf("unknown account type: %s", v) + } + + return nil +} + +// String returns the string representation of the account type. This matches +// the enum value of the GraphQL schema. +func (t AccountType) String() string { + switch t { + case AccountTypeBrokerage: + return "BROKERAGE" + case AccountTypeBank: + return "BANK" + case AccountTypeLoan: + return "LOAN" + default: + return "UNKNOWN" + } +} + +// MarshalJSON marshals the account type to JSON using the string +// representation. +func (t AccountType) MarshalJSON() ([]byte, error) { + return []byte(fmt.Sprintf(`"%s"`, t.String())), nil +} + +// UnmarshalJSON unmarshals the account type from JSON. It expects a string +// representation. +func (t *AccountType) UnmarshalJSON(data []byte) error { + var s string + if err := json.Unmarshal(data, &s); err != nil { + return err + } + + return t.Set(s) +} From 4e31beaff4685b714dd9eea64b66ba5655311a71 Mon Sep 17 00:00:00 2001 From: Christian Banse Date: Mon, 6 Jan 2025 14:16:25 +0100 Subject: [PATCH 25/35] Using stringer --- .github/workflows/build.yml | 1 + internal/enum/valueof.go | 25 ++++++++++++++++++ portfolio/accounts/type.go | 44 +++++++------------------------ portfolio/accounts/type_string.go | 26 ++++++++++++++++++ tools/go.mod | 2 +- tools/tools.go | 2 +- 6 files changed, 64 insertions(+), 36 deletions(-) create mode 100644 internal/enum/valueof.go create mode 100644 portfolio/accounts/type_string.go diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 8ee746c0..79eef6af 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -26,6 +26,7 @@ jobs: go install github.com/mfridman/tparse go install github.com/sqlc-dev/sqlc/cmd/sqlc go install github.com/99designs/gqlgen + go install golang.org/x/tools/cmd/stringer working-directory: ./tools - name: Build Backend run: | diff --git a/internal/enum/valueof.go b/internal/enum/valueof.go new file mode 100644 index 00000000..e9ad0a99 --- /dev/null +++ b/internal/enum/valueof.go @@ -0,0 +1,25 @@ +package enum + +import "fmt" + +// ValueOf returns the index of the value v in the name/index slice. +func ValueOf(v string, name string, index []uint8) int { + for i := range len(index) - 1 { + if name[index[i]:index[i+1]] == v { + return i + 1 + } + } + + return -1 +} + +// Set sets the target to the value represented by v (using [ValueOf]). +func Set[T ~int](target *T, v string, name string, index []uint8) error { + i := ValueOf(v, name, index) + if i == -1 { + return fmt.Errorf("unknown value: %s", v) + } else { + *target = T(i) + return nil + } +} diff --git a/portfolio/accounts/type.go b/portfolio/accounts/type.go index fc6c7ec9..a0aaf0ad 100644 --- a/portfolio/accounts/type.go +++ b/portfolio/accounts/type.go @@ -3,56 +3,32 @@ package accounts import ( "encoding/json" "fmt" + + "github.com/oxisto/money-gopher/internal/enum" ) +//go:generate stringer -linecomment -type=AccountType -output=type_string.go + // AccountType is the type of an account. type AccountType int const ( // AccountTypeBrokerage represents a brokerage account. - AccountTypeBrokerage AccountType = iota + 1 + AccountTypeBrokerage AccountType = iota + 1 // BROKERAGE // AccountTypeBank represents a bank account. - AccountTypeBank + AccountTypeBank // BANK // AccountTypeLoan represents a loan account. - AccountTypeLoan - // AccountTypeUnknown represents an unknown account type. - AccountTypeUnknown + AccountTypeLoan // LOAN ) +// Get implements [flag.Getter]. func (t *AccountType) Get() any { return t } +// Set implements [flag.Value]. func (t *AccountType) Set(v string) error { - switch v { - case "BROKERAGE": - *t = AccountTypeBrokerage - case "BANK": - *t = AccountTypeBank - case "LOAN": - *t = AccountTypeLoan - case "UNKNOWN": - *t = AccountTypeUnknown - default: - return fmt.Errorf("unknown account type: %s", v) - } - - return nil -} - -// String returns the string representation of the account type. This matches -// the enum value of the GraphQL schema. -func (t AccountType) String() string { - switch t { - case AccountTypeBrokerage: - return "BROKERAGE" - case AccountTypeBank: - return "BANK" - case AccountTypeLoan: - return "LOAN" - default: - return "UNKNOWN" - } + return enum.Set(t, v, _AccountType_name, _AccountType_index[:]) } // MarshalJSON marshals the account type to JSON using the string diff --git a/portfolio/accounts/type_string.go b/portfolio/accounts/type_string.go new file mode 100644 index 00000000..19de771f --- /dev/null +++ b/portfolio/accounts/type_string.go @@ -0,0 +1,26 @@ +// Code generated by "stringer -linecomment -type=AccountType -output=type_string.go"; DO NOT EDIT. + +package accounts + +import "strconv" + +func _() { + // An "invalid array index" compiler error signifies that the constant values have changed. + // Re-run the stringer command to generate them again. + var x [1]struct{} + _ = x[AccountTypeBrokerage-1] + _ = x[AccountTypeBank-2] + _ = x[AccountTypeLoan-3] +} + +const _AccountType_name = "BROKERAGEBANKLOAN" + +var _AccountType_index = [...]uint8{0, 9, 13, 17} + +func (i AccountType) String() string { + i -= 1 + if i < 0 || i >= AccountType(len(_AccountType_index)-1) { + return "AccountType(" + strconv.FormatInt(int64(i+1), 10) + ")" + } + return _AccountType_name[_AccountType_index[i]:_AccountType_index[i+1]] +} diff --git a/tools/go.mod b/tools/go.mod index 0dc0f6e7..c785ddb5 100644 --- a/tools/go.mod +++ b/tools/go.mod @@ -6,6 +6,7 @@ require ( github.com/99designs/gqlgen v0.17.61 github.com/mfridman/tparse v0.16.0 github.com/sqlc-dev/sqlc v1.27.0 + golang.org/x/tools v0.24.0 ) require ( @@ -65,7 +66,6 @@ require ( golang.org/x/sync v0.10.0 // indirect golang.org/x/sys v0.28.0 // indirect golang.org/x/text v0.21.0 // indirect - golang.org/x/tools v0.24.0 // indirect google.golang.org/genproto/googleapis/api v0.0.0-20240528184218-531527333157 // indirect google.golang.org/genproto/googleapis/rpc v0.0.0-20240528184218-531527333157 // indirect google.golang.org/grpc v1.65.0 // indirect diff --git a/tools/tools.go b/tools/tools.go index 85d7ad32..585622a6 100644 --- a/tools/tools.go +++ b/tools/tools.go @@ -6,6 +6,6 @@ package tools import ( _ "github.com/99designs/gqlgen" _ "github.com/mfridman/tparse" - _ "github.com/mpyw/sqlc-restruct/cmd/sqlc-restruct" _ "github.com/sqlc-dev/sqlc/cmd/sqlc" + _ "golang.org/x/tools/cmd/stringer" ) From e81a55c1a5b9397a7fd1c8527dcc6c91d6ee8d1c Mon Sep 17 00:00:00 2001 From: Christian Banse Date: Mon, 6 Jan 2025 14:22:49 +0100 Subject: [PATCH 26/35] new internal enum package --- internal/enum/valueof.go | 34 ++++++++++++++++++++++++++++++---- portfolio/accounts/type.go | 18 +++++------------- 2 files changed, 35 insertions(+), 17 deletions(-) diff --git a/internal/enum/valueof.go b/internal/enum/valueof.go index e9ad0a99..8b9c88a3 100644 --- a/internal/enum/valueof.go +++ b/internal/enum/valueof.go @@ -1,6 +1,16 @@ package enum -import "fmt" +import ( + "encoding/json" + "flag" + "fmt" +) + +// Enum is an interface for enum types that support [flag.Value]. +type Enum interface { + flag.Value + ~int +} // ValueOf returns the index of the value v in the name/index slice. func ValueOf(v string, name string, index []uint8) int { @@ -13,13 +23,29 @@ func ValueOf(v string, name string, index []uint8) int { return -1 } -// Set sets the target to the value represented by v (using [ValueOf]). -func Set[T ~int](target *T, v string, name string, index []uint8) error { +// Set sets the target enum to the value represented by v (using [ValueOf]). +func Set[T Enum](enum *T, v string, name string, index []uint8) error { i := ValueOf(v, name, index) if i == -1 { return fmt.Errorf("unknown value: %s", v) } else { - *target = T(i) + *enum = T(i) return nil } } + +// MarshalJSON marshals the enum to JSON using the string representation. +func MarshalJSON[T Enum](enum T) ([]byte, error) { + return []byte(fmt.Sprintf(`"%s"`, enum.String())), nil +} + +// UnmarshalJSON unmarshals the enum from JSON. It expects a string +// representation. +func UnmarshalJSON[T Enum](enum T, data []byte) error { + var s string + if err := json.Unmarshal(data, &s); err != nil { + return err + } + + return enum.Set(s) +} diff --git a/portfolio/accounts/type.go b/portfolio/accounts/type.go index a0aaf0ad..a82b8487 100644 --- a/portfolio/accounts/type.go +++ b/portfolio/accounts/type.go @@ -1,9 +1,6 @@ package accounts import ( - "encoding/json" - "fmt" - "github.com/oxisto/money-gopher/internal/enum" ) @@ -27,23 +24,18 @@ func (t *AccountType) Get() any { } // Set implements [flag.Value]. -func (t *AccountType) Set(v string) error { - return enum.Set(t, v, _AccountType_name, _AccountType_index[:]) +func (t AccountType) Set(v string) error { + return enum.Set(&t, v, _AccountType_name, _AccountType_index[:]) } // MarshalJSON marshals the account type to JSON using the string // representation. func (t AccountType) MarshalJSON() ([]byte, error) { - return []byte(fmt.Sprintf(`"%s"`, t.String())), nil + return enum.MarshalJSON(t) } // UnmarshalJSON unmarshals the account type from JSON. It expects a string // representation. -func (t *AccountType) UnmarshalJSON(data []byte) error { - var s string - if err := json.Unmarshal(data, &s); err != nil { - return err - } - - return t.Set(s) +func (t AccountType) UnmarshalJSON(data []byte) error { + return enum.UnmarshalJSON(t, data) } From 5c4a3890fd6a72421009d88972d975fa0e7ff112 Mon Sep 17 00:00:00 2001 From: Christian Banse Date: Mon, 6 Jan 2025 21:04:29 +0100 Subject: [PATCH 27/35] Create and list accounts --- cli/commands/account.go | 27 ++++++++++++++++ cli/commands/account_test.go | 55 +++++++++++++++++++++++++++++++++ graph/schema.resolvers_test.go | 52 +++++++++++++++++++++++++++++++ internal/testdata/securities.go | 15 +++++++++ money-gopher.code-workspace | 1 + 5 files changed, 150 insertions(+) diff --git a/cli/commands/account.go b/cli/commands/account.go index 013afcfa..ce7c0ad1 100644 --- a/cli/commands/account.go +++ b/cli/commands/account.go @@ -46,6 +46,11 @@ var AccountCmd = &cli.Command{ }()}, }, }, + { + Name: "list", + Usage: "Lists all accounts", + Action: ListAccounts, + }, }, } @@ -78,3 +83,25 @@ func CreateAccount(ctx context.Context, cmd *cli.Command) (err error) { return nil } + +// ListAccounts lists all accounts. +func ListAccounts(ctx context.Context, cmd *cli.Command) (err error) { + s := mcli.FromContext(ctx) + + var query struct { + Accounts []struct { + ID string `json:"id"` + DisplayName string `json:"displayName"` + Type accounts.AccountType `json:"type"` + } `json:"accounts"` + } + + err = s.GraphQL.Query(context.Background(), &query, nil) + if err != nil { + return err + } + + s.WriteJSON(cmd.Writer, query) + + return nil +} diff --git a/cli/commands/account_test.go b/cli/commands/account_test.go index 61cd4743..94b998d2 100644 --- a/cli/commands/account_test.go +++ b/cli/commands/account_test.go @@ -20,9 +20,12 @@ import ( "context" "testing" + "github.com/oxisto/assert" "github.com/oxisto/money-gopher/internal" + "github.com/oxisto/money-gopher/internal/testdata" "github.com/oxisto/money-gopher/internal/testing/clitest" "github.com/oxisto/money-gopher/internal/testing/servertest" + "github.com/oxisto/money-gopher/persistence" "github.com/urfave/cli/v3" ) @@ -52,6 +55,7 @@ func TestCreateAccount(t *testing.T) { }, }, } + for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { if err := CreateAccount(tt.args.ctx, tt.args.cmd); (err != nil) != tt.wantErr { @@ -60,3 +64,54 @@ func TestCreateAccount(t *testing.T) { }) } } + +func TestListAccounts(t *testing.T) { + srv := servertest.NewServer(internal.NewTestDB(t, func(db *persistence.DB) { + _, err := db.Queries.CreateAccount(context.Background(), testdata.TestCreateBankAccountParams) + assert.NoError(t, err) + })) + defer srv.Close() + + type args struct { + ctx context.Context + cmd *cli.Command + } + tests := []struct { + name string + args args + wantErr bool + wantRec assert.Want[*clitest.CommandRecorder] + }{ + { + name: "happy path", + args: args{ + ctx: clitest.NewSessionContext(t, srv), + cmd: clitest.MockCommand(t, AccountCmd.Command("list").Flags), + }, + wantRec: func(t *testing.T, rec *clitest.CommandRecorder) bool { + return assert.Equals(t, `{ + "accounts": [ + { + "id": "myaccount", + "displayName": "My Account", + "type": "BANK" + } + ] +} +`, rec.String()) + }, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + rec := clitest.Record(tt.args.cmd) + + if err := ListAccounts(tt.args.ctx, tt.args.cmd); (err != nil) != tt.wantErr { + t.Errorf("ListAccounts() error = %v, wantErr %v", err, tt.wantErr) + } + + tt.wantRec(t, rec) + }) + } +} diff --git a/graph/schema.resolvers_test.go b/graph/schema.resolvers_test.go index 11863bd7..a60ad001 100644 --- a/graph/schema.resolvers_test.go +++ b/graph/schema.resolvers_test.go @@ -2,9 +2,11 @@ package graph import ( "context" + "reflect" "testing" "github.com/oxisto/assert" + "github.com/oxisto/money-gopher/internal/testdata" "github.com/oxisto/money-gopher/internal/testing/persistencetest" "github.com/oxisto/money-gopher/models" "github.com/oxisto/money-gopher/persistence" @@ -75,3 +77,53 @@ func Test_mutationResolver_CreateSecurity(t *testing.T) { }) } } + +func Test_queryResolver_Accounts(t *testing.T) { + type fields struct { + Resolver *Resolver + } + type args struct { + ctx context.Context + } + tests := []struct { + name string + fields fields + args args + want []*persistence.Account + wantErr bool + }{ + { + name: "happy path", + fields: fields{ + Resolver: &Resolver{ + DB: persistencetest.NewTestDB(t, func(db *persistence.DB) { + _, err := db.Queries.CreateAccount(context.Background(), testdata.TestCreateBankAccountParams) + assert.NoError(t, err) + }), + }, + }, + args: args{ + ctx: context.TODO(), + }, + want: []*persistence.Account{ + testdata.TestBankAccount, + }, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + r := &queryResolver{ + Resolver: tt.fields.Resolver, + } + got, err := r.Accounts(tt.args.ctx) + if (err != nil) != tt.wantErr { + t.Errorf("queryResolver.Accounts() error = %v, wantErr %v", err, tt.wantErr) + return + } + if !reflect.DeepEqual(got, tt.want) { + t.Errorf("queryResolver.Accounts() = %v, want %v", got, tt.want) + } + }) + } +} diff --git a/internal/testdata/securities.go b/internal/testdata/securities.go index 9a4c6f12..fde12bdc 100644 --- a/internal/testdata/securities.go +++ b/internal/testdata/securities.go @@ -5,6 +5,7 @@ import ( "github.com/oxisto/money-gopher/internal/testing/quotetest" "github.com/oxisto/money-gopher/persistence" + "github.com/oxisto/money-gopher/portfolio/accounts" ) // TestSecurity is a test security. @@ -35,3 +36,17 @@ var TestUpsertListedSecurityParams = persistence.UpsertListedSecurityParams{ Ticker: TestListedSecurity.Ticker, Currency: TestListedSecurity.Currency, } + +// TestBankAccount is a test bank account. +var TestBankAccount = &persistence.Account{ + ID: "myaccount", + DisplayName: "My Account", + Type: accounts.AccountTypeBank, +} + +// TestCreateBankAccountParams is a test bank account creation parameter. +var TestCreateBankAccountParams = persistence.CreateAccountParams{ + ID: TestBankAccount.ID, + DisplayName: TestBankAccount.DisplayName, + Type: TestBankAccount.Type, +} diff --git a/money-gopher.code-workspace b/money-gopher.code-workspace index 8b61e426..b9c89c34 100644 --- a/money-gopher.code-workspace +++ b/money-gopher.code-workspace @@ -36,6 +36,7 @@ "moneyd", "moneygopher", "multierr", + "myaccount", "mybank", "mycash", "myportfolio", From 59233df126794d2d8b505719d585564c624470d4 Mon Sep 17 00:00:00 2001 From: Christian Banse Date: Mon, 6 Jan 2025 21:27:12 +0100 Subject: [PATCH 28/35] Delete account works --- cli/commands/account.go | 38 ++++++++++ cli/commands/account_test.go | 42 +++++++++++ graph/generated.go | 109 +++++++++++++++++++++++++++ graph/schema.graphqls | 1 + graph/schema.resolvers.go | 5 ++ persistence/accounts.sql.go | 18 +++++ persistence/sql/queries/accounts.sql | 5 ++ 7 files changed, 218 insertions(+) diff --git a/cli/commands/account.go b/cli/commands/account.go index ce7c0ad1..fe4b6bd5 100644 --- a/cli/commands/account.go +++ b/cli/commands/account.go @@ -18,12 +18,14 @@ package commands import ( "context" + "errors" "fmt" mcli "github.com/oxisto/money-gopher/cli" "github.com/oxisto/money-gopher/models" "github.com/oxisto/money-gopher/portfolio/accounts" + "github.com/shurcooL/graphql" "github.com/urfave/cli/v3" ) @@ -51,6 +53,15 @@ var AccountCmd = &cli.Command{ Usage: "Lists all accounts", Action: ListAccounts, }, + { + Name: "delete", + Usage: "Deletes an account", + Action: DeleteAccount, + Flags: []cli.Flag{ + &cli.StringFlag{Name: "id", Usage: "The unique ID for the account", Required: true}, + &cli.BoolFlag{Name: "confirm", Usage: "Confirm account deletion", Required: true}, + }, + }, }, } @@ -105,3 +116,30 @@ func ListAccounts(ctx context.Context, cmd *cli.Command) (err error) { return nil } + +// DeleteAccount deletes an account. +func DeleteAccount(ctx context.Context, cmd *cli.Command) (err error) { + s := mcli.FromContext(ctx) + + // Confirm deletion + if !cmd.Bool("confirm") { + return errors.New("please confirm delete with --confirm") + } + + var query struct { + DeleteAccount struct { + ID string `json:"id"` + } `graphql:"deleteAccount(id: $id)" json:"account"` + } + + err = s.GraphQL.Mutate(context.Background(), &query, map[string]interface{}{ + "id": graphql.String(cmd.String("id")), + }) + if err != nil { + return err + } + + fmt.Fprintf(cmd.Writer, "Account %q deleted.\n", query.DeleteAccount.ID) + + return nil +} diff --git a/cli/commands/account_test.go b/cli/commands/account_test.go index 94b998d2..130d0a9d 100644 --- a/cli/commands/account_test.go +++ b/cli/commands/account_test.go @@ -115,3 +115,45 @@ func TestListAccounts(t *testing.T) { }) } } + +func TestDeleteAccount(t *testing.T) { + srv := servertest.NewServer(internal.NewTestDB(t, func(db *persistence.DB) { + _, err := db.Queries.CreateAccount(context.Background(), testdata.TestCreateBankAccountParams) + assert.NoError(t, err) + })) + defer srv.Close() + + type args struct { + ctx context.Context + cmd *cli.Command + } + tests := []struct { + name string + args args + wantErr bool + wantRec assert.Want[*clitest.CommandRecorder] + }{ + { + name: "happy path", + args: args{ + ctx: clitest.NewSessionContext(t, srv), + cmd: clitest.MockCommand(t, AccountCmd.Command("delete").Flags, "--id", "myaccount", "--confirm"), + }, + wantRec: func(t *testing.T, rec *clitest.CommandRecorder) bool { + return assert.Equals(t, "Account \"myaccount\" deleted.\n", rec.String()) + }, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + rec := clitest.Record(tt.args.cmd) + + if err := DeleteAccount(tt.args.ctx, tt.args.cmd); (err != nil) != tt.wantErr { + t.Errorf("DeleteAccount() error = %v, wantErr %v", err, tt.wantErr) + } + + tt.wantRec(t, rec) + }) + } +} diff --git a/graph/generated.go b/graph/generated.go index 83f739f4..cba512cc 100644 --- a/graph/generated.go +++ b/graph/generated.go @@ -85,6 +85,7 @@ type ComplexityRoot struct { CreateAccount func(childComplexity int, input models.AccountInput) int CreatePortfolio func(childComplexity int, input models.PortfolioInput) int CreateSecurity func(childComplexity int, input models.SecurityInput) int + DeleteAccount func(childComplexity int, id string) int TriggerQuoteUpdate func(childComplexity int, securityIDs []string) int UpdateSecurity func(childComplexity int, id string, input models.SecurityInput) int } @@ -157,6 +158,7 @@ type MutationResolver interface { UpdateSecurity(ctx context.Context, id string, input models.SecurityInput) (*persistence.Security, error) CreatePortfolio(ctx context.Context, input models.PortfolioInput) (*persistence.Portfolio, error) CreateAccount(ctx context.Context, input models.AccountInput) (*persistence.Account, error) + DeleteAccount(ctx context.Context, id string) (*persistence.Account, error) TriggerQuoteUpdate(ctx context.Context, securityIDs []string) ([]*persistence.ListedSecurity, error) } type PortfolioResolver interface { @@ -328,6 +330,18 @@ func (e *executableSchema) Complexity(typeName, field string, childComplexity in return e.complexity.Mutation.CreateSecurity(childComplexity, args["input"].(models.SecurityInput)), true + case "Mutation.deleteAccount": + if e.complexity.Mutation.DeleteAccount == nil { + break + } + + args, err := ec.field_Mutation_deleteAccount_args(context.TODO(), rawArgs) + if err != nil { + return 0, false + } + + return e.complexity.Mutation.DeleteAccount(childComplexity, args["id"].(string)), true + case "Mutation.triggerQuoteUpdate": if e.complexity.Mutation.TriggerQuoteUpdate == nil { break @@ -821,6 +835,29 @@ func (ec *executionContext) field_Mutation_createSecurity_argsInput( return zeroVal, nil } +func (ec *executionContext) field_Mutation_deleteAccount_args(ctx context.Context, rawArgs map[string]any) (map[string]any, error) { + var err error + args := map[string]any{} + arg0, err := ec.field_Mutation_deleteAccount_argsID(ctx, rawArgs) + if err != nil { + return nil, err + } + args["id"] = arg0 + return args, nil +} +func (ec *executionContext) field_Mutation_deleteAccount_argsID( + ctx context.Context, + rawArgs map[string]any, +) (string, error) { + ctx = graphql.WithPathContext(ctx, graphql.NewPathWithField("id")) + if tmp, ok := rawArgs["id"]; ok { + return ec.unmarshalNString2string(ctx, tmp) + } + + var zeroVal string + return zeroVal, nil +} + func (ec *executionContext) field_Mutation_triggerQuoteUpdate_args(ctx context.Context, rawArgs map[string]any) (map[string]any, error) { var err error args := map[string]any{} @@ -1901,6 +1938,71 @@ func (ec *executionContext) fieldContext_Mutation_createAccount(ctx context.Cont return fc, nil } +func (ec *executionContext) _Mutation_deleteAccount(ctx context.Context, field graphql.CollectedField) (ret graphql.Marshaler) { + fc, err := ec.fieldContext_Mutation_deleteAccount(ctx, field) + if err != nil { + return graphql.Null + } + ctx = graphql.WithFieldContext(ctx, fc) + defer func() { + if r := recover(); r != nil { + ec.Error(ctx, ec.Recover(ctx, r)) + ret = graphql.Null + } + }() + resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (any, error) { + ctx = rctx // use context from middleware stack in children + return ec.resolvers.Mutation().DeleteAccount(rctx, fc.Args["id"].(string)) + }) + if err != nil { + ec.Error(ctx, err) + return graphql.Null + } + if resTmp == nil { + if !graphql.HasFieldError(ctx, fc) { + ec.Errorf(ctx, "must not be null") + } + return graphql.Null + } + res := resTmp.(*persistence.Account) + fc.Result = res + return ec.marshalNAccount2ᚖgithubᚗcomᚋoxistoᚋmoneyᚑgopherᚋpersistenceᚐAccount(ctx, field.Selections, res) +} + +func (ec *executionContext) fieldContext_Mutation_deleteAccount(ctx context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { + fc = &graphql.FieldContext{ + Object: "Mutation", + Field: field, + IsMethod: true, + IsResolver: true, + Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + switch field.Name { + case "id": + return ec.fieldContext_Account_id(ctx, field) + case "displayName": + return ec.fieldContext_Account_displayName(ctx, field) + case "type": + return ec.fieldContext_Account_type(ctx, field) + case "referenceAccount": + return ec.fieldContext_Account_referenceAccount(ctx, field) + } + return nil, fmt.Errorf("no field named %q was found under type Account", field.Name) + }, + } + defer func() { + if r := recover(); r != nil { + err = ec.Recover(ctx, r) + ec.Error(ctx, err) + } + }() + ctx = graphql.WithFieldContext(ctx, fc) + if fc.Args, err = ec.field_Mutation_deleteAccount_args(ctx, field.ArgumentMap(ec.Variables)); err != nil { + ec.Error(ctx, err) + return fc, err + } + return fc, nil +} + func (ec *executionContext) _Mutation_triggerQuoteUpdate(ctx context.Context, field graphql.CollectedField) (ret graphql.Marshaler) { fc, err := ec.fieldContext_Mutation_triggerQuoteUpdate(ctx, field) if err != nil { @@ -6180,6 +6282,13 @@ func (ec *executionContext) _Mutation(ctx context.Context, sel ast.SelectionSet) if out.Values[i] == graphql.Null { out.Invalids++ } + case "deleteAccount": + out.Values[i] = ec.OperationContext.RootResolverMiddleware(innerCtx, func(ctx context.Context) (res graphql.Marshaler) { + return ec._Mutation_deleteAccount(ctx, field) + }) + if out.Values[i] == graphql.Null { + out.Invalids++ + } case "triggerQuoteUpdate": out.Values[i] = ec.OperationContext.RootResolverMiddleware(innerCtx, func(ctx context.Context) (res graphql.Marshaler) { return ec._Mutation_triggerQuoteUpdate(ctx, field) diff --git a/graph/schema.graphqls b/graph/schema.graphqls index 0a08382a..7c5a9ec2 100644 --- a/graph/schema.graphqls +++ b/graph/schema.graphqls @@ -175,6 +175,7 @@ type Mutation { createPortfolio(input: PortfolioInput!): Portfolio! createAccount(input: AccountInput!): Account! + deleteAccount(id: String!): Account! """ Triggers a quote update for the given security IDs. If no security IDs are diff --git a/graph/schema.resolvers.go b/graph/schema.resolvers.go index f9ba55ee..c8ff6699 100644 --- a/graph/schema.resolvers.go +++ b/graph/schema.resolvers.go @@ -119,6 +119,11 @@ func (r *mutationResolver) CreateAccount(ctx context.Context, input models.Accou }) } +// DeleteAccount is the resolver for the deleteAccount field. +func (r *mutationResolver) DeleteAccount(ctx context.Context, id string) (*persistence.Account, error) { + return r.DB.DeleteAccount(ctx, id) +} + // TriggerQuoteUpdate is the resolver for the triggerQuoteUpdate field. func (r *mutationResolver) TriggerQuoteUpdate(ctx context.Context, securityIDs []string) (updated []*persistence.ListedSecurity, err error) { updated, err = r.QuoteUpdater.UpdateQuotes(ctx, securityIDs) diff --git a/persistence/accounts.sql.go b/persistence/accounts.sql.go index 4d9cb46c..f22574c9 100644 --- a/persistence/accounts.sql.go +++ b/persistence/accounts.sql.go @@ -82,6 +82,24 @@ func (q *Queries) CreatePortfolio(ctx context.Context, arg CreatePortfolioParams return &i, err } +const deleteAccount = `-- name: DeleteAccount :one +DELETE FROM accounts +WHERE + id = ? RETURNING id, display_name, type, reference_account_id +` + +func (q *Queries) DeleteAccount(ctx context.Context, id string) (*Account, error) { + row := q.db.QueryRowContext(ctx, deleteAccount, id) + var i Account + err := row.Scan( + &i.ID, + &i.DisplayName, + &i.Type, + &i.ReferenceAccountID, + ) + return &i, err +} + const getAccount = `-- name: GetAccount :one SELECT id, display_name, type, reference_account_id diff --git a/persistence/sql/queries/accounts.sql b/persistence/sql/queries/accounts.sql index e776b90c..95b7a37b 100644 --- a/persistence/sql/queries/accounts.sql +++ b/persistence/sql/queries/accounts.sql @@ -52,6 +52,11 @@ INSERT INTO VALUES (?, ?, ?, ?) RETURNING *; +-- name: DeleteAccount :one +DELETE FROM accounts +WHERE + id = ? RETURNING *; + -- name: CreateBankAccount :one INSERT INTO bank_accounts (id, display_name) From 448b2fa8387b91f2314498e0868ed2d147ea0517 Mon Sep 17 00:00:00 2001 From: Christian Banse Date: Mon, 6 Jan 2025 21:57:02 +0100 Subject: [PATCH 29/35] Fixed test --- internal/enum/valueof.go | 5 ++--- portfolio/accounts/type.go | 6 +++--- 2 files changed, 5 insertions(+), 6 deletions(-) diff --git a/internal/enum/valueof.go b/internal/enum/valueof.go index 8b9c88a3..d7a8bcaf 100644 --- a/internal/enum/valueof.go +++ b/internal/enum/valueof.go @@ -9,7 +9,6 @@ import ( // Enum is an interface for enum types that support [flag.Value]. type Enum interface { flag.Value - ~int } // ValueOf returns the index of the value v in the name/index slice. @@ -24,7 +23,7 @@ func ValueOf(v string, name string, index []uint8) int { } // Set sets the target enum to the value represented by v (using [ValueOf]). -func Set[T Enum](enum *T, v string, name string, index []uint8) error { +func Set[T ~int](enum *T, v string, name string, index []uint8) error { i := ValueOf(v, name, index) if i == -1 { return fmt.Errorf("unknown value: %s", v) @@ -35,7 +34,7 @@ func Set[T Enum](enum *T, v string, name string, index []uint8) error { } // MarshalJSON marshals the enum to JSON using the string representation. -func MarshalJSON[T Enum](enum T) ([]byte, error) { +func MarshalJSON[T fmt.Stringer](enum T) ([]byte, error) { return []byte(fmt.Sprintf(`"%s"`, enum.String())), nil } diff --git a/portfolio/accounts/type.go b/portfolio/accounts/type.go index a82b8487..6731ebd7 100644 --- a/portfolio/accounts/type.go +++ b/portfolio/accounts/type.go @@ -24,8 +24,8 @@ func (t *AccountType) Get() any { } // Set implements [flag.Value]. -func (t AccountType) Set(v string) error { - return enum.Set(&t, v, _AccountType_name, _AccountType_index[:]) +func (t *AccountType) Set(v string) error { + return enum.Set(t, v, _AccountType_name, _AccountType_index[:]) } // MarshalJSON marshals the account type to JSON using the string @@ -36,6 +36,6 @@ func (t AccountType) MarshalJSON() ([]byte, error) { // UnmarshalJSON unmarshals the account type from JSON. It expects a string // representation. -func (t AccountType) UnmarshalJSON(data []byte) error { +func (t *AccountType) UnmarshalJSON(data []byte) error { return enum.UnmarshalJSON(t, data) } From 94e7bfa711b748479774afbe9dd552a6d810b5c0 Mon Sep 17 00:00:00 2001 From: Christian Banse Date: Tue, 7 Jan 2025 20:11:26 +0100 Subject: [PATCH 30/35] Added transactions --- cli/commands/account.go | 37 + cli/commands/account_test.go | 21 + go.mod | 2 +- graph/generated.go | 951 ++++++++++++++++-- graph/schema.graphqls | 12 + graph/schema.resolvers.go | 30 + graph/schema.resolvers_test.go | 85 ++ internal/testdata/securities.go | 71 +- money-gopher.code-workspace | 2 + persistence/accounts.sql.go | 107 ++ persistence/models.go | 24 + .../sql/migrations/0002_create_accounts.sql | 21 +- persistence/sql/queries/accounts.sql | 28 +- sqlc.yaml | 20 + 14 files changed, 1328 insertions(+), 83 deletions(-) diff --git a/cli/commands/account.go b/cli/commands/account.go index fe4b6bd5..a1d3c2bb 100644 --- a/cli/commands/account.go +++ b/cli/commands/account.go @@ -20,6 +20,7 @@ import ( "context" "errors" "fmt" + "time" mcli "github.com/oxisto/money-gopher/cli" "github.com/oxisto/money-gopher/models" @@ -62,6 +63,20 @@ var AccountCmd = &cli.Command{ &cli.BoolFlag{Name: "confirm", Usage: "Confirm account deletion", Required: true}, }, }, + { + Name: "transactions", + Usage: "Subcommands supporting transactions within one portfolio", + Commands: []*cli.Command{ + { + Name: "list", + Usage: "Lists all transactions for an account", + Action: ListTransactions, + Flags: []cli.Flag{ + &cli.StringFlag{Name: "account-id", Usage: "The ID of the account the transaction is coming from or is destined to", Required: true}, + }, + }, + }, + }, }, } @@ -143,3 +158,25 @@ func DeleteAccount(ctx context.Context, cmd *cli.Command) (err error) { return nil } + +// ListTransactions lists all transactions for an account. +func ListTransactions(ctx context.Context, cmd *cli.Command) (err error) { + s := mcli.FromContext(ctx) + + var query struct { + Transactions []struct { + ID string `json:"id"` + Time time.Time `json:"time"` + Type string `json:"type"` + } `json:"transactions"` + } + + err = s.GraphQL.Query(context.Background(), &query, nil) + if err != nil { + return err + } + + s.WriteJSON(cmd.Writer, query) + + return nil +} diff --git a/cli/commands/account_test.go b/cli/commands/account_test.go index 130d0a9d..f1ffb800 100644 --- a/cli/commands/account_test.go +++ b/cli/commands/account_test.go @@ -157,3 +157,24 @@ func TestDeleteAccount(t *testing.T) { }) } } + +func TestListTransactions(t *testing.T) { + type args struct { + ctx context.Context + cmd *cli.Command + } + tests := []struct { + name string + args args + wantErr bool + }{ + // TODO: Add test cases. + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if err := ListTransactions(tt.args.ctx, tt.args.cmd); (err != nil) != tt.wantErr { + t.Errorf("ListTransactions() error = %v, wantErr %v", err, tt.wantErr) + } + }) + } +} diff --git a/go.mod b/go.mod index 5fc6ce07..20a39f69 100644 --- a/go.mod +++ b/go.mod @@ -5,6 +5,7 @@ go 1.23.4 require ( github.com/99designs/gqlgen v0.17.61 github.com/fatih/color v1.18.0 + github.com/google/uuid v1.6.0 github.com/lmittmann/tint v1.0.6 github.com/mattn/go-colorable v0.1.13 github.com/mattn/go-isatty v0.0.20 @@ -24,7 +25,6 @@ require ( github.com/go-viper/mapstructure/v2 v2.2.1 // indirect github.com/golang-jwt/jwt/v5 v5.2.1 // indirect github.com/google/go-cmp v0.6.0 // indirect - github.com/google/uuid v1.6.0 // indirect github.com/mfridman/interpolate v0.0.2 // indirect github.com/sethvargo/go-retry v0.3.0 // indirect github.com/sosodev/duration v1.3.1 // indirect diff --git a/graph/generated.go b/graph/generated.go index cba512cc..de6900d3 100644 --- a/graph/generated.go +++ b/graph/generated.go @@ -50,6 +50,7 @@ type ResolverRoot interface { PortfolioEvent() PortfolioEventResolver Query() QueryResolver Security() SecurityResolver + Transaction() TransactionResolver } type DirectiveRoot struct { @@ -129,12 +130,13 @@ type ComplexityRoot struct { } Query struct { - Account func(childComplexity int, id string) int - Accounts func(childComplexity int) int - Portfolio func(childComplexity int, id string) int - Portfolios func(childComplexity int) int - Securities func(childComplexity int) int - Security func(childComplexity int, id string) int + Account func(childComplexity int, id string) int + Accounts func(childComplexity int) int + Portfolio func(childComplexity int, id string) int + Portfolios func(childComplexity int) int + Securities func(childComplexity int) int + Security func(childComplexity int, id string) int + Transactions func(childComplexity int, accountID string) int } Security struct { @@ -143,6 +145,16 @@ type ComplexityRoot struct { ListedAs func(childComplexity int) int QuoteProvider func(childComplexity int) int } + + Transaction struct { + Amount func(childComplexity int) int + DestinationAccount func(childComplexity int) int + Fees func(childComplexity int) int + Price func(childComplexity int) int + Security func(childComplexity int) int + SourceAccount func(childComplexity int) int + Time func(childComplexity int) int + } } type AccountResolver interface { @@ -178,11 +190,18 @@ type QueryResolver interface { Portfolios(ctx context.Context) ([]*persistence.Portfolio, error) Account(ctx context.Context, id string) (*persistence.Account, error) Accounts(ctx context.Context) ([]*persistence.Account, error) + Transactions(ctx context.Context, accountID string) ([]*persistence.Transaction, error) } type SecurityResolver interface { QuoteProvider(ctx context.Context, obj *persistence.Security) (*string, error) ListedAs(ctx context.Context, obj *persistence.Security) ([]*persistence.ListedSecurity, error) } +type TransactionResolver interface { + Time(ctx context.Context, obj *persistence.Transaction) (string, error) + SourceAccount(ctx context.Context, obj *persistence.Transaction) (*persistence.Account, error) + DestinationAccount(ctx context.Context, obj *persistence.Transaction) (*persistence.Account, error) + Security(ctx context.Context, obj *persistence.Transaction) (*persistence.Security, error) +} type executableSchema struct { schema *ast.Schema @@ -610,6 +629,18 @@ func (e *executableSchema) Complexity(typeName, field string, childComplexity in return e.complexity.Query.Security(childComplexity, args["id"].(string)), true + case "Query.transactions": + if e.complexity.Query.Transactions == nil { + break + } + + args, err := ec.field_Query_transactions_args(context.TODO(), rawArgs) + if err != nil { + return 0, false + } + + return e.complexity.Query.Transactions(childComplexity, args["accountID"].(string)), true + case "Security.displayName": if e.complexity.Security.DisplayName == nil { break @@ -638,6 +669,55 @@ func (e *executableSchema) Complexity(typeName, field string, childComplexity in return e.complexity.Security.QuoteProvider(childComplexity), true + case "Transaction.amount": + if e.complexity.Transaction.Amount == nil { + break + } + + return e.complexity.Transaction.Amount(childComplexity), true + + case "Transaction.destinationAccount": + if e.complexity.Transaction.DestinationAccount == nil { + break + } + + return e.complexity.Transaction.DestinationAccount(childComplexity), true + + case "Transaction.fees": + if e.complexity.Transaction.Fees == nil { + break + } + + return e.complexity.Transaction.Fees(childComplexity), true + + case "Transaction.price": + if e.complexity.Transaction.Price == nil { + break + } + + return e.complexity.Transaction.Price(childComplexity), true + + case "Transaction.security": + if e.complexity.Transaction.Security == nil { + break + } + + return e.complexity.Transaction.Security(childComplexity), true + + case "Transaction.sourceAccount": + if e.complexity.Transaction.SourceAccount == nil { + break + } + + return e.complexity.Transaction.SourceAccount(childComplexity), true + + case "Transaction.time": + if e.complexity.Transaction.Time == nil { + break + } + + return e.complexity.Transaction.Time(childComplexity), true + } return 0, false } @@ -1037,6 +1117,29 @@ func (ec *executionContext) field_Query_security_argsID( return zeroVal, nil } +func (ec *executionContext) field_Query_transactions_args(ctx context.Context, rawArgs map[string]any) (map[string]any, error) { + var err error + args := map[string]any{} + arg0, err := ec.field_Query_transactions_argsAccountID(ctx, rawArgs) + if err != nil { + return nil, err + } + args["accountID"] = arg0 + return args, nil +} +func (ec *executionContext) field_Query_transactions_argsAccountID( + ctx context.Context, + rawArgs map[string]any, +) (string, error) { + ctx = graphql.WithPathContext(ctx, graphql.NewPathWithField("accountID")) + if tmp, ok := rawArgs["accountID"]; ok { + return ec.unmarshalNString2string(ctx, tmp) + } + + var zeroVal string + return zeroVal, nil +} + func (ec *executionContext) field___Type_enumValues_args(ctx context.Context, rawArgs map[string]any) (map[string]any, error) { var err error args := map[string]any{} @@ -3708,6 +3811,77 @@ func (ec *executionContext) fieldContext_Query_accounts(_ context.Context, field return fc, nil } +func (ec *executionContext) _Query_transactions(ctx context.Context, field graphql.CollectedField) (ret graphql.Marshaler) { + fc, err := ec.fieldContext_Query_transactions(ctx, field) + if err != nil { + return graphql.Null + } + ctx = graphql.WithFieldContext(ctx, fc) + defer func() { + if r := recover(); r != nil { + ec.Error(ctx, ec.Recover(ctx, r)) + ret = graphql.Null + } + }() + resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (any, error) { + ctx = rctx // use context from middleware stack in children + return ec.resolvers.Query().Transactions(rctx, fc.Args["accountID"].(string)) + }) + if err != nil { + ec.Error(ctx, err) + return graphql.Null + } + if resTmp == nil { + if !graphql.HasFieldError(ctx, fc) { + ec.Errorf(ctx, "must not be null") + } + return graphql.Null + } + res := resTmp.([]*persistence.Transaction) + fc.Result = res + return ec.marshalNTransaction2ᚕᚖgithubᚗcomᚋoxistoᚋmoneyᚑgopherᚋpersistenceᚐTransactionᚄ(ctx, field.Selections, res) +} + +func (ec *executionContext) fieldContext_Query_transactions(ctx context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { + fc = &graphql.FieldContext{ + Object: "Query", + Field: field, + IsMethod: true, + IsResolver: true, + Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + switch field.Name { + case "time": + return ec.fieldContext_Transaction_time(ctx, field) + case "sourceAccount": + return ec.fieldContext_Transaction_sourceAccount(ctx, field) + case "destinationAccount": + return ec.fieldContext_Transaction_destinationAccount(ctx, field) + case "security": + return ec.fieldContext_Transaction_security(ctx, field) + case "amount": + return ec.fieldContext_Transaction_amount(ctx, field) + case "price": + return ec.fieldContext_Transaction_price(ctx, field) + case "fees": + return ec.fieldContext_Transaction_fees(ctx, field) + } + return nil, fmt.Errorf("no field named %q was found under type Transaction", field.Name) + }, + } + defer func() { + if r := recover(); r != nil { + err = ec.Recover(ctx, r) + ec.Error(ctx, err) + } + }() + ctx = graphql.WithFieldContext(ctx, fc) + if fc.Args, err = ec.field_Query_transactions_args(ctx, field.ArgumentMap(ec.Variables)); err != nil { + ec.Error(ctx, err) + return fc, err + } + return fc, nil +} + func (ec *executionContext) _Query___type(ctx context.Context, field graphql.CollectedField) (ret graphql.Marshaler) { fc, err := ec.fieldContext_Query___type(ctx, field) if err != nil { @@ -4019,8 +4193,8 @@ func (ec *executionContext) fieldContext_Security_listedAs(_ context.Context, fi return fc, nil } -func (ec *executionContext) ___Directive_name(ctx context.Context, field graphql.CollectedField, obj *introspection.Directive) (ret graphql.Marshaler) { - fc, err := ec.fieldContext___Directive_name(ctx, field) +func (ec *executionContext) _Transaction_time(ctx context.Context, field graphql.CollectedField, obj *persistence.Transaction) (ret graphql.Marshaler) { + fc, err := ec.fieldContext_Transaction_time(ctx, field) if err != nil { return graphql.Null } @@ -4033,7 +4207,7 @@ func (ec *executionContext) ___Directive_name(ctx context.Context, field graphql }() resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (any, error) { ctx = rctx // use context from middleware stack in children - return obj.Name, nil + return ec.resolvers.Transaction().Time(rctx, obj) }) if err != nil { ec.Error(ctx, err) @@ -4047,24 +4221,24 @@ func (ec *executionContext) ___Directive_name(ctx context.Context, field graphql } res := resTmp.(string) fc.Result = res - return ec.marshalNString2string(ctx, field.Selections, res) + return ec.marshalNDate2string(ctx, field.Selections, res) } -func (ec *executionContext) fieldContext___Directive_name(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { +func (ec *executionContext) fieldContext_Transaction_time(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { fc = &graphql.FieldContext{ - Object: "__Directive", + Object: "Transaction", Field: field, - IsMethod: false, - IsResolver: false, + IsMethod: true, + IsResolver: true, Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { - return nil, errors.New("field of type String does not have child fields") + return nil, errors.New("field of type Date does not have child fields") }, } return fc, nil } -func (ec *executionContext) ___Directive_description(ctx context.Context, field graphql.CollectedField, obj *introspection.Directive) (ret graphql.Marshaler) { - fc, err := ec.fieldContext___Directive_description(ctx, field) +func (ec *executionContext) _Transaction_sourceAccount(ctx context.Context, field graphql.CollectedField, obj *persistence.Transaction) (ret graphql.Marshaler) { + fc, err := ec.fieldContext_Transaction_sourceAccount(ctx, field) if err != nil { return graphql.Null } @@ -4077,35 +4251,48 @@ func (ec *executionContext) ___Directive_description(ctx context.Context, field }() resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (any, error) { ctx = rctx // use context from middleware stack in children - return obj.Description(), nil + return ec.resolvers.Transaction().SourceAccount(rctx, obj) }) if err != nil { ec.Error(ctx, err) return graphql.Null } if resTmp == nil { + if !graphql.HasFieldError(ctx, fc) { + ec.Errorf(ctx, "must not be null") + } return graphql.Null } - res := resTmp.(*string) + res := resTmp.(*persistence.Account) fc.Result = res - return ec.marshalOString2ᚖstring(ctx, field.Selections, res) + return ec.marshalNAccount2ᚖgithubᚗcomᚋoxistoᚋmoneyᚑgopherᚋpersistenceᚐAccount(ctx, field.Selections, res) } -func (ec *executionContext) fieldContext___Directive_description(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { +func (ec *executionContext) fieldContext_Transaction_sourceAccount(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { fc = &graphql.FieldContext{ - Object: "__Directive", + Object: "Transaction", Field: field, IsMethod: true, - IsResolver: false, + IsResolver: true, Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { - return nil, errors.New("field of type String does not have child fields") + switch field.Name { + case "id": + return ec.fieldContext_Account_id(ctx, field) + case "displayName": + return ec.fieldContext_Account_displayName(ctx, field) + case "type": + return ec.fieldContext_Account_type(ctx, field) + case "referenceAccount": + return ec.fieldContext_Account_referenceAccount(ctx, field) + } + return nil, fmt.Errorf("no field named %q was found under type Account", field.Name) }, } return fc, nil } -func (ec *executionContext) ___Directive_locations(ctx context.Context, field graphql.CollectedField, obj *introspection.Directive) (ret graphql.Marshaler) { - fc, err := ec.fieldContext___Directive_locations(ctx, field) +func (ec *executionContext) _Transaction_destinationAccount(ctx context.Context, field graphql.CollectedField, obj *persistence.Transaction) (ret graphql.Marshaler) { + fc, err := ec.fieldContext_Transaction_destinationAccount(ctx, field) if err != nil { return graphql.Null } @@ -4118,7 +4305,7 @@ func (ec *executionContext) ___Directive_locations(ctx context.Context, field gr }() resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (any, error) { ctx = rctx // use context from middleware stack in children - return obj.Locations, nil + return ec.resolvers.Transaction().DestinationAccount(rctx, obj) }) if err != nil { ec.Error(ctx, err) @@ -4130,26 +4317,36 @@ func (ec *executionContext) ___Directive_locations(ctx context.Context, field gr } return graphql.Null } - res := resTmp.([]string) + res := resTmp.(*persistence.Account) fc.Result = res - return ec.marshalN__DirectiveLocation2ᚕstringᚄ(ctx, field.Selections, res) + return ec.marshalNAccount2ᚖgithubᚗcomᚋoxistoᚋmoneyᚑgopherᚋpersistenceᚐAccount(ctx, field.Selections, res) } -func (ec *executionContext) fieldContext___Directive_locations(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { +func (ec *executionContext) fieldContext_Transaction_destinationAccount(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { fc = &graphql.FieldContext{ - Object: "__Directive", + Object: "Transaction", Field: field, - IsMethod: false, - IsResolver: false, + IsMethod: true, + IsResolver: true, Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { - return nil, errors.New("field of type __DirectiveLocation does not have child fields") + switch field.Name { + case "id": + return ec.fieldContext_Account_id(ctx, field) + case "displayName": + return ec.fieldContext_Account_displayName(ctx, field) + case "type": + return ec.fieldContext_Account_type(ctx, field) + case "referenceAccount": + return ec.fieldContext_Account_referenceAccount(ctx, field) + } + return nil, fmt.Errorf("no field named %q was found under type Account", field.Name) }, } return fc, nil } -func (ec *executionContext) ___Directive_args(ctx context.Context, field graphql.CollectedField, obj *introspection.Directive) (ret graphql.Marshaler) { - fc, err := ec.fieldContext___Directive_args(ctx, field) +func (ec *executionContext) _Transaction_security(ctx context.Context, field graphql.CollectedField, obj *persistence.Transaction) (ret graphql.Marshaler) { + fc, err := ec.fieldContext_Transaction_security(ctx, field) if err != nil { return graphql.Null } @@ -4162,7 +4359,7 @@ func (ec *executionContext) ___Directive_args(ctx context.Context, field graphql }() resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (any, error) { ctx = rctx // use context from middleware stack in children - return obj.Args, nil + return ec.resolvers.Transaction().Security(rctx, obj) }) if err != nil { ec.Error(ctx, err) @@ -4174,36 +4371,36 @@ func (ec *executionContext) ___Directive_args(ctx context.Context, field graphql } return graphql.Null } - res := resTmp.([]introspection.InputValue) + res := resTmp.(*persistence.Security) fc.Result = res - return ec.marshalN__InputValue2ᚕgithubᚗcomᚋ99designsᚋgqlgenᚋgraphqlᚋintrospectionᚐInputValueᚄ(ctx, field.Selections, res) + return ec.marshalNSecurity2ᚖgithubᚗcomᚋoxistoᚋmoneyᚑgopherᚋpersistenceᚐSecurity(ctx, field.Selections, res) } -func (ec *executionContext) fieldContext___Directive_args(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { +func (ec *executionContext) fieldContext_Transaction_security(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { fc = &graphql.FieldContext{ - Object: "__Directive", + Object: "Transaction", Field: field, - IsMethod: false, - IsResolver: false, + IsMethod: true, + IsResolver: true, Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { switch field.Name { - case "name": - return ec.fieldContext___InputValue_name(ctx, field) - case "description": - return ec.fieldContext___InputValue_description(ctx, field) - case "type": - return ec.fieldContext___InputValue_type(ctx, field) - case "defaultValue": - return ec.fieldContext___InputValue_defaultValue(ctx, field) + case "id": + return ec.fieldContext_Security_id(ctx, field) + case "displayName": + return ec.fieldContext_Security_displayName(ctx, field) + case "quoteProvider": + return ec.fieldContext_Security_quoteProvider(ctx, field) + case "listedAs": + return ec.fieldContext_Security_listedAs(ctx, field) } - return nil, fmt.Errorf("no field named %q was found under type __InputValue", field.Name) + return nil, fmt.Errorf("no field named %q was found under type Security", field.Name) }, } return fc, nil } -func (ec *executionContext) ___Directive_isRepeatable(ctx context.Context, field graphql.CollectedField, obj *introspection.Directive) (ret graphql.Marshaler) { - fc, err := ec.fieldContext___Directive_isRepeatable(ctx, field) +func (ec *executionContext) _Transaction_amount(ctx context.Context, field graphql.CollectedField, obj *persistence.Transaction) (ret graphql.Marshaler) { + fc, err := ec.fieldContext_Transaction_amount(ctx, field) if err != nil { return graphql.Null } @@ -4216,7 +4413,7 @@ func (ec *executionContext) ___Directive_isRepeatable(ctx context.Context, field }() resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (any, error) { ctx = rctx // use context from middleware stack in children - return obj.IsRepeatable, nil + return obj.Amount, nil }) if err != nil { ec.Error(ctx, err) @@ -4228,26 +4425,26 @@ func (ec *executionContext) ___Directive_isRepeatable(ctx context.Context, field } return graphql.Null } - res := resTmp.(bool) + res := resTmp.(float64) fc.Result = res - return ec.marshalNBoolean2bool(ctx, field.Selections, res) + return ec.marshalNFloat2float64(ctx, field.Selections, res) } -func (ec *executionContext) fieldContext___Directive_isRepeatable(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { +func (ec *executionContext) fieldContext_Transaction_amount(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { fc = &graphql.FieldContext{ - Object: "__Directive", + Object: "Transaction", Field: field, IsMethod: false, IsResolver: false, Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { - return nil, errors.New("field of type Boolean does not have child fields") + return nil, errors.New("field of type Float does not have child fields") }, } return fc, nil } -func (ec *executionContext) ___EnumValue_name(ctx context.Context, field graphql.CollectedField, obj *introspection.EnumValue) (ret graphql.Marshaler) { - fc, err := ec.fieldContext___EnumValue_name(ctx, field) +func (ec *executionContext) _Transaction_price(ctx context.Context, field graphql.CollectedField, obj *persistence.Transaction) (ret graphql.Marshaler) { + fc, err := ec.fieldContext_Transaction_price(ctx, field) if err != nil { return graphql.Null } @@ -4260,7 +4457,7 @@ func (ec *executionContext) ___EnumValue_name(ctx context.Context, field graphql }() resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (any, error) { ctx = rctx // use context from middleware stack in children - return obj.Name, nil + return obj.Price, nil }) if err != nil { ec.Error(ctx, err) @@ -4272,26 +4469,32 @@ func (ec *executionContext) ___EnumValue_name(ctx context.Context, field graphql } return graphql.Null } - res := resTmp.(string) + res := resTmp.(*currency.Currency) fc.Result = res - return ec.marshalNString2string(ctx, field.Selections, res) + return ec.marshalNCurrency2ᚖgithubᚗcomᚋoxistoᚋmoneyᚑgopherᚋcurrencyᚐCurrency(ctx, field.Selections, res) } -func (ec *executionContext) fieldContext___EnumValue_name(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { +func (ec *executionContext) fieldContext_Transaction_price(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { fc = &graphql.FieldContext{ - Object: "__EnumValue", + Object: "Transaction", Field: field, IsMethod: false, IsResolver: false, Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { - return nil, errors.New("field of type String does not have child fields") + switch field.Name { + case "amount": + return ec.fieldContext_Currency_amount(ctx, field) + case "symbol": + return ec.fieldContext_Currency_symbol(ctx, field) + } + return nil, fmt.Errorf("no field named %q was found under type Currency", field.Name) }, } return fc, nil } -func (ec *executionContext) ___EnumValue_description(ctx context.Context, field graphql.CollectedField, obj *introspection.EnumValue) (ret graphql.Marshaler) { - fc, err := ec.fieldContext___EnumValue_description(ctx, field) +func (ec *executionContext) _Transaction_fees(ctx context.Context, field graphql.CollectedField, obj *persistence.Transaction) (ret graphql.Marshaler) { + fc, err := ec.fieldContext_Transaction_fees(ctx, field) if err != nil { return graphql.Null } @@ -4304,35 +4507,356 @@ func (ec *executionContext) ___EnumValue_description(ctx context.Context, field }() resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (any, error) { ctx = rctx // use context from middleware stack in children - return obj.Description(), nil + return obj.Fees, nil }) if err != nil { ec.Error(ctx, err) return graphql.Null } if resTmp == nil { + if !graphql.HasFieldError(ctx, fc) { + ec.Errorf(ctx, "must not be null") + } return graphql.Null } - res := resTmp.(*string) + res := resTmp.(*currency.Currency) fc.Result = res - return ec.marshalOString2ᚖstring(ctx, field.Selections, res) + return ec.marshalNCurrency2ᚖgithubᚗcomᚋoxistoᚋmoneyᚑgopherᚋcurrencyᚐCurrency(ctx, field.Selections, res) } -func (ec *executionContext) fieldContext___EnumValue_description(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { +func (ec *executionContext) fieldContext_Transaction_fees(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { fc = &graphql.FieldContext{ - Object: "__EnumValue", + Object: "Transaction", Field: field, - IsMethod: true, + IsMethod: false, IsResolver: false, Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { - return nil, errors.New("field of type String does not have child fields") + switch field.Name { + case "amount": + return ec.fieldContext_Currency_amount(ctx, field) + case "symbol": + return ec.fieldContext_Currency_symbol(ctx, field) + } + return nil, fmt.Errorf("no field named %q was found under type Currency", field.Name) }, } return fc, nil } -func (ec *executionContext) ___EnumValue_isDeprecated(ctx context.Context, field graphql.CollectedField, obj *introspection.EnumValue) (ret graphql.Marshaler) { - fc, err := ec.fieldContext___EnumValue_isDeprecated(ctx, field) +func (ec *executionContext) ___Directive_name(ctx context.Context, field graphql.CollectedField, obj *introspection.Directive) (ret graphql.Marshaler) { + fc, err := ec.fieldContext___Directive_name(ctx, field) + if err != nil { + return graphql.Null + } + ctx = graphql.WithFieldContext(ctx, fc) + defer func() { + if r := recover(); r != nil { + ec.Error(ctx, ec.Recover(ctx, r)) + ret = graphql.Null + } + }() + resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (any, error) { + ctx = rctx // use context from middleware stack in children + return obj.Name, nil + }) + if err != nil { + ec.Error(ctx, err) + return graphql.Null + } + if resTmp == nil { + if !graphql.HasFieldError(ctx, fc) { + ec.Errorf(ctx, "must not be null") + } + return graphql.Null + } + res := resTmp.(string) + fc.Result = res + return ec.marshalNString2string(ctx, field.Selections, res) +} + +func (ec *executionContext) fieldContext___Directive_name(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { + fc = &graphql.FieldContext{ + Object: "__Directive", + Field: field, + IsMethod: false, + IsResolver: false, + Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + return nil, errors.New("field of type String does not have child fields") + }, + } + return fc, nil +} + +func (ec *executionContext) ___Directive_description(ctx context.Context, field graphql.CollectedField, obj *introspection.Directive) (ret graphql.Marshaler) { + fc, err := ec.fieldContext___Directive_description(ctx, field) + if err != nil { + return graphql.Null + } + ctx = graphql.WithFieldContext(ctx, fc) + defer func() { + if r := recover(); r != nil { + ec.Error(ctx, ec.Recover(ctx, r)) + ret = graphql.Null + } + }() + resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (any, error) { + ctx = rctx // use context from middleware stack in children + return obj.Description(), nil + }) + if err != nil { + ec.Error(ctx, err) + return graphql.Null + } + if resTmp == nil { + return graphql.Null + } + res := resTmp.(*string) + fc.Result = res + return ec.marshalOString2ᚖstring(ctx, field.Selections, res) +} + +func (ec *executionContext) fieldContext___Directive_description(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { + fc = &graphql.FieldContext{ + Object: "__Directive", + Field: field, + IsMethod: true, + IsResolver: false, + Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + return nil, errors.New("field of type String does not have child fields") + }, + } + return fc, nil +} + +func (ec *executionContext) ___Directive_locations(ctx context.Context, field graphql.CollectedField, obj *introspection.Directive) (ret graphql.Marshaler) { + fc, err := ec.fieldContext___Directive_locations(ctx, field) + if err != nil { + return graphql.Null + } + ctx = graphql.WithFieldContext(ctx, fc) + defer func() { + if r := recover(); r != nil { + ec.Error(ctx, ec.Recover(ctx, r)) + ret = graphql.Null + } + }() + resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (any, error) { + ctx = rctx // use context from middleware stack in children + return obj.Locations, nil + }) + if err != nil { + ec.Error(ctx, err) + return graphql.Null + } + if resTmp == nil { + if !graphql.HasFieldError(ctx, fc) { + ec.Errorf(ctx, "must not be null") + } + return graphql.Null + } + res := resTmp.([]string) + fc.Result = res + return ec.marshalN__DirectiveLocation2ᚕstringᚄ(ctx, field.Selections, res) +} + +func (ec *executionContext) fieldContext___Directive_locations(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { + fc = &graphql.FieldContext{ + Object: "__Directive", + Field: field, + IsMethod: false, + IsResolver: false, + Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + return nil, errors.New("field of type __DirectiveLocation does not have child fields") + }, + } + return fc, nil +} + +func (ec *executionContext) ___Directive_args(ctx context.Context, field graphql.CollectedField, obj *introspection.Directive) (ret graphql.Marshaler) { + fc, err := ec.fieldContext___Directive_args(ctx, field) + if err != nil { + return graphql.Null + } + ctx = graphql.WithFieldContext(ctx, fc) + defer func() { + if r := recover(); r != nil { + ec.Error(ctx, ec.Recover(ctx, r)) + ret = graphql.Null + } + }() + resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (any, error) { + ctx = rctx // use context from middleware stack in children + return obj.Args, nil + }) + if err != nil { + ec.Error(ctx, err) + return graphql.Null + } + if resTmp == nil { + if !graphql.HasFieldError(ctx, fc) { + ec.Errorf(ctx, "must not be null") + } + return graphql.Null + } + res := resTmp.([]introspection.InputValue) + fc.Result = res + return ec.marshalN__InputValue2ᚕgithubᚗcomᚋ99designsᚋgqlgenᚋgraphqlᚋintrospectionᚐInputValueᚄ(ctx, field.Selections, res) +} + +func (ec *executionContext) fieldContext___Directive_args(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { + fc = &graphql.FieldContext{ + Object: "__Directive", + Field: field, + IsMethod: false, + IsResolver: false, + Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + switch field.Name { + case "name": + return ec.fieldContext___InputValue_name(ctx, field) + case "description": + return ec.fieldContext___InputValue_description(ctx, field) + case "type": + return ec.fieldContext___InputValue_type(ctx, field) + case "defaultValue": + return ec.fieldContext___InputValue_defaultValue(ctx, field) + } + return nil, fmt.Errorf("no field named %q was found under type __InputValue", field.Name) + }, + } + return fc, nil +} + +func (ec *executionContext) ___Directive_isRepeatable(ctx context.Context, field graphql.CollectedField, obj *introspection.Directive) (ret graphql.Marshaler) { + fc, err := ec.fieldContext___Directive_isRepeatable(ctx, field) + if err != nil { + return graphql.Null + } + ctx = graphql.WithFieldContext(ctx, fc) + defer func() { + if r := recover(); r != nil { + ec.Error(ctx, ec.Recover(ctx, r)) + ret = graphql.Null + } + }() + resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (any, error) { + ctx = rctx // use context from middleware stack in children + return obj.IsRepeatable, nil + }) + if err != nil { + ec.Error(ctx, err) + return graphql.Null + } + if resTmp == nil { + if !graphql.HasFieldError(ctx, fc) { + ec.Errorf(ctx, "must not be null") + } + return graphql.Null + } + res := resTmp.(bool) + fc.Result = res + return ec.marshalNBoolean2bool(ctx, field.Selections, res) +} + +func (ec *executionContext) fieldContext___Directive_isRepeatable(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { + fc = &graphql.FieldContext{ + Object: "__Directive", + Field: field, + IsMethod: false, + IsResolver: false, + Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + return nil, errors.New("field of type Boolean does not have child fields") + }, + } + return fc, nil +} + +func (ec *executionContext) ___EnumValue_name(ctx context.Context, field graphql.CollectedField, obj *introspection.EnumValue) (ret graphql.Marshaler) { + fc, err := ec.fieldContext___EnumValue_name(ctx, field) + if err != nil { + return graphql.Null + } + ctx = graphql.WithFieldContext(ctx, fc) + defer func() { + if r := recover(); r != nil { + ec.Error(ctx, ec.Recover(ctx, r)) + ret = graphql.Null + } + }() + resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (any, error) { + ctx = rctx // use context from middleware stack in children + return obj.Name, nil + }) + if err != nil { + ec.Error(ctx, err) + return graphql.Null + } + if resTmp == nil { + if !graphql.HasFieldError(ctx, fc) { + ec.Errorf(ctx, "must not be null") + } + return graphql.Null + } + res := resTmp.(string) + fc.Result = res + return ec.marshalNString2string(ctx, field.Selections, res) +} + +func (ec *executionContext) fieldContext___EnumValue_name(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { + fc = &graphql.FieldContext{ + Object: "__EnumValue", + Field: field, + IsMethod: false, + IsResolver: false, + Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + return nil, errors.New("field of type String does not have child fields") + }, + } + return fc, nil +} + +func (ec *executionContext) ___EnumValue_description(ctx context.Context, field graphql.CollectedField, obj *introspection.EnumValue) (ret graphql.Marshaler) { + fc, err := ec.fieldContext___EnumValue_description(ctx, field) + if err != nil { + return graphql.Null + } + ctx = graphql.WithFieldContext(ctx, fc) + defer func() { + if r := recover(); r != nil { + ec.Error(ctx, ec.Recover(ctx, r)) + ret = graphql.Null + } + }() + resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (any, error) { + ctx = rctx // use context from middleware stack in children + return obj.Description(), nil + }) + if err != nil { + ec.Error(ctx, err) + return graphql.Null + } + if resTmp == nil { + return graphql.Null + } + res := resTmp.(*string) + fc.Result = res + return ec.marshalOString2ᚖstring(ctx, field.Selections, res) +} + +func (ec *executionContext) fieldContext___EnumValue_description(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { + fc = &graphql.FieldContext{ + Object: "__EnumValue", + Field: field, + IsMethod: true, + IsResolver: false, + Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + return nil, errors.New("field of type String does not have child fields") + }, + } + return fc, nil +} + +func (ec *executionContext) ___EnumValue_isDeprecated(ctx context.Context, field graphql.CollectedField, obj *introspection.EnumValue) (ret graphql.Marshaler) { + fc, err := ec.fieldContext___EnumValue_isDeprecated(ctx, field) if err != nil { return graphql.Null } @@ -6872,6 +7396,28 @@ func (ec *executionContext) _Query(ctx context.Context, sel ast.SelectionSet) gr func(ctx context.Context) graphql.Marshaler { return innerFunc(ctx, out) }) } + out.Concurrently(i, func(ctx context.Context) graphql.Marshaler { return rrm(innerCtx) }) + case "transactions": + field := field + + innerFunc := func(ctx context.Context, fs *graphql.FieldSet) (res graphql.Marshaler) { + defer func() { + if r := recover(); r != nil { + ec.Error(ctx, ec.Recover(ctx, r)) + } + }() + res = ec._Query_transactions(ctx, field) + if res == graphql.Null { + atomic.AddUint32(&fs.Invalids, 1) + } + return res + } + + rrm := func(ctx context.Context) graphql.Marshaler { + return ec.OperationContext.RootResolverMiddleware(ctx, + func(ctx context.Context) graphql.Marshaler { return innerFunc(ctx, out) }) + } + out.Concurrently(i, func(ctx context.Context) graphql.Marshaler { return rrm(innerCtx) }) case "__type": out.Values[i] = ec.OperationContext.RootResolverMiddleware(innerCtx, func(ctx context.Context) (res graphql.Marshaler) { @@ -7014,6 +7560,199 @@ func (ec *executionContext) _Security(ctx context.Context, sel ast.SelectionSet, return out } +var transactionImplementors = []string{"Transaction"} + +func (ec *executionContext) _Transaction(ctx context.Context, sel ast.SelectionSet, obj *persistence.Transaction) graphql.Marshaler { + fields := graphql.CollectFields(ec.OperationContext, sel, transactionImplementors) + + out := graphql.NewFieldSet(fields) + deferred := make(map[string]*graphql.FieldSet) + for i, field := range fields { + switch field.Name { + case "__typename": + out.Values[i] = graphql.MarshalString("Transaction") + case "time": + field := field + + innerFunc := func(ctx context.Context, fs *graphql.FieldSet) (res graphql.Marshaler) { + defer func() { + if r := recover(); r != nil { + ec.Error(ctx, ec.Recover(ctx, r)) + } + }() + res = ec._Transaction_time(ctx, field, obj) + if res == graphql.Null { + atomic.AddUint32(&fs.Invalids, 1) + } + return res + } + + if field.Deferrable != nil { + dfs, ok := deferred[field.Deferrable.Label] + di := 0 + if ok { + dfs.AddField(field) + di = len(dfs.Values) - 1 + } else { + dfs = graphql.NewFieldSet([]graphql.CollectedField{field}) + deferred[field.Deferrable.Label] = dfs + } + dfs.Concurrently(di, func(ctx context.Context) graphql.Marshaler { + return innerFunc(ctx, dfs) + }) + + // don't run the out.Concurrently() call below + out.Values[i] = graphql.Null + continue + } + + out.Concurrently(i, func(ctx context.Context) graphql.Marshaler { return innerFunc(ctx, out) }) + case "sourceAccount": + field := field + + innerFunc := func(ctx context.Context, fs *graphql.FieldSet) (res graphql.Marshaler) { + defer func() { + if r := recover(); r != nil { + ec.Error(ctx, ec.Recover(ctx, r)) + } + }() + res = ec._Transaction_sourceAccount(ctx, field, obj) + if res == graphql.Null { + atomic.AddUint32(&fs.Invalids, 1) + } + return res + } + + if field.Deferrable != nil { + dfs, ok := deferred[field.Deferrable.Label] + di := 0 + if ok { + dfs.AddField(field) + di = len(dfs.Values) - 1 + } else { + dfs = graphql.NewFieldSet([]graphql.CollectedField{field}) + deferred[field.Deferrable.Label] = dfs + } + dfs.Concurrently(di, func(ctx context.Context) graphql.Marshaler { + return innerFunc(ctx, dfs) + }) + + // don't run the out.Concurrently() call below + out.Values[i] = graphql.Null + continue + } + + out.Concurrently(i, func(ctx context.Context) graphql.Marshaler { return innerFunc(ctx, out) }) + case "destinationAccount": + field := field + + innerFunc := func(ctx context.Context, fs *graphql.FieldSet) (res graphql.Marshaler) { + defer func() { + if r := recover(); r != nil { + ec.Error(ctx, ec.Recover(ctx, r)) + } + }() + res = ec._Transaction_destinationAccount(ctx, field, obj) + if res == graphql.Null { + atomic.AddUint32(&fs.Invalids, 1) + } + return res + } + + if field.Deferrable != nil { + dfs, ok := deferred[field.Deferrable.Label] + di := 0 + if ok { + dfs.AddField(field) + di = len(dfs.Values) - 1 + } else { + dfs = graphql.NewFieldSet([]graphql.CollectedField{field}) + deferred[field.Deferrable.Label] = dfs + } + dfs.Concurrently(di, func(ctx context.Context) graphql.Marshaler { + return innerFunc(ctx, dfs) + }) + + // don't run the out.Concurrently() call below + out.Values[i] = graphql.Null + continue + } + + out.Concurrently(i, func(ctx context.Context) graphql.Marshaler { return innerFunc(ctx, out) }) + case "security": + field := field + + innerFunc := func(ctx context.Context, fs *graphql.FieldSet) (res graphql.Marshaler) { + defer func() { + if r := recover(); r != nil { + ec.Error(ctx, ec.Recover(ctx, r)) + } + }() + res = ec._Transaction_security(ctx, field, obj) + if res == graphql.Null { + atomic.AddUint32(&fs.Invalids, 1) + } + return res + } + + if field.Deferrable != nil { + dfs, ok := deferred[field.Deferrable.Label] + di := 0 + if ok { + dfs.AddField(field) + di = len(dfs.Values) - 1 + } else { + dfs = graphql.NewFieldSet([]graphql.CollectedField{field}) + deferred[field.Deferrable.Label] = dfs + } + dfs.Concurrently(di, func(ctx context.Context) graphql.Marshaler { + return innerFunc(ctx, dfs) + }) + + // don't run the out.Concurrently() call below + out.Values[i] = graphql.Null + continue + } + + out.Concurrently(i, func(ctx context.Context) graphql.Marshaler { return innerFunc(ctx, out) }) + case "amount": + out.Values[i] = ec._Transaction_amount(ctx, field, obj) + if out.Values[i] == graphql.Null { + atomic.AddUint32(&out.Invalids, 1) + } + case "price": + out.Values[i] = ec._Transaction_price(ctx, field, obj) + if out.Values[i] == graphql.Null { + atomic.AddUint32(&out.Invalids, 1) + } + case "fees": + out.Values[i] = ec._Transaction_fees(ctx, field, obj) + if out.Values[i] == graphql.Null { + atomic.AddUint32(&out.Invalids, 1) + } + default: + panic("unknown field " + strconv.Quote(field.Name)) + } + } + out.Dispatch(ctx) + if out.Invalids > 0 { + return graphql.Null + } + + atomic.AddInt32(&ec.deferred, int32(len(deferred))) + + for label, dfs := range deferred { + ec.processDeferredGroup(graphql.DeferredGroup{ + Label: label, + Path: graphql.GetPath(ctx), + FieldSet: dfs, + Context: ctx, + }) + } + + return out +} + var __DirectiveImplementors = []string{"__Directive"} func (ec *executionContext) ___Directive(ctx context.Context, sel ast.SelectionSet, obj *introspection.Directive) graphql.Marshaler { @@ -7862,6 +8601,60 @@ func (ec *executionContext) marshalNString2string(ctx context.Context, sel ast.S return res } +func (ec *executionContext) marshalNTransaction2ᚕᚖgithubᚗcomᚋoxistoᚋmoneyᚑgopherᚋpersistenceᚐTransactionᚄ(ctx context.Context, sel ast.SelectionSet, v []*persistence.Transaction) graphql.Marshaler { + ret := make(graphql.Array, len(v)) + var wg sync.WaitGroup + isLen1 := len(v) == 1 + if !isLen1 { + wg.Add(len(v)) + } + for i := range v { + i := i + fc := &graphql.FieldContext{ + Index: &i, + Result: &v[i], + } + ctx := graphql.WithFieldContext(ctx, fc) + f := func(i int) { + defer func() { + if r := recover(); r != nil { + ec.Error(ctx, ec.Recover(ctx, r)) + ret = nil + } + }() + if !isLen1 { + defer wg.Done() + } + ret[i] = ec.marshalNTransaction2ᚖgithubᚗcomᚋoxistoᚋmoneyᚑgopherᚋpersistenceᚐTransaction(ctx, sel, v[i]) + } + if isLen1 { + f(i) + } else { + go f(i) + } + + } + wg.Wait() + + for _, e := range ret { + if e == graphql.Null { + return graphql.Null + } + } + + return ret +} + +func (ec *executionContext) marshalNTransaction2ᚖgithubᚗcomᚋoxistoᚋmoneyᚑgopherᚋpersistenceᚐTransaction(ctx context.Context, sel ast.SelectionSet, v *persistence.Transaction) graphql.Marshaler { + if v == nil { + if !graphql.HasFieldError(ctx, graphql.GetFieldContext(ctx)) { + ec.Errorf(ctx, "the requested element is null which the schema does not allow") + } + return graphql.Null + } + return ec._Transaction(ctx, sel, v) +} + func (ec *executionContext) marshalN__Directive2githubᚗcomᚋ99designsᚋgqlgenᚋgraphqlᚋintrospectionᚐDirective(ctx context.Context, sel ast.SelectionSet, v introspection.Directive) graphql.Marshaler { return ec.___Directive(ctx, sel, &v) } diff --git a/graph/schema.graphqls b/graph/schema.graphqls index 7c5a9ec2..e9b2d61e 100644 --- a/graph/schema.graphqls +++ b/graph/schema.graphqls @@ -77,6 +77,16 @@ type PortfolioEvent { security: Security } +type Transaction { + time: Date! + sourceAccount: Account! + destinationAccount: Account! + security: Security! + amount: Float! + price: Currency! + fees: Currency! +} + type PortfolioSnapshot { time: Date! positions: [PortfolioPosition!]! @@ -193,4 +203,6 @@ type Query { account(id: String!): Account accounts: [Account!]! + + transactions(accountID: String!): [Transaction!]! } diff --git a/graph/schema.resolvers.go b/graph/schema.resolvers.go index c8ff6699..7ee58973 100644 --- a/graph/schema.resolvers.go +++ b/graph/schema.resolvers.go @@ -6,6 +6,7 @@ package graph import ( "context" + "database/sql" "fmt" "slices" "time" @@ -200,6 +201,11 @@ func (r *queryResolver) Accounts(ctx context.Context) ([]*persistence.Account, e return r.DB.ListAccounts(ctx) } +// Transactions is the resolver for the transactions field. +func (r *queryResolver) Transactions(ctx context.Context, accountID string) ([]*persistence.Transaction, error) { + return r.DB.ListTransactionsByAccountID(ctx, sql.NullString{String: accountID, Valid: true}) +} + // QuoteProvider is the resolver for the quoteProvider field. func (r *securityResolver) QuoteProvider(ctx context.Context, obj *persistence.Security) (*string, error) { if obj.QuoteProvider.Valid { @@ -214,6 +220,26 @@ func (r *securityResolver) ListedAs(ctx context.Context, obj *persistence.Securi return obj.ListedAs(ctx, r.DB) } +// Time is the resolver for the time field. +func (r *transactionResolver) Time(ctx context.Context, obj *persistence.Transaction) (string, error) { + panic(fmt.Errorf("not implemented: Time - time")) +} + +// SourceAccount is the resolver for the sourceAccount field. +func (r *transactionResolver) SourceAccount(ctx context.Context, obj *persistence.Transaction) (*persistence.Account, error) { + panic(fmt.Errorf("not implemented: SourceAccount - sourceAccount")) +} + +// DestinationAccount is the resolver for the destinationAccount field. +func (r *transactionResolver) DestinationAccount(ctx context.Context, obj *persistence.Transaction) (*persistence.Account, error) { + panic(fmt.Errorf("not implemented: DestinationAccount - destinationAccount")) +} + +// Security is the resolver for the security field. +func (r *transactionResolver) Security(ctx context.Context, obj *persistence.Transaction) (*persistence.Security, error) { + panic(fmt.Errorf("not implemented: Security - security")) +} + // Account returns AccountResolver implementation. func (r *Resolver) Account() AccountResolver { return &accountResolver{r} } @@ -235,6 +261,9 @@ func (r *Resolver) Query() QueryResolver { return &queryResolver{r} } // Security returns SecurityResolver implementation. func (r *Resolver) Security() SecurityResolver { return &securityResolver{r} } +// Transaction returns TransactionResolver implementation. +func (r *Resolver) Transaction() TransactionResolver { return &transactionResolver{r} } + type accountResolver struct{ *Resolver } type listedSecurityResolver struct{ *Resolver } type mutationResolver struct{ *Resolver } @@ -242,3 +271,4 @@ type portfolioResolver struct{ *Resolver } type portfolioEventResolver struct{ *Resolver } type queryResolver struct{ *Resolver } type securityResolver struct{ *Resolver } +type transactionResolver struct{ *Resolver } diff --git a/graph/schema.resolvers_test.go b/graph/schema.resolvers_test.go index a60ad001..c703ca9d 100644 --- a/graph/schema.resolvers_test.go +++ b/graph/schema.resolvers_test.go @@ -127,3 +127,88 @@ func Test_queryResolver_Accounts(t *testing.T) { }) } } + +func Test_queryResolver_Transactions(t *testing.T) { + type fields struct { + Resolver *Resolver + } + type args struct { + ctx context.Context + accountID string + } + tests := []struct { + name string + fields fields + args args + want []*persistence.Transaction + wantErr bool + }{ + { + name: "list brokerage transactions", + fields: fields{ + Resolver: &Resolver{ + DB: persistencetest.NewTestDB(t, func(db *persistence.DB) { + _, err := db.Queries.CreateAccount(context.Background(), testdata.TestCreateBankAccountParams) + assert.NoError(t, err) + + _, err = db.Queries.CreateAccount(context.Background(), testdata.TestCreateBrokerageAccountParams) + assert.NoError(t, err) + + _, err = db.Queries.CreateTransaction(context.Background(), testdata.TestCreateBuyTransactionParams) + assert.NoError(t, err) + }), + }, + }, + args: args{ + ctx: context.TODO(), + accountID: testdata.TestBrokerageAccount.ID, + }, + want: []*persistence.Transaction{ + testdata.TestBuyTransaction, + }, + }, + { + name: "list bank transactions", + fields: fields{ + Resolver: &Resolver{ + DB: persistencetest.NewTestDB(t, func(db *persistence.DB) { + _, err := db.Queries.CreateAccount(context.Background(), testdata.TestCreateBankAccountParams) + assert.NoError(t, err) + + _, err = db.Queries.CreateAccount(context.Background(), testdata.TestCreateBrokerageAccountParams) + assert.NoError(t, err) + + _, err = db.Queries.CreateTransaction(context.Background(), testdata.TestCreateBuyTransactionParams) + assert.NoError(t, err) + + _, err = db.Queries.CreateTransaction(context.Background(), testdata.TestCreateDepositTransactionParams) + assert.NoError(t, err) + }), + }, + }, + args: args{ + ctx: context.TODO(), + accountID: testdata.TestBankAccount.ID, + }, + want: []*persistence.Transaction{ + testdata.TestBuyTransaction, + testdata.TestDepositTransaction, + }, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + r := &queryResolver{ + Resolver: tt.fields.Resolver, + } + got, err := r.Transactions(tt.args.ctx, tt.args.accountID) + if (err != nil) != tt.wantErr { + t.Errorf("queryResolver.Transactions() error = %v, wantErr %v", err, tt.wantErr) + return + } + + assert.Equals(t, tt.want, got) + }) + } +} diff --git a/internal/testdata/securities.go b/internal/testdata/securities.go index fde12bdc..2ef7b151 100644 --- a/internal/testdata/securities.go +++ b/internal/testdata/securities.go @@ -2,10 +2,14 @@ package testdata import ( "database/sql" + "time" + "github.com/google/uuid" + "github.com/oxisto/money-gopher/currency" "github.com/oxisto/money-gopher/internal/testing/quotetest" "github.com/oxisto/money-gopher/persistence" "github.com/oxisto/money-gopher/portfolio/accounts" + "github.com/oxisto/money-gopher/portfolio/events" ) // TestSecurity is a test security. @@ -44,9 +48,74 @@ var TestBankAccount = &persistence.Account{ Type: accounts.AccountTypeBank, } -// TestCreateBankAccountParams is a test bank account creation parameter. +// TestBrokerageAccount is a test security account. +var TestBrokerageAccount = &persistence.Account{ + ID: "mybrokerage", + DisplayName: "My Brokerage", + Type: accounts.AccountTypeBrokerage, +} + +// TestCreateBankAccountParams is a test bank account creation parameter for +// [TestBankAccount]. var TestCreateBankAccountParams = persistence.CreateAccountParams{ ID: TestBankAccount.ID, DisplayName: TestBankAccount.DisplayName, Type: TestBankAccount.Type, } + +// TestCreateBrokerageAccountParams is a test brokerage account creation +// parameter for [TestBrokerageAccount]. +var TestCreateBrokerageAccountParams = persistence.CreateAccountParams{ + ID: TestBrokerageAccount.ID, + DisplayName: TestBrokerageAccount.DisplayName, + Type: TestBrokerageAccount.Type, +} + +// TestBuyTransaction is a test buy transaction of [TestSecurity]. The buy is +// initiated from [TestBankAccount] and the stocks are deposited in +// [TestBrokerageAccount]. +var TestBuyTransaction = &persistence.Transaction{ + ID: uuid.NewString(), + SourceAccountID: sql.NullString{String: TestBankAccount.ID, Valid: true}, + DestinationAccountID: sql.NullString{String: TestBrokerageAccount.ID, Valid: true}, + Time: time.Now(), + Type: events.PortfolioEventTypeBuy, + Amount: 100, + SecurityID: sql.NullString{String: TestSecurity.ID, Valid: true}, + Price: currency.Value(100), +} + +// TestCreateBuyTransactionParams is a test buy transaction creation parameter +// for [TestBuyTransaction]. +var TestCreateBuyTransactionParams = persistence.CreateTransactionParams{ + ID: TestBuyTransaction.ID, + SourceAccountID: TestBuyTransaction.SourceAccountID, + DestinationAccountID: TestBuyTransaction.DestinationAccountID, + Time: TestBuyTransaction.Time, + Type: TestBuyTransaction.Type, + Amount: TestBuyTransaction.Amount, + SecurityID: TestBuyTransaction.SecurityID, + Price: TestBuyTransaction.Price, +} + +// TestDepositTransaction is a test deposit transaction. The deposit is made to +// [TestBankAccount]. +var TestDepositTransaction = &persistence.Transaction{ + ID: uuid.NewString(), + DestinationAccountID: sql.NullString{String: TestBankAccount.ID, Valid: true}, + Time: time.Now(), + Type: events.PortfolioEventTypeBuy, + Amount: 1, + Price: currency.Value(100), +} + +// TestCreateDepositTransactionParams is a test deposit transaction creation +// parameter for [TestDepositTransaction]. +var TestCreateDepositTransactionParams = persistence.CreateTransactionParams{ + ID: TestDepositTransaction.ID, + DestinationAccountID: TestDepositTransaction.DestinationAccountID, + Time: TestDepositTransaction.Time, + Type: TestDepositTransaction.Type, + Amount: TestDepositTransaction.Amount, + Price: TestDepositTransaction.Price, +} diff --git a/money-gopher.code-workspace b/money-gopher.code-workspace index b9c89c34..722ac7fd 100644 --- a/money-gopher.code-workspace +++ b/money-gopher.code-workspace @@ -27,6 +27,7 @@ "ISIN", "jotaen", "kongcompletion", + "linecomment", "lmittmann", "mapstructure", "mattn", @@ -60,6 +61,7 @@ "tailwindcss", "timestamppb", "tparse", + "unmarshals", "urfave", "vektah" ], diff --git a/persistence/accounts.sql.go b/persistence/accounts.sql.go index f22574c9..105fa046 100644 --- a/persistence/accounts.sql.go +++ b/persistence/accounts.sql.go @@ -8,8 +8,11 @@ package persistence import ( "context" "database/sql" + "time" + currency "github.com/oxisto/money-gopher/currency" "github.com/oxisto/money-gopher/portfolio/accounts" + "github.com/oxisto/money-gopher/portfolio/events" ) const createAccount = `-- name: CreateAccount :one @@ -82,6 +85,66 @@ func (q *Queries) CreatePortfolio(ctx context.Context, arg CreatePortfolioParams return &i, err } +const createTransaction = `-- name: CreateTransaction :one +INSERT INTO + transactions ( + id, + source_account_id, + destination_account_id, + time, + type, + security_id, + amount, + price, + fees, + taxes + ) +VALUES + (?, ?, ?, ?, ?, ?, ?, ?, ?, ?) RETURNING id, source_account_id, destination_account_id, time, type, security_id, amount, price, fees, taxes +` + +type CreateTransactionParams struct { + ID string + SourceAccountID sql.NullString + DestinationAccountID sql.NullString + Time time.Time + Type events.PortfolioEventType + SecurityID sql.NullString + Amount float64 + Price *currency.Currency + Fees *currency.Currency + Taxes *currency.Currency +} + +func (q *Queries) CreateTransaction(ctx context.Context, arg CreateTransactionParams) (*Transaction, error) { + row := q.db.QueryRowContext(ctx, createTransaction, + arg.ID, + arg.SourceAccountID, + arg.DestinationAccountID, + arg.Time, + arg.Type, + arg.SecurityID, + arg.Amount, + arg.Price, + arg.Fees, + arg.Taxes, + ) + var i Transaction + err := row.Scan( + &i.ID, + &i.SourceAccountID, + &i.DestinationAccountID, + &i.Time, + &i.Type, + &i.SecurityID, + &i.Amount, + &i.Price, + &i.Fees, + &i.Taxes, + ) + return &i, err +} + const deleteAccount = `-- name: DeleteAccount :one DELETE FROM accounts WHERE @@ -263,3 +326,47 @@ func (q *Queries) ListPortfolios(ctx context.Context) ([]*Portfolio, error) { } return items, nil } + +const listTransactionsByAccountID = `-- name: ListTransactionsByAccountID :many +SELECT + id, source_account_id, destination_account_id, time, type, security_id, amount, price, fees, taxes +FROM + transactions +WHERE + source_account_id = ?1 + OR destination_account_id = ?1 +` + +func (q *Queries) ListTransactionsByAccountID(ctx context.Context, accountID sql.NullString) ([]*Transaction, error) { + rows, err := q.db.QueryContext(ctx, listTransactionsByAccountID, accountID) + if err != nil { + return nil, err + } + defer rows.Close() + var items []*Transaction + for rows.Next() { + var i Transaction + if err := rows.Scan( + &i.ID, + &i.SourceAccountID, + &i.DestinationAccountID, + &i.Time, + &i.Type, + &i.SecurityID, + &i.Amount, + &i.Price, + &i.Fees, + &i.Taxes, + ); err != nil { + return nil, err + } + items = append(items, &i) + } + if err := rows.Close(); err != nil { + return nil, err + } + if err := rows.Err(); err != nil { + return nil, err + } + return items, nil +} diff --git a/persistence/models.go b/persistence/models.go index d7fa07b8..c584d87d 100644 --- a/persistence/models.go +++ b/persistence/models.go @@ -83,3 +83,27 @@ type Security struct { // QuoteProvider is the name of the provider that provides quotes for this security. QuoteProvider sql.NullString } + +// Transactions represents a transaction in an account. +type Transaction struct { + // ID is the primary identifier for a transaction. + ID string + // SourceAccountID is the ID of the account that the transaction originated from. + SourceAccountID sql.NullString + // DestinationAccountID is the ID of the account that the transaction is destined for. + DestinationAccountID sql.NullString + // Time is the time that the transaction occurred. + Time time.Time + // Type is the type of the transaction. Depending on the type, different fields (source, destination) will be used. + Type events.PortfolioEventType + // SecurityID is the ID of the security that the transaction is related to. Can be empty if the transaction is not related to a security. + SecurityID sql.NullString + // Amount is the amount of the transaction. + Amount float64 + // Price is the price of the transaction. + Price *currency.Currency + // Fees is the fees of the transaction. + Fees *currency.Currency + // Taxes is the taxes of the transaction. + Taxes *currency.Currency +} diff --git a/persistence/sql/migrations/0002_create_accounts.sql b/persistence/sql/migrations/0002_create_accounts.sql index d253d4d9..bf5fbf13 100644 --- a/persistence/sql/migrations/0002_create_accounts.sql +++ b/persistence/sql/migrations/0002_create_accounts.sql @@ -39,6 +39,23 @@ CREATE TABLE FOREIGN KEY (reference_account_id) REFERENCES accounts (id) ON DELETE RESTRICT ); +CREATE TABLE + IF NOT EXISTS transactions ( + -- Transactions represents a transaction in an account. + id TEXT PRIMARY KEY, -- ID is the primary identifier for a transaction. + source_account_id TEXT, -- SourceAccountID is the ID of the account that the transaction originated from. + destination_account_id TEXT, -- DestinationAccountID is the ID of the account that the transaction is destined for. + time DATETIME NOT NULL, -- Time is the time that the transaction occurred. + type INTEGER NOT NULL, -- Type is the type of the transaction. Depending on the type, different fields (source, destination) will be used. + security_id TEXT, -- SecurityID is the ID of the security that the transaction is related to. Can be empty if the transaction is not related to a security. + amount REAL NOT NULL, -- Amount is the amount of the transaction. + price JSONB, -- Price is the price of the transaction. + fees JSONB, -- Fees is the fees of the transaction. + taxes JSONB, -- Taxes is the taxes of the transaction. + FOREIGN KEY (source_account_id) REFERENCES accounts (id) ON DELETE RESTRICT, + FOREIGN KEY (destination_account_id) REFERENCES accounts (id) ON DELETE RESTRICT + ); + CREATE TABLE IF NOT EXISTS portfolio_accounts ( -- PortfolioAccounts represents the relationship between portfolios and accounts. @@ -58,4 +75,6 @@ DROP TABLE portfolio_accounts; DROP TABLE bank_accounts; -DROP TABLE accounts; \ No newline at end of file +DROP TABLE accounts; + +DROP TABLE transactions; \ No newline at end of file diff --git a/persistence/sql/queries/accounts.sql b/persistence/sql/queries/accounts.sql index 95b7a37b..26c86191 100644 --- a/persistence/sql/queries/accounts.sql +++ b/persistence/sql/queries/accounts.sql @@ -67,4 +67,30 @@ VALUES INSERT INTO portfolios (id, display_name, bank_account_id) VALUES - (?, ?, ?) RETURNING *; \ No newline at end of file + (?, ?, ?) RETURNING *; + +-- name: CreateTransaction :one +INSERT INTO + transactions ( + id, + source_account_id, + destination_account_id, + time, + type, + security_id, + amount, + price, + fees, + taxes + ) +VALUES + (?, ?, ?, ?, ?, ?, ?, ?, ?, ?) RETURNING *; + +-- name: ListTransactionsByAccountID :many +SELECT + * +FROM + transactions +WHERE + source_account_id = sqlc.arg ('account_id') + OR destination_account_id = sqlc.arg ('account_id'); \ No newline at end of file diff --git a/sqlc.yaml b/sqlc.yaml index 216025ed..be451618 100644 --- a/sqlc.yaml +++ b/sqlc.yaml @@ -31,6 +31,26 @@ sql: package: "currency" type: "Currency" pointer: true + - column: "transactions.type" + go_type: github.com/oxisto/money-gopher/portfolio/events.PortfolioEventType + - column: "transactions.price" + go_type: + import: "github.com/oxisto/money-gopher/currency" + package: "currency" + type: "Currency" + pointer: true + - column: "transactions.fees" + go_type: + import: "github.com/oxisto/money-gopher/currency" + package: "currency" + type: "Currency" + pointer: true + - column: "transactions.taxes" + go_type: + import: "github.com/oxisto/money-gopher/currency" + package: "currency" + type: "Currency" + pointer: true - column: "listed_securities.latest_quote" go_type: import: "github.com/oxisto/money-gopher/currency" From 0efdc9ba0993e849893bdb4752eaa52473161d1f Mon Sep 17 00:00:00 2001 From: Christian Banse Date: Sun, 12 Jan 2025 18:36:55 +0100 Subject: [PATCH 31/35] Implemented more portfolio --- cli/commands/account.go | 4 +- cli/commands/portfolio.go | 57 +- cli/commands/portfolio_test.go | 36 +- finance/calculation.go | 4 +- finance/calculation_test.go | 4 +- finance/snapshot.go | 20 +- graph/generated.go | 516 ++++++++---------- graph/schema.graphqls | 11 +- graph/schema.resolvers.go | 71 ++- graph/schema.resolvers_test.go | 66 +++ import/csv/csv_importer.go | 27 +- import/csv/csv_importer_test.go | 97 ++-- internal/testdata/securities.go | 20 + models/models_gen.go | 12 +- persistence/accounts.sql.go | 175 ++++-- persistence/extra.go | 7 +- persistence/models.go | 23 +- .../sql/migrations/0002_create_accounts.sql | 31 +- persistence/sql/queries/accounts.sql | 67 ++- 19 files changed, 700 insertions(+), 548 deletions(-) diff --git a/cli/commands/account.go b/cli/commands/account.go index a1d3c2bb..0cb38654 100644 --- a/cli/commands/account.go +++ b/cli/commands/account.go @@ -84,8 +84,6 @@ var AccountCmd = &cli.Command{ func CreateAccount(ctx context.Context, cmd *cli.Command) (err error) { s := mcli.FromContext(ctx) - fmt.Printf("%+v", cmd.Generic("type")) - var query struct { CreateAccount struct { ID string `json:"id"` @@ -105,7 +103,7 @@ func CreateAccount(ctx context.Context, cmd *cli.Command) (err error) { return err } - fmt.Fprintln(cmd.Writer, query.CreateAccount) + s.WriteJSON(cmd.Writer, query.CreateAccount) return nil } diff --git a/cli/commands/portfolio.go b/cli/commands/portfolio.go index aa496199..fb22db70 100644 --- a/cli/commands/portfolio.go +++ b/cli/commands/portfolio.go @@ -21,6 +21,7 @@ import ( "fmt" mcli "github.com/oxisto/money-gopher/cli" + "github.com/oxisto/money-gopher/models" "github.com/fatih/color" "github.com/urfave/cli/v3" @@ -39,7 +40,7 @@ var PortfolioCmd = &cli.Command{ Flags: []cli.Flag{ &cli.StringFlag{Name: "id", Usage: "The identifier of the portfolio, e.g. mybank-myportfolio", Required: true}, &cli.StringFlag{Name: "display-name", Usage: "The display name of the portfolio"}, - &cli.StringFlag{Name: "bank-account-id", Usage: "The bank account ID of the portfolio"}, + &cli.StringSliceFlag{Name: "account-ids", Usage: "The account IDs that should be linked to the portfolio"}, }, }, { @@ -90,7 +91,22 @@ var PortfolioCmd = &cli.Command{ } // ListPortfolio lists all portfolios. -func ListPortfolio(ctx context.Context, cmd *cli.Command) error { +func ListPortfolio(ctx context.Context, cmd *cli.Command) (err error) { + s := mcli.FromContext(ctx) + + var query struct { + Portfolios []struct { + ID string `json:"id"` + DisplayName string `json:"displayName"` + } `json:"portfolios"` + } + + err = s.GraphQL.Query(context.Background(), &query, nil) + if err != nil { + return err + } + + s.WriteJSON(cmd.Writer, query) /* s := mcli.FromContext(ctx) res, err := s.PortfolioClient.ListPortfolios( @@ -138,23 +154,34 @@ func ListPortfolio(ctx context.Context, cmd *cli.Command) error { } // CreatePortfolio creates a new portfolio. -func CreatePortfolio(ctx context.Context, cmd *cli.Command) error { - /*s := mcli.FromContext(ctx) - res, err := s.PortfolioClient.CreatePortfolio( - context.Background(), - connect.NewRequest(&portfoliov1.CreatePortfolioRequest{ - Portfolio: &portfoliov1.Portfolio{ - Id: cmd.String("id"), - DisplayName: cmd.String("display-name"), - BankAccountId: cmd.String("bank-account-id"), - }, - }), - ) +func CreatePortfolio(ctx context.Context, cmd *cli.Command) (err error) { + s := mcli.FromContext(ctx) + + var query struct { + CreatePortfolio struct { + ID string `json:"id"` + DisplayName string `json:"displayName"` + Accounts []struct { + ID string `json:"id"` + DisplayName string `json:"displayName"` + Type string `json:"type"` + } `json:"accounts"` + } `graphql:"createPortfolio(input: $input)" json:"account"` + } + + err = s.GraphQL.Mutate(context.Background(), &query, map[string]interface{}{ + "input": models.PortfolioInput{ + ID: cmd.String("id"), + DisplayName: cmd.String("display-name"), + AccountIds: cmd.StringSlice("account-ids"), + }, + }) if err != nil { return err } - fmt.Println(res.Msg)*/ + s.WriteJSON(cmd.Writer, query.CreatePortfolio) + return nil } diff --git a/cli/commands/portfolio_test.go b/cli/commands/portfolio_test.go index acedb0a2..b9e14521 100644 --- a/cli/commands/portfolio_test.go +++ b/cli/commands/portfolio_test.go @@ -22,6 +22,7 @@ import ( "github.com/oxisto/assert" "github.com/oxisto/money-gopher/internal" + "github.com/oxisto/money-gopher/internal/testdata" "github.com/oxisto/money-gopher/internal/testing/clitest" "github.com/oxisto/money-gopher/internal/testing/servertest" "github.com/oxisto/money-gopher/persistence" @@ -29,7 +30,19 @@ import ( ) func TestListPortfolio(t *testing.T) { - srv := servertest.NewServer(internal.NewTestDB(t)) + srv := servertest.NewServer(internal.NewTestDB(t, func(db *persistence.DB) { + _, err := db.Queries.CreateAccount(context.Background(), testdata.TestCreateBankAccountParams) + assert.NoError(t, err) + + _, err = db.Queries.CreateAccount(context.Background(), testdata.TestCreateBrokerageAccountParams) + assert.NoError(t, err) + + _, err = db.Queries.CreatePortfolio(context.Background(), testdata.TestCreatePortfolioParams) + assert.NoError(t, err) + + db.Queries.AddAccountToPortfolio(context.Background(), testdata.TestAddAccountToPortfolioParams) + assert.NoError(t, err) + })) defer srv.Close() type args struct { @@ -45,7 +58,9 @@ func TestListPortfolio(t *testing.T) { name: "happy path", args: args{ ctx: clitest.NewSessionContext(t, srv), - cmd: &cli.Command{}, + cmd: clitest.MockCommand(t, + PortfolioCmd.Command("list").Flags, + ), }, }, } @@ -61,10 +76,7 @@ func TestListPortfolio(t *testing.T) { func TestCreatePortfolio(t *testing.T) { srv := servertest.NewServer(internal.NewTestDB(t, func(db *persistence.DB) { - _, err := db.Queries.CreateBankAccount(context.Background(), persistence.CreateBankAccountParams{ - ID: "mybank", - DisplayName: "My Bank", - }) + _, err := db.Queries.CreateAccount(context.Background(), testdata.TestCreateBankAccountParams) assert.NoError(t, err) })) defer srv.Close() @@ -84,9 +96,9 @@ func TestCreatePortfolio(t *testing.T) { ctx: clitest.NewSessionContext(t, srv), cmd: clitest.MockCommand(t, PortfolioCmd.Command("create").Flags, - "--bank-account-id", "mybank", "--id", "mynewportfolio", "--display-name", "My New Portfolio", + "--account-ids", testdata.TestCreateBankAccountParams.ID, ), }, }, @@ -220,16 +232,12 @@ func TestImportTransactions(t *testing.T) { func TestPredictPortfolios(t *testing.T) { srv := servertest.NewServer(internal.NewTestDB(t, func(db *persistence.DB) { - _, err := db.Queries.CreateBankAccount(context.Background(), persistence.CreateBankAccountParams{ - ID: "mybank", - DisplayName: "My Bank", - }) + _, err := db.Queries.CreateAccount(context.Background(), testdata.TestCreateBankAccountParams) assert.NoError(t, err) _, err = db.Queries.CreatePortfolio(context.Background(), persistence.CreatePortfolioParams{ - ID: "mybank/myportfolio", - DisplayName: "My Portfolio", - BankAccountID: "mybank", + ID: "mybank/myportfolio", + DisplayName: "My Portfolio", }) assert.NoError(t, err) })) diff --git a/finance/calculation.go b/finance/calculation.go index 47e057f7..4d22c8e3 100644 --- a/finance/calculation.go +++ b/finance/calculation.go @@ -48,7 +48,7 @@ type calculation struct { } // NewCalculation creates a new calculation struct and applies all events -func NewCalculation(events []*persistence.PortfolioEvent) *calculation { +func NewCalculation(events []*persistence.Transaction) *calculation { var c calculation c.Fees = currency.Zero() c.Taxes = currency.Zero() @@ -61,7 +61,7 @@ func NewCalculation(events []*persistence.PortfolioEvent) *calculation { return &c } -func (c *calculation) Apply(tx *persistence.PortfolioEvent) { +func (c *calculation) Apply(tx *persistence.Transaction) { switch tx.Type { case events.PortfolioEventTypeDeliveryInbound: fallthrough diff --git a/finance/calculation_test.go b/finance/calculation_test.go index 50a7745e..8a872055 100644 --- a/finance/calculation_test.go +++ b/finance/calculation_test.go @@ -28,7 +28,7 @@ import ( func TestNewCalculation(t *testing.T) { type args struct { - txs []*persistence.PortfolioEvent + txs []*persistence.Transaction } tests := []struct { name string @@ -38,7 +38,7 @@ func TestNewCalculation(t *testing.T) { { name: "buy and sell", args: args{ - txs: []*persistence.PortfolioEvent{ + txs: []*persistence.Transaction{ { Type: events.PortfolioEventTypeDepositCash, Price: currency.Value(500000), diff --git a/finance/snapshot.go b/finance/snapshot.go index 2ddbd645..eff5bb6b 100644 --- a/finance/snapshot.go +++ b/finance/snapshot.go @@ -18,7 +18,7 @@ import ( // securities by their IDs. type SnapshotDataProvider interface { ListListedSecuritiesBySecurityID(ctx context.Context, securityID string) ([]*persistence.ListedSecurity, error) - ListPortfolioEventsByPortfolioID(ctx context.Context, portfolioID string) ([]*persistence.PortfolioEvent, error) + ListTransactionsByPortfolioID(ctx context.Context, portfolioID string) ([]*persistence.Transaction, error) ListSecuritiesByIDs(ctx context.Context, ids []string) ([]*persistence.Security, error) } @@ -36,15 +36,15 @@ func BuildSnapshot( provider SnapshotDataProvider, ) (snap *models.PortfolioSnapshot, err error) { var ( - events []*persistence.PortfolioEvent - m map[string][]*persistence.PortfolioEvent + events []*persistence.Transaction + m map[string][]*persistence.Transaction ids []string secs []*persistence.Security secmap map[string]*persistence.Security ) // Retrieve events - events, err = provider.ListPortfolioEventsByPortfolioID(ctx, portfolioID) + events, err = provider.ListTransactionsByPortfolioID(ctx, portfolioID) if err != nil { return nil, err } @@ -133,8 +133,8 @@ func BuildSnapshot( // eventsBefore returns all events that occurred before a given time. // TODO: move to SQL query -func eventsBefore(events []*persistence.PortfolioEvent, t time.Time) (out []*persistence.PortfolioEvent) { - out = make([]*persistence.PortfolioEvent, 0, len(events)) +func eventsBefore(events []*persistence.Transaction, t time.Time) (out []*persistence.Transaction) { + out = make([]*persistence.Transaction, 0, len(events)) for _, event := range events { if event.Time.After(t) { @@ -148,13 +148,13 @@ func eventsBefore(events []*persistence.PortfolioEvent, t time.Time) (out []*per } // groupByPortfolio groups the events by their security ID. -func groupByPortfolio(events []*persistence.PortfolioEvent) (m map[string][]*persistence.PortfolioEvent) { - m = make(map[string][]*persistence.PortfolioEvent) +func groupByPortfolio(events []*persistence.Transaction) (m map[string][]*persistence.Transaction) { + m = make(map[string][]*persistence.Transaction) for _, event := range events { name := event.SecurityID - if name != "" { - m[name] = append(m[name], event) + if name.Valid { + m[name.String] = append(m[name.String], event) } else { // a little bit of a hack m["cash"] = append(m["cash"], event) diff --git a/graph/generated.go b/graph/generated.go index de6900d3..ed6cb14f 100644 --- a/graph/generated.go +++ b/graph/generated.go @@ -47,7 +47,6 @@ type ResolverRoot interface { ListedSecurity() ListedSecurityResolver Mutation() MutationResolver Portfolio() PortfolioResolver - PortfolioEvent() PortfolioEventResolver Query() QueryResolver Security() SecurityResolver Transaction() TransactionResolver @@ -64,11 +63,6 @@ type ComplexityRoot struct { Type func(childComplexity int) int } - BankAccount struct { - DisplayName func(childComplexity int) int - ID func(childComplexity int) int - } - Currency struct { Amount func(childComplexity int) int Symbol func(childComplexity int) int @@ -88,11 +82,12 @@ type ComplexityRoot struct { CreateSecurity func(childComplexity int, input models.SecurityInput) int DeleteAccount func(childComplexity int, id string) int TriggerQuoteUpdate func(childComplexity int, securityIDs []string) int + UpdatePortfolio func(childComplexity int, id string, input models.PortfolioInput) int UpdateSecurity func(childComplexity int, id string, input models.SecurityInput) int } Portfolio struct { - BankAccount func(childComplexity int) int + Accounts func(childComplexity int) int DisplayName func(childComplexity int) int Events func(childComplexity int) int ID func(childComplexity int) int @@ -158,7 +153,7 @@ type ComplexityRoot struct { } type AccountResolver interface { - ReferenceAccount(ctx context.Context, obj *persistence.Account) (*persistence.BankAccount, error) + ReferenceAccount(ctx context.Context, obj *persistence.Account) (*persistence.Account, error) } type ListedSecurityResolver interface { Security(ctx context.Context, obj *persistence.ListedSecurity) (*persistence.Security, error) @@ -169,19 +164,15 @@ type MutationResolver interface { CreateSecurity(ctx context.Context, input models.SecurityInput) (*persistence.Security, error) UpdateSecurity(ctx context.Context, id string, input models.SecurityInput) (*persistence.Security, error) CreatePortfolio(ctx context.Context, input models.PortfolioInput) (*persistence.Portfolio, error) + UpdatePortfolio(ctx context.Context, id string, input models.PortfolioInput) (*persistence.Portfolio, error) CreateAccount(ctx context.Context, input models.AccountInput) (*persistence.Account, error) DeleteAccount(ctx context.Context, id string) (*persistence.Account, error) TriggerQuoteUpdate(ctx context.Context, securityIDs []string) ([]*persistence.ListedSecurity, error) } type PortfolioResolver interface { - BankAccount(ctx context.Context, obj *persistence.Portfolio) (*persistence.BankAccount, error) + Accounts(ctx context.Context, obj *persistence.Portfolio) ([]*persistence.Account, error) Snapshot(ctx context.Context, obj *persistence.Portfolio, when string) (*models.PortfolioSnapshot, error) - Events(ctx context.Context, obj *persistence.Portfolio) ([]*persistence.PortfolioEvent, error) -} -type PortfolioEventResolver interface { - Time(ctx context.Context, obj *persistence.PortfolioEvent) (string, error) - - Security(ctx context.Context, obj *persistence.PortfolioEvent) (*persistence.Security, error) + Events(ctx context.Context, obj *persistence.Portfolio) ([]*models.PortfolioEvent, error) } type QueryResolver interface { Security(ctx context.Context, id string) (*persistence.Security, error) @@ -250,20 +241,6 @@ func (e *executableSchema) Complexity(typeName, field string, childComplexity in return e.complexity.Account.Type(childComplexity), true - case "BankAccount.displayName": - if e.complexity.BankAccount.DisplayName == nil { - break - } - - return e.complexity.BankAccount.DisplayName(childComplexity), true - - case "BankAccount.id": - if e.complexity.BankAccount.ID == nil { - break - } - - return e.complexity.BankAccount.ID(childComplexity), true - case "Currency.amount": if e.complexity.Currency.Amount == nil { break @@ -373,6 +350,18 @@ func (e *executableSchema) Complexity(typeName, field string, childComplexity in return e.complexity.Mutation.TriggerQuoteUpdate(childComplexity, args["securityIDs"].([]string)), true + case "Mutation.updatePortfolio": + if e.complexity.Mutation.UpdatePortfolio == nil { + break + } + + args, err := ec.field_Mutation_updatePortfolio_args(context.TODO(), rawArgs) + if err != nil { + return 0, false + } + + return e.complexity.Mutation.UpdatePortfolio(childComplexity, args["id"].(string), args["input"].(models.PortfolioInput)), true + case "Mutation.updateSecurity": if e.complexity.Mutation.UpdateSecurity == nil { break @@ -385,12 +374,12 @@ func (e *executableSchema) Complexity(typeName, field string, childComplexity in return e.complexity.Mutation.UpdateSecurity(childComplexity, args["id"].(string), args["input"].(models.SecurityInput)), true - case "Portfolio.bankAccount": - if e.complexity.Portfolio.BankAccount == nil { + case "Portfolio.accounts": + if e.complexity.Portfolio.Accounts == nil { break } - return e.complexity.Portfolio.BankAccount(childComplexity), true + return e.complexity.Portfolio.Accounts(childComplexity), true case "Portfolio.displayName": if e.complexity.Portfolio.DisplayName == nil { @@ -961,6 +950,47 @@ func (ec *executionContext) field_Mutation_triggerQuoteUpdate_argsSecurityIDs( return zeroVal, nil } +func (ec *executionContext) field_Mutation_updatePortfolio_args(ctx context.Context, rawArgs map[string]any) (map[string]any, error) { + var err error + args := map[string]any{} + arg0, err := ec.field_Mutation_updatePortfolio_argsID(ctx, rawArgs) + if err != nil { + return nil, err + } + args["id"] = arg0 + arg1, err := ec.field_Mutation_updatePortfolio_argsInput(ctx, rawArgs) + if err != nil { + return nil, err + } + args["input"] = arg1 + return args, nil +} +func (ec *executionContext) field_Mutation_updatePortfolio_argsID( + ctx context.Context, + rawArgs map[string]any, +) (string, error) { + ctx = graphql.WithPathContext(ctx, graphql.NewPathWithField("id")) + if tmp, ok := rawArgs["id"]; ok { + return ec.unmarshalNID2string(ctx, tmp) + } + + var zeroVal string + return zeroVal, nil +} + +func (ec *executionContext) field_Mutation_updatePortfolio_argsInput( + ctx context.Context, + rawArgs map[string]any, +) (models.PortfolioInput, error) { + ctx = graphql.WithPathContext(ctx, graphql.NewPathWithField("input")) + if tmp, ok := rawArgs["input"]; ok { + return ec.unmarshalNPortfolioInput2githubᚗcomᚋoxistoᚋmoneyᚑgopherᚋmodelsᚐPortfolioInput(ctx, tmp) + } + + var zeroVal models.PortfolioInput + return zeroVal, nil +} + func (ec *executionContext) field_Mutation_updateSecurity_args(ctx context.Context, rawArgs map[string]any) (map[string]any, error) { var err error args := map[string]any{} @@ -1349,9 +1379,9 @@ func (ec *executionContext) _Account_referenceAccount(ctx context.Context, field if resTmp == nil { return graphql.Null } - res := resTmp.(*persistence.BankAccount) + res := resTmp.(*persistence.Account) fc.Result = res - return ec.marshalOBankAccount2ᚖgithubᚗcomᚋoxistoᚋmoneyᚑgopherᚋpersistenceᚐBankAccount(ctx, field.Selections, res) + return ec.marshalOAccount2ᚖgithubᚗcomᚋoxistoᚋmoneyᚑgopherᚋpersistenceᚐAccount(ctx, field.Selections, res) } func (ec *executionContext) fieldContext_Account_referenceAccount(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { @@ -1363,99 +1393,15 @@ func (ec *executionContext) fieldContext_Account_referenceAccount(_ context.Cont Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { switch field.Name { case "id": - return ec.fieldContext_BankAccount_id(ctx, field) + return ec.fieldContext_Account_id(ctx, field) case "displayName": - return ec.fieldContext_BankAccount_displayName(ctx, field) + return ec.fieldContext_Account_displayName(ctx, field) + case "type": + return ec.fieldContext_Account_type(ctx, field) + case "referenceAccount": + return ec.fieldContext_Account_referenceAccount(ctx, field) } - return nil, fmt.Errorf("no field named %q was found under type BankAccount", field.Name) - }, - } - return fc, nil -} - -func (ec *executionContext) _BankAccount_id(ctx context.Context, field graphql.CollectedField, obj *persistence.BankAccount) (ret graphql.Marshaler) { - fc, err := ec.fieldContext_BankAccount_id(ctx, field) - if err != nil { - return graphql.Null - } - ctx = graphql.WithFieldContext(ctx, fc) - defer func() { - if r := recover(); r != nil { - ec.Error(ctx, ec.Recover(ctx, r)) - ret = graphql.Null - } - }() - resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (any, error) { - ctx = rctx // use context from middleware stack in children - return obj.ID, nil - }) - if err != nil { - ec.Error(ctx, err) - return graphql.Null - } - if resTmp == nil { - if !graphql.HasFieldError(ctx, fc) { - ec.Errorf(ctx, "must not be null") - } - return graphql.Null - } - res := resTmp.(string) - fc.Result = res - return ec.marshalNString2string(ctx, field.Selections, res) -} - -func (ec *executionContext) fieldContext_BankAccount_id(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { - fc = &graphql.FieldContext{ - Object: "BankAccount", - Field: field, - IsMethod: false, - IsResolver: false, - Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { - return nil, errors.New("field of type String does not have child fields") - }, - } - return fc, nil -} - -func (ec *executionContext) _BankAccount_displayName(ctx context.Context, field graphql.CollectedField, obj *persistence.BankAccount) (ret graphql.Marshaler) { - fc, err := ec.fieldContext_BankAccount_displayName(ctx, field) - if err != nil { - return graphql.Null - } - ctx = graphql.WithFieldContext(ctx, fc) - defer func() { - if r := recover(); r != nil { - ec.Error(ctx, ec.Recover(ctx, r)) - ret = graphql.Null - } - }() - resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (any, error) { - ctx = rctx // use context from middleware stack in children - return obj.DisplayName, nil - }) - if err != nil { - ec.Error(ctx, err) - return graphql.Null - } - if resTmp == nil { - if !graphql.HasFieldError(ctx, fc) { - ec.Errorf(ctx, "must not be null") - } - return graphql.Null - } - res := resTmp.(string) - fc.Result = res - return ec.marshalNString2string(ctx, field.Selections, res) -} - -func (ec *executionContext) fieldContext_BankAccount_displayName(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { - fc = &graphql.FieldContext{ - Object: "BankAccount", - Field: field, - IsMethod: false, - IsResolver: false, - Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { - return nil, errors.New("field of type String does not have child fields") + return nil, fmt.Errorf("no field named %q was found under type Account", field.Name) }, } return fc, nil @@ -1952,8 +1898,8 @@ func (ec *executionContext) fieldContext_Mutation_createPortfolio(ctx context.Co return ec.fieldContext_Portfolio_id(ctx, field) case "displayName": return ec.fieldContext_Portfolio_displayName(ctx, field) - case "bankAccount": - return ec.fieldContext_Portfolio_bankAccount(ctx, field) + case "accounts": + return ec.fieldContext_Portfolio_accounts(ctx, field) case "snapshot": return ec.fieldContext_Portfolio_snapshot(ctx, field) case "events": @@ -1976,6 +1922,73 @@ func (ec *executionContext) fieldContext_Mutation_createPortfolio(ctx context.Co return fc, nil } +func (ec *executionContext) _Mutation_updatePortfolio(ctx context.Context, field graphql.CollectedField) (ret graphql.Marshaler) { + fc, err := ec.fieldContext_Mutation_updatePortfolio(ctx, field) + if err != nil { + return graphql.Null + } + ctx = graphql.WithFieldContext(ctx, fc) + defer func() { + if r := recover(); r != nil { + ec.Error(ctx, ec.Recover(ctx, r)) + ret = graphql.Null + } + }() + resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (any, error) { + ctx = rctx // use context from middleware stack in children + return ec.resolvers.Mutation().UpdatePortfolio(rctx, fc.Args["id"].(string), fc.Args["input"].(models.PortfolioInput)) + }) + if err != nil { + ec.Error(ctx, err) + return graphql.Null + } + if resTmp == nil { + if !graphql.HasFieldError(ctx, fc) { + ec.Errorf(ctx, "must not be null") + } + return graphql.Null + } + res := resTmp.(*persistence.Portfolio) + fc.Result = res + return ec.marshalNPortfolio2ᚖgithubᚗcomᚋoxistoᚋmoneyᚑgopherᚋpersistenceᚐPortfolio(ctx, field.Selections, res) +} + +func (ec *executionContext) fieldContext_Mutation_updatePortfolio(ctx context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { + fc = &graphql.FieldContext{ + Object: "Mutation", + Field: field, + IsMethod: true, + IsResolver: true, + Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + switch field.Name { + case "id": + return ec.fieldContext_Portfolio_id(ctx, field) + case "displayName": + return ec.fieldContext_Portfolio_displayName(ctx, field) + case "accounts": + return ec.fieldContext_Portfolio_accounts(ctx, field) + case "snapshot": + return ec.fieldContext_Portfolio_snapshot(ctx, field) + case "events": + return ec.fieldContext_Portfolio_events(ctx, field) + } + return nil, fmt.Errorf("no field named %q was found under type Portfolio", field.Name) + }, + } + defer func() { + if r := recover(); r != nil { + err = ec.Recover(ctx, r) + ec.Error(ctx, err) + } + }() + ctx = graphql.WithFieldContext(ctx, fc) + if fc.Args, err = ec.field_Mutation_updatePortfolio_args(ctx, field.ArgumentMap(ec.Variables)); err != nil { + ec.Error(ctx, err) + return fc, err + } + return fc, nil +} + func (ec *executionContext) _Mutation_createAccount(ctx context.Context, field graphql.CollectedField) (ret graphql.Marshaler) { fc, err := ec.fieldContext_Mutation_createAccount(ctx, field) if err != nil { @@ -2261,8 +2274,8 @@ func (ec *executionContext) fieldContext_Portfolio_displayName(_ context.Context return fc, nil } -func (ec *executionContext) _Portfolio_bankAccount(ctx context.Context, field graphql.CollectedField, obj *persistence.Portfolio) (ret graphql.Marshaler) { - fc, err := ec.fieldContext_Portfolio_bankAccount(ctx, field) +func (ec *executionContext) _Portfolio_accounts(ctx context.Context, field graphql.CollectedField, obj *persistence.Portfolio) (ret graphql.Marshaler) { + fc, err := ec.fieldContext_Portfolio_accounts(ctx, field) if err != nil { return graphql.Null } @@ -2275,7 +2288,7 @@ func (ec *executionContext) _Portfolio_bankAccount(ctx context.Context, field gr }() resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (any, error) { ctx = rctx // use context from middleware stack in children - return ec.resolvers.Portfolio().BankAccount(rctx, obj) + return ec.resolvers.Portfolio().Accounts(rctx, obj) }) if err != nil { ec.Error(ctx, err) @@ -2287,12 +2300,12 @@ func (ec *executionContext) _Portfolio_bankAccount(ctx context.Context, field gr } return graphql.Null } - res := resTmp.(*persistence.BankAccount) + res := resTmp.([]*persistence.Account) fc.Result = res - return ec.marshalNBankAccount2ᚖgithubᚗcomᚋoxistoᚋmoneyᚑgopherᚋpersistenceᚐBankAccount(ctx, field.Selections, res) + return ec.marshalNAccount2ᚕᚖgithubᚗcomᚋoxistoᚋmoneyᚑgopherᚋpersistenceᚐAccountᚄ(ctx, field.Selections, res) } -func (ec *executionContext) fieldContext_Portfolio_bankAccount(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { +func (ec *executionContext) fieldContext_Portfolio_accounts(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { fc = &graphql.FieldContext{ Object: "Portfolio", Field: field, @@ -2301,11 +2314,15 @@ func (ec *executionContext) fieldContext_Portfolio_bankAccount(_ context.Context Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { switch field.Name { case "id": - return ec.fieldContext_BankAccount_id(ctx, field) + return ec.fieldContext_Account_id(ctx, field) case "displayName": - return ec.fieldContext_BankAccount_displayName(ctx, field) + return ec.fieldContext_Account_displayName(ctx, field) + case "type": + return ec.fieldContext_Account_type(ctx, field) + case "referenceAccount": + return ec.fieldContext_Account_referenceAccount(ctx, field) } - return nil, fmt.Errorf("no field named %q was found under type BankAccount", field.Name) + return nil, fmt.Errorf("no field named %q was found under type Account", field.Name) }, } return fc, nil @@ -2409,9 +2426,9 @@ func (ec *executionContext) _Portfolio_events(ctx context.Context, field graphql } return graphql.Null } - res := resTmp.([]*persistence.PortfolioEvent) + res := resTmp.([]*models.PortfolioEvent) fc.Result = res - return ec.marshalNPortfolioEvent2ᚕᚖgithubᚗcomᚋoxistoᚋmoneyᚑgopherᚋpersistenceᚐPortfolioEventᚄ(ctx, field.Selections, res) + return ec.marshalNPortfolioEvent2ᚕᚖgithubᚗcomᚋoxistoᚋmoneyᚑgopherᚋmodelsᚐPortfolioEventᚄ(ctx, field.Selections, res) } func (ec *executionContext) fieldContext_Portfolio_events(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { @@ -2435,7 +2452,7 @@ func (ec *executionContext) fieldContext_Portfolio_events(_ context.Context, fie return fc, nil } -func (ec *executionContext) _PortfolioEvent_time(ctx context.Context, field graphql.CollectedField, obj *persistence.PortfolioEvent) (ret graphql.Marshaler) { +func (ec *executionContext) _PortfolioEvent_time(ctx context.Context, field graphql.CollectedField, obj *models.PortfolioEvent) (ret graphql.Marshaler) { fc, err := ec.fieldContext_PortfolioEvent_time(ctx, field) if err != nil { return graphql.Null @@ -2449,7 +2466,7 @@ func (ec *executionContext) _PortfolioEvent_time(ctx context.Context, field grap }() resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (any, error) { ctx = rctx // use context from middleware stack in children - return ec.resolvers.PortfolioEvent().Time(rctx, obj) + return obj.Time, nil }) if err != nil { ec.Error(ctx, err) @@ -2470,8 +2487,8 @@ func (ec *executionContext) fieldContext_PortfolioEvent_time(_ context.Context, fc = &graphql.FieldContext{ Object: "PortfolioEvent", Field: field, - IsMethod: true, - IsResolver: true, + IsMethod: false, + IsResolver: false, Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { return nil, errors.New("field of type Date does not have child fields") }, @@ -2479,7 +2496,7 @@ func (ec *executionContext) fieldContext_PortfolioEvent_time(_ context.Context, return fc, nil } -func (ec *executionContext) _PortfolioEvent_type(ctx context.Context, field graphql.CollectedField, obj *persistence.PortfolioEvent) (ret graphql.Marshaler) { +func (ec *executionContext) _PortfolioEvent_type(ctx context.Context, field graphql.CollectedField, obj *models.PortfolioEvent) (ret graphql.Marshaler) { fc, err := ec.fieldContext_PortfolioEvent_type(ctx, field) if err != nil { return graphql.Null @@ -2523,7 +2540,7 @@ func (ec *executionContext) fieldContext_PortfolioEvent_type(_ context.Context, return fc, nil } -func (ec *executionContext) _PortfolioEvent_security(ctx context.Context, field graphql.CollectedField, obj *persistence.PortfolioEvent) (ret graphql.Marshaler) { +func (ec *executionContext) _PortfolioEvent_security(ctx context.Context, field graphql.CollectedField, obj *models.PortfolioEvent) (ret graphql.Marshaler) { fc, err := ec.fieldContext_PortfolioEvent_security(ctx, field) if err != nil { return graphql.Null @@ -2537,7 +2554,7 @@ func (ec *executionContext) _PortfolioEvent_security(ctx context.Context, field }() resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (any, error) { ctx = rctx // use context from middleware stack in children - return ec.resolvers.PortfolioEvent().Security(rctx, obj) + return obj.Security, nil }) if err != nil { ec.Error(ctx, err) @@ -2555,8 +2572,8 @@ func (ec *executionContext) fieldContext_PortfolioEvent_security(_ context.Conte fc = &graphql.FieldContext{ Object: "PortfolioEvent", Field: field, - IsMethod: true, - IsResolver: true, + IsMethod: false, + IsResolver: false, Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { switch field.Name { case "id": @@ -3615,8 +3632,8 @@ func (ec *executionContext) fieldContext_Query_portfolio(ctx context.Context, fi return ec.fieldContext_Portfolio_id(ctx, field) case "displayName": return ec.fieldContext_Portfolio_displayName(ctx, field) - case "bankAccount": - return ec.fieldContext_Portfolio_bankAccount(ctx, field) + case "accounts": + return ec.fieldContext_Portfolio_accounts(ctx, field) case "snapshot": return ec.fieldContext_Portfolio_snapshot(ctx, field) case "events": @@ -3682,8 +3699,8 @@ func (ec *executionContext) fieldContext_Query_portfolios(_ context.Context, fie return ec.fieldContext_Portfolio_id(ctx, field) case "displayName": return ec.fieldContext_Portfolio_displayName(ctx, field) - case "bankAccount": - return ec.fieldContext_Portfolio_bankAccount(ctx, field) + case "accounts": + return ec.fieldContext_Portfolio_accounts(ctx, field) case "snapshot": return ec.fieldContext_Portfolio_snapshot(ctx, field) case "events": @@ -6398,7 +6415,7 @@ func (ec *executionContext) unmarshalInputPortfolioInput(ctx context.Context, ob asMap[k] = v } - fieldsInOrder := [...]string{"id", "displayName"} + fieldsInOrder := [...]string{"id", "displayName", "accountIds"} for _, k := range fieldsInOrder { v, ok := asMap[k] if !ok { @@ -6419,6 +6436,13 @@ func (ec *executionContext) unmarshalInputPortfolioInput(ctx context.Context, ob return it, err } it.DisplayName = data + case "accountIds": + ctx := graphql.WithPathContext(ctx, graphql.NewPathWithField("accountIds")) + data, err := ec.unmarshalNString2ᚕstringᚄ(ctx, v) + if err != nil { + return it, err + } + it.AccountIds = data } } @@ -6556,50 +6580,6 @@ func (ec *executionContext) _Account(ctx context.Context, sel ast.SelectionSet, return out } -var bankAccountImplementors = []string{"BankAccount"} - -func (ec *executionContext) _BankAccount(ctx context.Context, sel ast.SelectionSet, obj *persistence.BankAccount) graphql.Marshaler { - fields := graphql.CollectFields(ec.OperationContext, sel, bankAccountImplementors) - - out := graphql.NewFieldSet(fields) - deferred := make(map[string]*graphql.FieldSet) - for i, field := range fields { - switch field.Name { - case "__typename": - out.Values[i] = graphql.MarshalString("BankAccount") - case "id": - out.Values[i] = ec._BankAccount_id(ctx, field, obj) - if out.Values[i] == graphql.Null { - out.Invalids++ - } - case "displayName": - out.Values[i] = ec._BankAccount_displayName(ctx, field, obj) - if out.Values[i] == graphql.Null { - out.Invalids++ - } - default: - panic("unknown field " + strconv.Quote(field.Name)) - } - } - out.Dispatch(ctx) - if out.Invalids > 0 { - return graphql.Null - } - - atomic.AddInt32(&ec.deferred, int32(len(deferred))) - - for label, dfs := range deferred { - ec.processDeferredGroup(graphql.DeferredGroup{ - Label: label, - Path: graphql.GetPath(ctx), - FieldSet: dfs, - Context: ctx, - }) - } - - return out -} - var currencyImplementors = []string{"Currency"} func (ec *executionContext) _Currency(ctx context.Context, sel ast.SelectionSet, obj *currency.Currency) graphql.Marshaler { @@ -6799,6 +6779,13 @@ func (ec *executionContext) _Mutation(ctx context.Context, sel ast.SelectionSet) if out.Values[i] == graphql.Null { out.Invalids++ } + case "updatePortfolio": + out.Values[i] = ec.OperationContext.RootResolverMiddleware(innerCtx, func(ctx context.Context) (res graphql.Marshaler) { + return ec._Mutation_updatePortfolio(ctx, field) + }) + if out.Values[i] == graphql.Null { + out.Invalids++ + } case "createAccount": out.Values[i] = ec.OperationContext.RootResolverMiddleware(innerCtx, func(ctx context.Context) (res graphql.Marshaler) { return ec._Mutation_createAccount(ctx, field) @@ -6864,7 +6851,7 @@ func (ec *executionContext) _Portfolio(ctx context.Context, sel ast.SelectionSet if out.Values[i] == graphql.Null { atomic.AddUint32(&out.Invalids, 1) } - case "bankAccount": + case "accounts": field := field innerFunc := func(ctx context.Context, fs *graphql.FieldSet) (res graphql.Marshaler) { @@ -6873,7 +6860,7 @@ func (ec *executionContext) _Portfolio(ctx context.Context, sel ast.SelectionSet ec.Error(ctx, ec.Recover(ctx, r)) } }() - res = ec._Portfolio_bankAccount(ctx, field, obj) + res = ec._Portfolio_accounts(ctx, field, obj) if res == graphql.Null { atomic.AddUint32(&fs.Invalids, 1) } @@ -6994,7 +6981,7 @@ func (ec *executionContext) _Portfolio(ctx context.Context, sel ast.SelectionSet var portfolioEventImplementors = []string{"PortfolioEvent"} -func (ec *executionContext) _PortfolioEvent(ctx context.Context, sel ast.SelectionSet, obj *persistence.PortfolioEvent) graphql.Marshaler { +func (ec *executionContext) _PortfolioEvent(ctx context.Context, sel ast.SelectionSet, obj *models.PortfolioEvent) graphql.Marshaler { fields := graphql.CollectFields(ec.OperationContext, sel, portfolioEventImplementors) out := graphql.NewFieldSet(fields) @@ -7004,79 +6991,17 @@ func (ec *executionContext) _PortfolioEvent(ctx context.Context, sel ast.Selecti case "__typename": out.Values[i] = graphql.MarshalString("PortfolioEvent") case "time": - field := field - - innerFunc := func(ctx context.Context, fs *graphql.FieldSet) (res graphql.Marshaler) { - defer func() { - if r := recover(); r != nil { - ec.Error(ctx, ec.Recover(ctx, r)) - } - }() - res = ec._PortfolioEvent_time(ctx, field, obj) - if res == graphql.Null { - atomic.AddUint32(&fs.Invalids, 1) - } - return res - } - - if field.Deferrable != nil { - dfs, ok := deferred[field.Deferrable.Label] - di := 0 - if ok { - dfs.AddField(field) - di = len(dfs.Values) - 1 - } else { - dfs = graphql.NewFieldSet([]graphql.CollectedField{field}) - deferred[field.Deferrable.Label] = dfs - } - dfs.Concurrently(di, func(ctx context.Context) graphql.Marshaler { - return innerFunc(ctx, dfs) - }) - - // don't run the out.Concurrently() call below - out.Values[i] = graphql.Null - continue + out.Values[i] = ec._PortfolioEvent_time(ctx, field, obj) + if out.Values[i] == graphql.Null { + out.Invalids++ } - - out.Concurrently(i, func(ctx context.Context) graphql.Marshaler { return innerFunc(ctx, out) }) case "type": out.Values[i] = ec._PortfolioEvent_type(ctx, field, obj) if out.Values[i] == graphql.Null { - atomic.AddUint32(&out.Invalids, 1) + out.Invalids++ } case "security": - field := field - - innerFunc := func(ctx context.Context, _ *graphql.FieldSet) (res graphql.Marshaler) { - defer func() { - if r := recover(); r != nil { - ec.Error(ctx, ec.Recover(ctx, r)) - } - }() - res = ec._PortfolioEvent_security(ctx, field, obj) - return res - } - - if field.Deferrable != nil { - dfs, ok := deferred[field.Deferrable.Label] - di := 0 - if ok { - dfs.AddField(field) - di = len(dfs.Values) - 1 - } else { - dfs = graphql.NewFieldSet([]graphql.CollectedField{field}) - deferred[field.Deferrable.Label] = dfs - } - dfs.Concurrently(di, func(ctx context.Context) graphql.Marshaler { - return innerFunc(ctx, dfs) - }) - - // don't run the out.Concurrently() call below - out.Values[i] = graphql.Null - continue - } - - out.Concurrently(i, func(ctx context.Context) graphql.Marshaler { return innerFunc(ctx, out) }) + out.Values[i] = ec._PortfolioEvent_security(ctx, field, obj) default: panic("unknown field " + strconv.Quote(field.Name)) } @@ -8171,20 +8096,6 @@ var ( } ) -func (ec *executionContext) marshalNBankAccount2githubᚗcomᚋoxistoᚋmoneyᚑgopherᚋpersistenceᚐBankAccount(ctx context.Context, sel ast.SelectionSet, v persistence.BankAccount) graphql.Marshaler { - return ec._BankAccount(ctx, sel, &v) -} - -func (ec *executionContext) marshalNBankAccount2ᚖgithubᚗcomᚋoxistoᚋmoneyᚑgopherᚋpersistenceᚐBankAccount(ctx context.Context, sel ast.SelectionSet, v *persistence.BankAccount) graphql.Marshaler { - if v == nil { - if !graphql.HasFieldError(ctx, graphql.GetFieldContext(ctx)) { - ec.Errorf(ctx, "the requested element is null which the schema does not allow") - } - return graphql.Null - } - return ec._BankAccount(ctx, sel, v) -} - func (ec *executionContext) unmarshalNBoolean2bool(ctx context.Context, v any) (bool, error) { res, err := graphql.UnmarshalBoolean(v) return res, graphql.ErrorOnPath(ctx, err) @@ -8381,7 +8292,7 @@ func (ec *executionContext) marshalNPortfolio2ᚖgithubᚗcomᚋoxistoᚋmoney return ec._Portfolio(ctx, sel, v) } -func (ec *executionContext) marshalNPortfolioEvent2ᚕᚖgithubᚗcomᚋoxistoᚋmoneyᚑgopherᚋpersistenceᚐPortfolioEventᚄ(ctx context.Context, sel ast.SelectionSet, v []*persistence.PortfolioEvent) graphql.Marshaler { +func (ec *executionContext) marshalNPortfolioEvent2ᚕᚖgithubᚗcomᚋoxistoᚋmoneyᚑgopherᚋmodelsᚐPortfolioEventᚄ(ctx context.Context, sel ast.SelectionSet, v []*models.PortfolioEvent) graphql.Marshaler { ret := make(graphql.Array, len(v)) var wg sync.WaitGroup isLen1 := len(v) == 1 @@ -8405,7 +8316,7 @@ func (ec *executionContext) marshalNPortfolioEvent2ᚕᚖgithubᚗcomᚋoxisto if !isLen1 { defer wg.Done() } - ret[i] = ec.marshalNPortfolioEvent2ᚖgithubᚗcomᚋoxistoᚋmoneyᚑgopherᚋpersistenceᚐPortfolioEvent(ctx, sel, v[i]) + ret[i] = ec.marshalNPortfolioEvent2ᚖgithubᚗcomᚋoxistoᚋmoneyᚑgopherᚋmodelsᚐPortfolioEvent(ctx, sel, v[i]) } if isLen1 { f(i) @@ -8425,7 +8336,7 @@ func (ec *executionContext) marshalNPortfolioEvent2ᚕᚖgithubᚗcomᚋoxisto return ret } -func (ec *executionContext) marshalNPortfolioEvent2ᚖgithubᚗcomᚋoxistoᚋmoneyᚑgopherᚋpersistenceᚐPortfolioEvent(ctx context.Context, sel ast.SelectionSet, v *persistence.PortfolioEvent) graphql.Marshaler { +func (ec *executionContext) marshalNPortfolioEvent2ᚖgithubᚗcomᚋoxistoᚋmoneyᚑgopherᚋmodelsᚐPortfolioEvent(ctx context.Context, sel ast.SelectionSet, v *models.PortfolioEvent) graphql.Marshaler { if v == nil { if !graphql.HasFieldError(ctx, graphql.GetFieldContext(ctx)) { ec.Errorf(ctx, "the requested element is null which the schema does not allow") @@ -8601,6 +8512,38 @@ func (ec *executionContext) marshalNString2string(ctx context.Context, sel ast.S return res } +func (ec *executionContext) unmarshalNString2ᚕstringᚄ(ctx context.Context, v any) ([]string, error) { + var vSlice []any + if v != nil { + vSlice = graphql.CoerceList(v) + } + var err error + res := make([]string, len(vSlice)) + for i := range vSlice { + ctx := graphql.WithPathContext(ctx, graphql.NewPathWithIndex(i)) + res[i], err = ec.unmarshalNString2string(ctx, vSlice[i]) + if err != nil { + return nil, err + } + } + return res, nil +} + +func (ec *executionContext) marshalNString2ᚕstringᚄ(ctx context.Context, sel ast.SelectionSet, v []string) graphql.Marshaler { + ret := make(graphql.Array, len(v)) + for i := range v { + ret[i] = ec.marshalNString2string(ctx, sel, v[i]) + } + + for _, e := range ret { + if e == graphql.Null { + return graphql.Null + } + } + + return ret +} + func (ec *executionContext) marshalNTransaction2ᚕᚖgithubᚗcomᚋoxistoᚋmoneyᚑgopherᚋpersistenceᚐTransactionᚄ(ctx context.Context, sel ast.SelectionSet, v []*persistence.Transaction) graphql.Marshaler { ret := make(graphql.Array, len(v)) var wg sync.WaitGroup @@ -8915,13 +8858,6 @@ func (ec *executionContext) marshalOAccount2ᚖgithubᚗcomᚋoxistoᚋmoneyᚑg return ec._Account(ctx, sel, v) } -func (ec *executionContext) marshalOBankAccount2ᚖgithubᚗcomᚋoxistoᚋmoneyᚑgopherᚋpersistenceᚐBankAccount(ctx context.Context, sel ast.SelectionSet, v *persistence.BankAccount) graphql.Marshaler { - if v == nil { - return graphql.Null - } - return ec._BankAccount(ctx, sel, v) -} - func (ec *executionContext) unmarshalOBoolean2bool(ctx context.Context, v any) (bool, error) { res, err := graphql.UnmarshalBoolean(v) return res, graphql.ErrorOnPath(ctx, err) diff --git a/graph/schema.graphqls b/graph/schema.graphqls index e9b2d61e..80beeddd 100644 --- a/graph/schema.graphqls +++ b/graph/schema.graphqls @@ -30,7 +30,7 @@ type ListedSecurity { type Portfolio { id: String! displayName: String! - bankAccount: BankAccount! + accounts: [Account!]! snapshot(when: Date!): PortfolioSnapshot events: [PortfolioEvent!]! } @@ -144,16 +144,11 @@ type PortfolioPosition { gains: Float! } -type BankAccount { - id: String! - displayName: String! -} - type Account { id: String! displayName: String! type: AccountType! - referenceAccount: BankAccount + referenceAccount: Account } input SecurityInput { @@ -165,6 +160,7 @@ input SecurityInput { input PortfolioInput { id: String! displayName: String! + accountIds: [String!]! } input AccountInput { @@ -183,6 +179,7 @@ type Mutation { updateSecurity(id: ID!, input: SecurityInput!): Security! createPortfolio(input: PortfolioInput!): Portfolio! + updatePortfolio(id: ID!, input: PortfolioInput!): Portfolio! createAccount(input: AccountInput!): Account! deleteAccount(id: String!): Account! diff --git a/graph/schema.resolvers.go b/graph/schema.resolvers.go index 7ee58973..9bcfc5b8 100644 --- a/graph/schema.resolvers.go +++ b/graph/schema.resolvers.go @@ -17,7 +17,7 @@ import ( ) // ReferenceAccount is the resolver for the referenceAccount field. -func (r *accountResolver) ReferenceAccount(ctx context.Context, obj *persistence.Account) (*persistence.BankAccount, error) { +func (r *accountResolver) ReferenceAccount(ctx context.Context, obj *persistence.Account) (*persistence.Account, error) { panic(fmt.Errorf("not implemented: ReferenceAccount - referenceAccount")) } @@ -108,7 +108,52 @@ func (r *mutationResolver) UpdateSecurity(ctx context.Context, id string, input // CreatePortfolio is the resolver for the createPortfolio field. func (r *mutationResolver) CreatePortfolio(ctx context.Context, input models.PortfolioInput) (*persistence.Portfolio, error) { - panic(fmt.Errorf("not implemented: CreatePortfolio - createPortfolio")) + return withTx(r.Resolver, func(qtx *persistence.Queries) (*persistence.Portfolio, error) { + sec, err := qtx.CreatePortfolio(ctx, persistence.CreatePortfolioParams{ + ID: input.ID, + DisplayName: input.DisplayName, + }) + if err != nil { + return nil, err + } + + for _, accountID := range input.AccountIds { + err = qtx.AddAccountToPortfolio(ctx, persistence.AddAccountToPortfolioParams{ + PortfolioID: input.ID, + AccountID: accountID, + }) + if err != nil { + return nil, err + } + } + + return sec, nil + }) +} + +// UpdatePortfolio is the resolver for the updatePortfolio field. +func (r *mutationResolver) UpdatePortfolio(ctx context.Context, id string, input models.PortfolioInput) (*persistence.Portfolio, error) { + return withTx(r.Resolver, func(qtx *persistence.Queries) (*persistence.Portfolio, error) { + sec, err := qtx.UpdatePortfolio(ctx, persistence.UpdatePortfolioParams{ + ID: input.ID, + DisplayName: input.DisplayName, + }) + if err != nil { + return nil, err + } + + for _, accountID := range input.AccountIds { + err = qtx.AddAccountToPortfolio(ctx, persistence.AddAccountToPortfolioParams{ + PortfolioID: input.ID, + AccountID: accountID, + }) + if err != nil { + return nil, err + } + } + + return sec, nil + }) } // CreateAccount is the resolver for the createAccount field. @@ -135,9 +180,9 @@ func (r *mutationResolver) TriggerQuoteUpdate(ctx context.Context, securityIDs [ return } -// BankAccount is the resolver for the bankAccount field. -func (r *portfolioResolver) BankAccount(ctx context.Context, obj *persistence.Portfolio) (*persistence.BankAccount, error) { - return r.DB.GetBankAccount(ctx, obj.BankAccountID) +// Accounts is the resolver for the accounts field. +func (r *portfolioResolver) Accounts(ctx context.Context, obj *persistence.Portfolio) ([]*persistence.Account, error) { + return r.DB.ListAccountsByPortfolioID(ctx, obj.ID) } // Snapshot is the resolver for the snapshot field. @@ -157,20 +202,10 @@ func (r *portfolioResolver) Snapshot(ctx context.Context, obj *persistence.Portf } // Events is the resolver for the events field. -func (r *portfolioResolver) Events(ctx context.Context, obj *persistence.Portfolio) ([]*persistence.PortfolioEvent, error) { +func (r *portfolioResolver) Events(ctx context.Context, obj *persistence.Portfolio) ([]*models.PortfolioEvent, error) { panic(fmt.Errorf("not implemented: Events - events")) } -// Time is the resolver for the time field. -func (r *portfolioEventResolver) Time(ctx context.Context, obj *persistence.PortfolioEvent) (string, error) { - panic(fmt.Errorf("not implemented: Time - time")) -} - -// Security is the resolver for the security field. -func (r *portfolioEventResolver) Security(ctx context.Context, obj *persistence.PortfolioEvent) (*persistence.Security, error) { - panic(fmt.Errorf("not implemented: Security - security")) -} - // Security is the resolver for the security field. func (r *queryResolver) Security(ctx context.Context, id string) (*persistence.Security, error) { return r.DB.GetSecurity(ctx, id) @@ -252,9 +287,6 @@ func (r *Resolver) Mutation() MutationResolver { return &mutationResolver{r} } // Portfolio returns PortfolioResolver implementation. func (r *Resolver) Portfolio() PortfolioResolver { return &portfolioResolver{r} } -// PortfolioEvent returns PortfolioEventResolver implementation. -func (r *Resolver) PortfolioEvent() PortfolioEventResolver { return &portfolioEventResolver{r} } - // Query returns QueryResolver implementation. func (r *Resolver) Query() QueryResolver { return &queryResolver{r} } @@ -268,7 +300,6 @@ type accountResolver struct{ *Resolver } type listedSecurityResolver struct{ *Resolver } type mutationResolver struct{ *Resolver } type portfolioResolver struct{ *Resolver } -type portfolioEventResolver struct{ *Resolver } type queryResolver struct{ *Resolver } type securityResolver struct{ *Resolver } type transactionResolver struct{ *Resolver } diff --git a/graph/schema.resolvers_test.go b/graph/schema.resolvers_test.go index c703ca9d..076f8ac3 100644 --- a/graph/schema.resolvers_test.go +++ b/graph/schema.resolvers_test.go @@ -212,3 +212,69 @@ func Test_queryResolver_Transactions(t *testing.T) { }) } } + +func Test_mutationResolver_CreatePortfolio(t *testing.T) { + type fields struct { + Resolver *Resolver + } + type args struct { + ctx context.Context + input models.PortfolioInput + } + tests := []struct { + name string + fields fields + args args + want *persistence.Portfolio + wantErr bool + }{ + { + name: "happy path", + fields: fields{ + Resolver: &Resolver{ + DB: persistencetest.NewTestDB(t, func(db *persistence.DB) { + _, err := db.Queries.CreateAccount(context.Background(), testdata.TestCreateBankAccountParams) + assert.NoError(t, err) + + _, err = db.Queries.CreateAccount(context.Background(), testdata.TestCreateBrokerageAccountParams) + assert.NoError(t, err) + + _, err = db.Queries.CreateTransaction(context.Background(), testdata.TestCreateBuyTransactionParams) + assert.NoError(t, err) + }), + }, + }, + args: args{ + ctx: context.TODO(), + input: models.PortfolioInput{ + ID: "mybank/myportfolio", + DisplayName: "My Portfolio", + AccountIds: []string{ + testdata.TestCreateBankAccountParams.ID, + testdata.TestCreateBrokerageAccountParams.ID, + }, + }, + }, + want: &persistence.Portfolio{ + ID: "mybank/myportfolio", + DisplayName: "My Portfolio", + }, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + r := &mutationResolver{ + Resolver: tt.fields.Resolver, + } + got, err := r.CreatePortfolio(tt.args.ctx, tt.args.input) + if (err != nil) != tt.wantErr { + t.Errorf("mutationResolver.CreatePortfolio() error = %v, wantErr %v", err, tt.wantErr) + return + } + if !reflect.DeepEqual(got, tt.want) { + t.Errorf("mutationResolver.CreatePortfolio() = %v, want %v", got, tt.want) + } + }) + } +} diff --git a/import/csv/csv_importer.go b/import/csv/csv_importer.go index 5b6a7fa0..52302ac0 100644 --- a/import/csv/csv_importer.go +++ b/import/csv/csv_importer.go @@ -60,8 +60,12 @@ var ( // Import imports CSV records from a [io.Reader] containing portfolio // transactions. -func Import(r io.Reader, pname string) ( - txs []*persistence.PortfolioEvent, +func Import( + r io.Reader, + bankAccountID string, + brokerageAccountID string, +) ( + txs []*persistence.Transaction, secs []*persistence.Security, lss []*persistence.ListedSecurity, ) { @@ -73,7 +77,7 @@ func Import(r io.Reader, pname string) ( // Read until EOF for { - tx, sec, ls, err := readLine(cr, pname) + tx, sec, ls, err := readLine(cr, bankAccountID, brokerageAccountID) if errors.Is(err, io.EOF) { break } else if err != nil { @@ -98,8 +102,12 @@ func Import(r io.Reader, pname string) ( return } -func readLine(cr *csv.Reader, pname string) ( - tx *persistence.PortfolioEvent, +func readLine( + cr *csv.Reader, + bankAccountID string, + brokerageAccountID string, +) ( + tx *persistence.Transaction, sec *persistence.Security, ls []*persistence.ListedSecurity, err error) { @@ -113,7 +121,7 @@ func readLine(cr *csv.Reader, pname string) ( return nil, nil, nil, fmt.Errorf("%w: %w", ErrReadingCSV, err) } - tx = new(persistence.PortfolioEvent) + tx = new(persistence.Transaction) tx.Time, err = txTime(record[0]) if err != nil { return nil, nil, nil, fmt.Errorf("%w: %w", ErrParsingTime, err) @@ -148,9 +156,13 @@ func readLine(cr *csv.Reader, pname string) ( if tx.Type == events.PortfolioEventTypeBuy || tx.Type == events.PortfolioEventTypeDeliveryInbound { tx.Price = currency.Divide(currency.Minus(value, tx.Fees), tx.Amount) + tx.SourceAccountID = sql.NullString{String: bankAccountID, Valid: true} + tx.DestinationAccountID = sql.NullString{String: brokerageAccountID, Valid: true} } else if tx.Type == events.PortfolioEventTypeSell || tx.Type == events.PortfolioEventTypeDeliveryOutbound { tx.Price = currency.Times(currency.Divide(currency.Minus(currency.Minus(value, tx.Fees), tx.Taxes), tx.Amount), -1) + tx.SourceAccountID = sql.NullString{String: brokerageAccountID, Valid: true} + tx.DestinationAccountID = sql.NullString{String: bankAccountID, Valid: true} } sec = new(persistence.Security) @@ -170,8 +182,7 @@ func readLine(cr *csv.Reader, pname string) ( sec.QuoteProvider = sql.NullString{String: quote.QuoteProviderING, Valid: true} } - tx.PortfolioID = pname - tx.SecurityID = sec.ID + tx.SecurityID = sql.NullString{String: sec.ID, Valid: true} tx.MakeUniqueID() return diff --git a/import/csv/csv_importer_test.go b/import/csv/csv_importer_test.go index d8d6424b..987af397 100644 --- a/import/csv/csv_importer_test.go +++ b/import/csv/csv_importer_test.go @@ -25,6 +25,7 @@ import ( "time" "github.com/oxisto/money-gopher/currency" + "github.com/oxisto/money-gopher/internal/testdata" "github.com/oxisto/money-gopher/persistence" "github.com/oxisto/money-gopher/portfolio/events" "github.com/oxisto/money-gopher/securities/quote" @@ -34,13 +35,14 @@ import ( func TestImport(t *testing.T) { type args struct { - r io.Reader - pname string + r io.Reader + bankAccountID string + brokerageAccountID string } tests := []struct { name string args args - wantTxs assert.Want[[]*persistence.PortfolioEvent] + wantTxs assert.Want[[]*persistence.Transaction] wantSecs assert.Want[[]*persistence.Security] wantLss assert.Want[[]*persistence.ListedSecurity] }{ @@ -51,8 +53,10 @@ func TestImport(t *testing.T) { 2021-06-05T00:00;Buy;2.151,85;EUR;;;;10,25;0,00;20;US0378331005;865985;APC.F;Apple Inc.; 2021-06-05T00:00;Sell;-2.151,85;EUR;;;;10,25;0,00;20;US0378331005;865985;APC.F;Apple Inc.; 2021-06-18T00:00;Delivery (Inbound);912,66;EUR;;;;7,16;0,00;5;US09075V1026;A2PSR2;22UA.F;BioNTech SE;`)), + bankAccountID: testdata.TestBankAccount.ID, + brokerageAccountID: testdata.TestBrokerageAccount.ID, }, - wantTxs: func(t *testing.T, txs []*persistence.PortfolioEvent) bool { + wantTxs: func(t *testing.T, txs []*persistence.Transaction) bool { return assert.Equals(t, 3, len(txs)) }, wantSecs: func(t *testing.T, secs []*persistence.Security) bool { @@ -68,7 +72,7 @@ func TestImport(t *testing.T) { r: bytes.NewReader([]byte(`Date;Type;Value;Transaction Currency;Gross Amount;Currency Gross Amount;Exchange Rate;Fees;Taxes;Shares;ISIN;WKN;Ticker Symbol;Security Name;Note this;will;be;an;error`)), }, - wantTxs: func(t *testing.T, txs []*persistence.PortfolioEvent) bool { + wantTxs: func(t *testing.T, txs []*persistence.Transaction) bool { return assert.Equals(t, 0, len(txs)) }, wantSecs: func(t *testing.T, secs []*persistence.Security) bool { @@ -81,7 +85,7 @@ this;will;be;an;error`)), } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - gotTxs, gotSecs, gotLss := Import(tt.args.r, tt.args.pname) + gotTxs, gotSecs, gotLss := Import(tt.args.r, tt.args.bankAccountID, tt.args.brokerageAccountID) tt.wantTxs(t, gotTxs) tt.wantSecs(t, gotSecs) tt.wantLss(t, gotLss) @@ -91,13 +95,14 @@ this;will;be;an;error`)), func Test_readLine(t *testing.T) { type args struct { - cr *csv.Reader - pname string + cr *csv.Reader + bankAccountID string + brokerageAccountID string } tests := []struct { name string args args - wantTx *persistence.PortfolioEvent + wantTx *persistence.Transaction wantSec *persistence.Security wantLs []*persistence.ListedSecurity wantErr assert.Want[error] @@ -110,16 +115,20 @@ func Test_readLine(t *testing.T) { cr.Comma = ';' return cr }(), - }, - wantTx: &persistence.PortfolioEvent{ - ID: "9e7b470b7566beca", - SecurityID: "US0378331005", - Type: events.PortfolioEventTypeBuy, - Time: time.Date(2020, 1, 1, 0, 0, 0, 0, time.Local), - Amount: 20, - Fees: currency.Value(1025), - Taxes: currency.Zero(), - Price: currency.Value(10708), + bankAccountID: testdata.TestBankAccount.ID, + brokerageAccountID: testdata.TestBrokerageAccount.ID, + }, + wantTx: &persistence.Transaction{ + ID: "670240c1f8373a3f", + SecurityID: sql.NullString{String: "US0378331005", Valid: true}, + Type: events.PortfolioEventTypeBuy, + SourceAccountID: sql.NullString{String: testdata.TestBankAccount.ID, Valid: true}, + DestinationAccountID: sql.NullString{String: testdata.TestBrokerageAccount.ID, Valid: true}, + Time: time.Date(2020, 1, 1, 0, 0, 0, 0, time.Local), + Amount: 20, + Fees: currency.Value(1025), + Taxes: currency.Zero(), + Price: currency.Value(10708), }, wantSec: &persistence.Security{ ID: "US0378331005", @@ -142,16 +151,20 @@ func Test_readLine(t *testing.T) { cr.Comma = ';' return cr }(), - }, - wantTx: &persistence.PortfolioEvent{ - ID: "1070dafc882785a0", - SecurityID: "US00827B1061", - Type: events.PortfolioEventTypeBuy, - Time: time.Date(2022, 1, 1, 9, 0, 0, 0, time.Local), - Amount: 20, - Price: currency.Value(6040), - Fees: currency.Zero(), - Taxes: currency.Zero(), + bankAccountID: testdata.TestBankAccount.ID, + brokerageAccountID: testdata.TestBrokerageAccount.ID, + }, + wantTx: &persistence.Transaction{ + ID: "dbddb1c7b1ce1375", + SecurityID: sql.NullString{String: "US00827B1061", Valid: true}, + Type: events.PortfolioEventTypeBuy, + SourceAccountID: sql.NullString{String: testdata.TestBankAccount.ID, Valid: true}, + DestinationAccountID: sql.NullString{String: testdata.TestBrokerageAccount.ID, Valid: true}, + Time: time.Date(2022, 1, 1, 9, 0, 0, 0, time.Local), + Amount: 20, + Price: currency.Value(6040), + Fees: currency.Zero(), + Taxes: currency.Zero(), }, wantSec: &persistence.Security{ ID: "US00827B1061", @@ -174,16 +187,20 @@ func Test_readLine(t *testing.T) { cr.Comma = ';' return cr }(), - }, - wantTx: &persistence.PortfolioEvent{ - ID: "8bb43fed65b35685", - SecurityID: "DE0005557508", - Type: events.PortfolioEventTypeSell, - Time: time.Date(2022, 1, 1, 8, 0, 6, 0, time.Local), - Amount: 103, - Fees: currency.Zero(), - Taxes: currency.Value(1830), - Price: currency.Value(1552), + bankAccountID: testdata.TestBankAccount.ID, + brokerageAccountID: testdata.TestBrokerageAccount.ID, + }, + wantTx: &persistence.Transaction{ + ID: "4201924709e1f078", + SecurityID: sql.NullString{String: "DE0005557508", Valid: true}, + SourceAccountID: sql.NullString{String: testdata.TestBrokerageAccount.ID, Valid: true}, + DestinationAccountID: sql.NullString{String: testdata.TestBankAccount.ID, Valid: true}, + Type: events.PortfolioEventTypeSell, + Time: time.Date(2022, 1, 1, 8, 0, 6, 0, time.Local), + Amount: 103, + Fees: currency.Zero(), + Taxes: currency.Value(1830), + Price: currency.Value(1552), }, wantSec: &persistence.Security{ ID: "DE0005557508", @@ -279,7 +296,7 @@ func Test_readLine(t *testing.T) { } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - gotTx, gotSec, gotLs, err := readLine(tt.args.cr, tt.args.pname) + gotTx, gotSec, gotLs, err := readLine(tt.args.cr, tt.args.bankAccountID, tt.args.brokerageAccountID) if err != nil { tt.wantErr(t, err) return diff --git a/internal/testdata/securities.go b/internal/testdata/securities.go index 2ef7b151..960e257a 100644 --- a/internal/testdata/securities.go +++ b/internal/testdata/securities.go @@ -119,3 +119,23 @@ var TestCreateDepositTransactionParams = persistence.CreateTransactionParams{ Amount: TestDepositTransaction.Amount, Price: TestDepositTransaction.Price, } + +// TestPortfolio is a test portfolio. +var TestPortfolio = &persistence.Portfolio{ + ID: "myportfolio", + DisplayName: "My Portfolio", +} + +// TestCreatePortfolioParams is a test portfolio creation parameter for +// [TestPortfolio]. +var TestCreatePortfolioParams = persistence.CreatePortfolioParams{ + ID: TestPortfolio.ID, + DisplayName: TestPortfolio.DisplayName, +} + +// TestAddAccountToPortfolioParams is a test account addition parameter for +// [TestBankAccount] to [TestPortfolio]. +var TestAddAccountToPortfolioParams = persistence.AddAccountToPortfolioParams{ + PortfolioID: TestPortfolio.ID, + AccountID: TestBankAccount.ID, +} diff --git a/models/models_gen.go b/models/models_gen.go index 5a4da2c7..35a72cac 100644 --- a/models/models_gen.go +++ b/models/models_gen.go @@ -6,6 +6,7 @@ import ( "github.com/oxisto/money-gopher/currency" "github.com/oxisto/money-gopher/persistence" "github.com/oxisto/money-gopher/portfolio/accounts" + "github.com/oxisto/money-gopher/portfolio/events" ) type AccountInput struct { @@ -22,9 +23,16 @@ type ListedSecurityInput struct { type Mutation struct { } +type PortfolioEvent struct { + Time string `json:"time"` + Type events.PortfolioEventType `json:"type"` + Security *persistence.Security `json:"security,omitempty"` +} + type PortfolioInput struct { - ID string `json:"id"` - DisplayName string `json:"displayName"` + ID string `json:"id"` + DisplayName string `json:"displayName"` + AccountIds []string `json:"accountIds"` } type PortfolioPosition struct { diff --git a/persistence/accounts.sql.go b/persistence/accounts.sql.go index 105fa046..37c4b6a9 100644 --- a/persistence/accounts.sql.go +++ b/persistence/accounts.sql.go @@ -15,6 +15,23 @@ import ( "github.com/oxisto/money-gopher/portfolio/events" ) +const addAccountToPortfolio = `-- name: AddAccountToPortfolio :exec +INSERT INTO + portfolio_accounts (portfolio_id, account_id) +VALUES + (?, ?) +` + +type AddAccountToPortfolioParams struct { + PortfolioID string + AccountID string +} + +func (q *Queries) AddAccountToPortfolio(ctx context.Context, arg AddAccountToPortfolioParams) error { + _, err := q.db.ExecContext(ctx, addAccountToPortfolio, arg.PortfolioID, arg.AccountID) + return err +} + const createAccount = `-- name: CreateAccount :one INSERT INTO accounts (id, display_name, type, reference_account_id) @@ -46,42 +63,22 @@ func (q *Queries) CreateAccount(ctx context.Context, arg CreateAccountParams) (* return &i, err } -const createBankAccount = `-- name: CreateBankAccount :one +const createPortfolio = `-- name: CreatePortfolio :one INSERT INTO - bank_accounts (id, display_name) + portfolios (id, display_name) VALUES (?, ?) RETURNING id, display_name ` -type CreateBankAccountParams struct { +type CreatePortfolioParams struct { ID string DisplayName string } -func (q *Queries) CreateBankAccount(ctx context.Context, arg CreateBankAccountParams) (*BankAccount, error) { - row := q.db.QueryRowContext(ctx, createBankAccount, arg.ID, arg.DisplayName) - var i BankAccount - err := row.Scan(&i.ID, &i.DisplayName) - return &i, err -} - -const createPortfolio = `-- name: CreatePortfolio :one -INSERT INTO - portfolios (id, display_name, bank_account_id) -VALUES - (?, ?, ?) RETURNING id, display_name, bank_account_id -` - -type CreatePortfolioParams struct { - ID string - DisplayName string - BankAccountID string -} - func (q *Queries) CreatePortfolio(ctx context.Context, arg CreatePortfolioParams) (*Portfolio, error) { - row := q.db.QueryRowContext(ctx, createPortfolio, arg.ID, arg.DisplayName, arg.BankAccountID) + row := q.db.QueryRowContext(ctx, createPortfolio, arg.ID, arg.DisplayName) var i Portfolio - err := row.Scan(&i.ID, &i.DisplayName, &i.BankAccountID) + err := row.Scan(&i.ID, &i.DisplayName) return &i, err } @@ -184,25 +181,9 @@ func (q *Queries) GetAccount(ctx context.Context, id string) (*Account, error) { return &i, err } -const getBankAccount = `-- name: GetBankAccount :one -SELECT - id, display_name -FROM - bank_accounts -WHERE - id = ? -` - -func (q *Queries) GetBankAccount(ctx context.Context, id string) (*BankAccount, error) { - row := q.db.QueryRowContext(ctx, getBankAccount, id) - var i BankAccount - err := row.Scan(&i.ID, &i.DisplayName) - return &i, err -} - const getPortfolio = `-- name: GetPortfolio :one SELECT - id, display_name, bank_account_id + id, display_name FROM portfolios WHERE @@ -212,7 +193,7 @@ WHERE func (q *Queries) GetPortfolio(ctx context.Context, id string) (*Portfolio, error) { row := q.db.QueryRowContext(ctx, getPortfolio, id) var i Portfolio - err := row.Scan(&i.ID, &i.DisplayName, &i.BankAccountID) + err := row.Scan(&i.ID, &i.DisplayName) return &i, err } @@ -253,34 +234,30 @@ func (q *Queries) ListAccounts(ctx context.Context) ([]*Account, error) { return items, nil } -const listPortfolioEventsByPortfolioID = `-- name: ListPortfolioEventsByPortfolioID :many +const listAccountsByPortfolioID = `-- name: ListAccountsByPortfolioID :many SELECT - id, type, time, portfolio_id, security_id, amount, price, fees, taxes + accounts.id, accounts.display_name, accounts.type, accounts.reference_account_id FROM - portfolio_events + accounts + JOIN portfolio_accounts ON accounts.id = portfolio_accounts.account_id WHERE - portfolio_id = ? + portfolio_accounts.portfolio_id = ? ` -func (q *Queries) ListPortfolioEventsByPortfolioID(ctx context.Context, portfolioID string) ([]*PortfolioEvent, error) { - rows, err := q.db.QueryContext(ctx, listPortfolioEventsByPortfolioID, portfolioID) +func (q *Queries) ListAccountsByPortfolioID(ctx context.Context, portfolioID string) ([]*Account, error) { + rows, err := q.db.QueryContext(ctx, listAccountsByPortfolioID, portfolioID) if err != nil { return nil, err } defer rows.Close() - var items []*PortfolioEvent + var items []*Account for rows.Next() { - var i PortfolioEvent + var i Account if err := rows.Scan( &i.ID, + &i.DisplayName, &i.Type, - &i.Time, - &i.PortfolioID, - &i.SecurityID, - &i.Amount, - &i.Price, - &i.Fees, - &i.Taxes, + &i.ReferenceAccountID, ); err != nil { return nil, err } @@ -297,7 +274,7 @@ func (q *Queries) ListPortfolioEventsByPortfolioID(ctx context.Context, portfoli const listPortfolios = `-- name: ListPortfolios :many SELECT - id, display_name, bank_account_id + id, display_name FROM portfolios ORDER BY @@ -313,7 +290,7 @@ func (q *Queries) ListPortfolios(ctx context.Context) ([]*Portfolio, error) { var items []*Portfolio for rows.Next() { var i Portfolio - if err := rows.Scan(&i.ID, &i.DisplayName, &i.BankAccountID); err != nil { + if err := rows.Scan(&i.ID, &i.DisplayName); err != nil { return nil, err } items = append(items, &i) @@ -370,3 +347,81 @@ func (q *Queries) ListTransactionsByAccountID(ctx context.Context, accountID sql } return items, nil } + +const listTransactionsByPortfolioID = `-- name: ListTransactionsByPortfolioID :many +SELECT + id, source_account_id, destination_account_id, time, type, security_id, amount, price, fees, taxes +FROM + transactions +WHERE + source_account_id IN ( + SELECT + account_id + FROM + portfolio_accounts + WHERE + portfolio_accounts.portfolio_id = ?1 + ) + OR destination_account_id IN ( + SELECT + account_id + FROM + portfolio_accounts + WHERE + portfolio_accounts.portfolio_id = ?1 + ) +` + +func (q *Queries) ListTransactionsByPortfolioID(ctx context.Context, portfolioID string) ([]*Transaction, error) { + rows, err := q.db.QueryContext(ctx, listTransactionsByPortfolioID, portfolioID) + if err != nil { + return nil, err + } + defer rows.Close() + var items []*Transaction + for rows.Next() { + var i Transaction + if err := rows.Scan( + &i.ID, + &i.SourceAccountID, + &i.DestinationAccountID, + &i.Time, + &i.Type, + &i.SecurityID, + &i.Amount, + &i.Price, + &i.Fees, + &i.Taxes, + ); err != nil { + return nil, err + } + items = append(items, &i) + } + if err := rows.Close(); err != nil { + return nil, err + } + if err := rows.Err(); err != nil { + return nil, err + } + return items, nil +} + +const updatePortfolio = `-- name: UpdatePortfolio :one +UPDATE portfolios +SET + display_name = ? +WHERE + id = ? RETURNING id, display_name +` + +type UpdatePortfolioParams struct { + DisplayName string + ID string +} + +func (q *Queries) UpdatePortfolio(ctx context.Context, arg UpdatePortfolioParams) (*Portfolio, error) { + row := q.db.QueryRowContext(ctx, updatePortfolio, arg.DisplayName, arg.ID) + var i Portfolio + err := row.Scan(&i.ID, &i.DisplayName) + return &i, err +} diff --git a/persistence/extra.go b/persistence/extra.go index 982e53ec..12171697 100644 --- a/persistence/extra.go +++ b/persistence/extra.go @@ -18,10 +18,11 @@ func (s *Security) ListedAs(ctx context.Context, db *DB) ([]*ListedSecurity, err // - portfolio ID // - date // - amount -func (tx *PortfolioEvent) MakeUniqueID() { +func (tx *Transaction) MakeUniqueID() { h := fnv.New64a() - h.Write([]byte(tx.SecurityID)) - h.Write([]byte(tx.PortfolioID)) + h.Write([]byte(tx.SecurityID.String)) + h.Write([]byte(tx.SourceAccountID.String)) + h.Write([]byte(tx.DestinationAccountID.String)) h.Write([]byte(tx.Time.Format(time.DateTime))) h.Write([]byte(strconv.FormatInt(int64(tx.Type), 10))) h.Write([]byte(strconv.FormatInt(int64(tx.Amount), 10))) diff --git a/persistence/models.go b/persistence/models.go index c584d87d..8aa14d72 100644 --- a/persistence/models.go +++ b/persistence/models.go @@ -25,13 +25,6 @@ type Account struct { ReferenceAccountID sql.NullInt64 } -type BankAccount struct { - // ID is the primary identifier for a bank account. - ID string - // DisplayName is the human-readable name of the bank account. - DisplayName string -} - // ListedSecurity represents a security that is listed on a particular exchange. type ListedSecurity struct { // SecurityID is the ID of the security. @@ -46,14 +39,12 @@ type ListedSecurity struct { LatestQuoteTimestamp sql.NullTime } -// Portfolios represent a collection of securities and other positions +// Portfolios represent a collection of securities and other positions. type Portfolio struct { // ID is the primary identifier for a portfolio. ID string // DisplayName is the human-readable name of the portfolio. DisplayName string - // BankAccountID is the ID of the bank account that holds the portfolio. - BankAccountID string } // PortfolioAccounts represents the relationship between portfolios and accounts. @@ -62,18 +53,6 @@ type PortfolioAccount struct { AccountID string } -type PortfolioEvent struct { - ID string - Type events.PortfolioEventType - Time time.Time - PortfolioID string - SecurityID string - Amount float64 - Price *currency.Currency - Fees *currency.Currency - Taxes *currency.Currency -} - // Security represents a security that can be traded on an exchange. type Security struct { // ID is the primary identifier for a security. diff --git a/persistence/sql/migrations/0002_create_accounts.sql b/persistence/sql/migrations/0002_create_accounts.sql index bf5fbf13..f357246d 100644 --- a/persistence/sql/migrations/0002_create_accounts.sql +++ b/persistence/sql/migrations/0002_create_accounts.sql @@ -1,31 +1,10 @@ -- +goose Up CREATE TABLE IF NOT EXISTS portfolios ( - -- Portfolios represent a collection of securities and other positions - -- held by a user. + -- Portfolios represent a collection of securities and other positions. + -- held by a user. It can be seen as view over multiple accounts. id TEXT PRIMARY KEY, -- ID is the primary identifier for a portfolio. - display_name TEXT NOT NULL, -- DisplayName is the human-readable name of the portfolio. - bank_account_id TEXT NOT NULL, -- BankAccountID is the ID of the bank account that holds the portfolio. - FOREIGN KEY (bank_account_id) REFERENCES bank_accounts (id) ON DELETE RESTRICT - ); - -CREATE TABLE - IF NOT EXISTS portfolio_events ( - id TEXT PRIMARY KEY, - type INTEGER NOT NULL, - time DATETIME NOT NULL, - portfolio_id TEXT NOT NULL, - security_id TEXT NOT NULL, - amount REAL NOT NULL, - price JSONB, - fees JSONB, - taxes JSONB - ); - -CREATE TABLE - IF NOT EXISTS bank_accounts ( - id TEXT PRIMARY KEY, -- ID is the primary identifier for a bank account. - display_name TEXT NOT NULL -- DisplayName is the human-readable name of the bank account. + display_name TEXT NOT NULL -- DisplayName is the human-readable name of the portfolio. ); CREATE TABLE @@ -69,12 +48,8 @@ CREATE TABLE -- +goose Down DROP TABLE portfolios; -DROP TABLE portfolio_events; - DROP TABLE portfolio_accounts; -DROP TABLE bank_accounts; - DROP TABLE accounts; DROP TABLE transactions; \ No newline at end of file diff --git a/persistence/sql/queries/accounts.sql b/persistence/sql/queries/accounts.sql index 26c86191..efa4fbc0 100644 --- a/persistence/sql/queries/accounts.sql +++ b/persistence/sql/queries/accounts.sql @@ -14,14 +14,6 @@ FROM ORDER BY id; --- name: ListPortfolioEventsByPortfolioID :many -SELECT - * -FROM - portfolio_events -WHERE - portfolio_id = ?; - -- name: ListAccounts :many SELECT * @@ -38,14 +30,6 @@ FROM WHERE id = ?; --- name: GetBankAccount :one -SELECT - * -FROM - bank_accounts -WHERE - id = ?; - -- name: CreateAccount :one INSERT INTO accounts (id, display_name, type, reference_account_id) @@ -57,17 +41,24 @@ DELETE FROM accounts WHERE id = ? RETURNING *; --- name: CreateBankAccount :one +-- name: CreatePortfolio :one INSERT INTO - bank_accounts (id, display_name) + portfolios (id, display_name) VALUES (?, ?) RETURNING *; --- name: CreatePortfolio :one +-- name: UpdatePortfolio :one +UPDATE portfolios +SET + display_name = ? +WHERE + id = ? RETURNING *; + +-- name: AddAccountToPortfolio :exec INSERT INTO - portfolios (id, display_name, bank_account_id) + portfolio_accounts (portfolio_id, account_id) VALUES - (?, ?, ?) RETURNING *; + (?, ?); -- name: CreateTransaction :one INSERT INTO @@ -86,6 +77,15 @@ INSERT INTO VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?) RETURNING *; +-- name: ListAccountsByPortfolioID :many +SELECT + accounts.* +FROM + accounts + JOIN portfolio_accounts ON accounts.id = portfolio_accounts.account_id +WHERE + portfolio_accounts.portfolio_id = ?; + -- name: ListTransactionsByAccountID :many SELECT * @@ -93,4 +93,27 @@ FROM transactions WHERE source_account_id = sqlc.arg ('account_id') - OR destination_account_id = sqlc.arg ('account_id'); \ No newline at end of file + OR destination_account_id = sqlc.arg ('account_id'); + +-- name: ListTransactionsByPortfolioID :many +SELECT + * +FROM + transactions +WHERE + source_account_id IN ( + SELECT + account_id + FROM + portfolio_accounts + WHERE + portfolio_accounts.portfolio_id = sqlc.arg ('portfolio_id') + ) + OR destination_account_id IN ( + SELECT + account_id + FROM + portfolio_accounts + WHERE + portfolio_accounts.portfolio_id = sqlc.arg ('portfolio_id') + ); \ No newline at end of file From a09bfa2313a78bb29af8d24c1dd53df5778f7bce Mon Sep 17 00:00:00 2001 From: Christian Banse Date: Sun, 12 Jan 2025 19:22:22 +0100 Subject: [PATCH 32/35] More unit tests --- cli/commands/account.go | 13 ++-- cli/commands/account_test.go | 21 +++++- graph/generated.go | 138 +++++++++++++++++++++++++++++++++-- graph/schema.graphqls | 18 +++++ graph/schema.resolvers.go | 2 +- portfolio/events/type.go | 42 +++++++++-- 6 files changed, 213 insertions(+), 21 deletions(-) diff --git a/cli/commands/account.go b/cli/commands/account.go index 0cb38654..1fb49401 100644 --- a/cli/commands/account.go +++ b/cli/commands/account.go @@ -25,6 +25,7 @@ import ( mcli "github.com/oxisto/money-gopher/cli" "github.com/oxisto/money-gopher/models" "github.com/oxisto/money-gopher/portfolio/accounts" + "github.com/oxisto/money-gopher/portfolio/events" "github.com/shurcooL/graphql" "github.com/urfave/cli/v3" @@ -163,13 +164,15 @@ func ListTransactions(ctx context.Context, cmd *cli.Command) (err error) { var query struct { Transactions []struct { - ID string `json:"id"` - Time time.Time `json:"time"` - Type string `json:"type"` - } `json:"transactions"` + ID string `json:"id"` + Time time.Time `json:"time"` + Type events.PortfolioEventType `json:"type"` + } `graphql:"transactions(accountID: $accountID)" json:"transactions"` } - err = s.GraphQL.Query(context.Background(), &query, nil) + err = s.GraphQL.Query(context.Background(), &query, map[string]interface{}{ + "accountID": graphql.String(cmd.String("account-id")), + }) if err != nil { return err } diff --git a/cli/commands/account_test.go b/cli/commands/account_test.go index f1ffb800..82bbce6f 100644 --- a/cli/commands/account_test.go +++ b/cli/commands/account_test.go @@ -159,6 +159,18 @@ func TestDeleteAccount(t *testing.T) { } func TestListTransactions(t *testing.T) { + srv := servertest.NewServer(internal.NewTestDB(t, func(db *persistence.DB) { + _, err := db.Queries.CreateAccount(context.Background(), testdata.TestCreateBankAccountParams) + assert.NoError(t, err) + + _, err = db.Queries.CreateAccount(context.Background(), testdata.TestCreateBrokerageAccountParams) + assert.NoError(t, err) + + _, err = db.Queries.CreateTransaction(context.Background(), testdata.TestCreateBuyTransactionParams) + assert.NoError(t, err) + })) + defer srv.Close() + type args struct { ctx context.Context cmd *cli.Command @@ -168,8 +180,15 @@ func TestListTransactions(t *testing.T) { args args wantErr bool }{ - // TODO: Add test cases. + { + name: "happy path", + args: args{ + ctx: clitest.NewSessionContext(t, srv), + cmd: clitest.MockCommand(t, AccountCmd.Command("transactions").Command("list").Flags, "--account-id", testdata.TestBrokerageAccount.ID), + }, + }, } + for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { if err := ListTransactions(tt.args.ctx, tt.args.cmd); (err != nil) != tt.wantErr { diff --git a/graph/generated.go b/graph/generated.go index ed6cb14f..fbfdd444 100644 --- a/graph/generated.go +++ b/graph/generated.go @@ -145,10 +145,12 @@ type ComplexityRoot struct { Amount func(childComplexity int) int DestinationAccount func(childComplexity int) int Fees func(childComplexity int) int + ID func(childComplexity int) int Price func(childComplexity int) int Security func(childComplexity int) int SourceAccount func(childComplexity int) int Time func(childComplexity int) int + Type func(childComplexity int) int } } @@ -679,6 +681,13 @@ func (e *executableSchema) Complexity(typeName, field string, childComplexity in return e.complexity.Transaction.Fees(childComplexity), true + case "Transaction.id": + if e.complexity.Transaction.ID == nil { + break + } + + return e.complexity.Transaction.ID(childComplexity), true + case "Transaction.price": if e.complexity.Transaction.Price == nil { break @@ -707,6 +716,13 @@ func (e *executableSchema) Complexity(typeName, field string, childComplexity in return e.complexity.Transaction.Time(childComplexity), true + case "Transaction.type": + if e.complexity.Transaction.Type == nil { + break + } + + return e.complexity.Transaction.Type(childComplexity), true + } return 0, false } @@ -3867,6 +3883,8 @@ func (ec *executionContext) fieldContext_Query_transactions(ctx context.Context, IsResolver: true, Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { switch field.Name { + case "id": + return ec.fieldContext_Transaction_id(ctx, field) case "time": return ec.fieldContext_Transaction_time(ctx, field) case "sourceAccount": @@ -3881,6 +3899,8 @@ func (ec *executionContext) fieldContext_Query_transactions(ctx context.Context, return ec.fieldContext_Transaction_price(ctx, field) case "fees": return ec.fieldContext_Transaction_fees(ctx, field) + case "type": + return ec.fieldContext_Transaction_type(ctx, field) } return nil, fmt.Errorf("no field named %q was found under type Transaction", field.Name) }, @@ -4210,6 +4230,50 @@ func (ec *executionContext) fieldContext_Security_listedAs(_ context.Context, fi return fc, nil } +func (ec *executionContext) _Transaction_id(ctx context.Context, field graphql.CollectedField, obj *persistence.Transaction) (ret graphql.Marshaler) { + fc, err := ec.fieldContext_Transaction_id(ctx, field) + if err != nil { + return graphql.Null + } + ctx = graphql.WithFieldContext(ctx, fc) + defer func() { + if r := recover(); r != nil { + ec.Error(ctx, ec.Recover(ctx, r)) + ret = graphql.Null + } + }() + resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (any, error) { + ctx = rctx // use context from middleware stack in children + return obj.ID, nil + }) + if err != nil { + ec.Error(ctx, err) + return graphql.Null + } + if resTmp == nil { + if !graphql.HasFieldError(ctx, fc) { + ec.Errorf(ctx, "must not be null") + } + return graphql.Null + } + res := resTmp.(string) + fc.Result = res + return ec.marshalNString2string(ctx, field.Selections, res) +} + +func (ec *executionContext) fieldContext_Transaction_id(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { + fc = &graphql.FieldContext{ + Object: "Transaction", + Field: field, + IsMethod: false, + IsResolver: false, + Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + return nil, errors.New("field of type String does not have child fields") + }, + } + return fc, nil +} + func (ec *executionContext) _Transaction_time(ctx context.Context, field graphql.CollectedField, obj *persistence.Transaction) (ret graphql.Marshaler) { fc, err := ec.fieldContext_Transaction_time(ctx, field) if err != nil { @@ -4560,6 +4624,50 @@ func (ec *executionContext) fieldContext_Transaction_fees(_ context.Context, fie return fc, nil } +func (ec *executionContext) _Transaction_type(ctx context.Context, field graphql.CollectedField, obj *persistence.Transaction) (ret graphql.Marshaler) { + fc, err := ec.fieldContext_Transaction_type(ctx, field) + if err != nil { + return graphql.Null + } + ctx = graphql.WithFieldContext(ctx, fc) + defer func() { + if r := recover(); r != nil { + ec.Error(ctx, ec.Recover(ctx, r)) + ret = graphql.Null + } + }() + resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (any, error) { + ctx = rctx // use context from middleware stack in children + return obj.Type, nil + }) + if err != nil { + ec.Error(ctx, err) + return graphql.Null + } + if resTmp == nil { + if !graphql.HasFieldError(ctx, fc) { + ec.Errorf(ctx, "must not be null") + } + return graphql.Null + } + res := resTmp.(events.PortfolioEventType) + fc.Result = res + return ec.marshalNPortfolioEventType2githubᚗcomᚋoxistoᚋmoneyᚑgopherᚋportfolioᚋeventsᚐPortfolioEventType(ctx, field.Selections, res) +} + +func (ec *executionContext) fieldContext_Transaction_type(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { + fc = &graphql.FieldContext{ + Object: "Transaction", + Field: field, + IsMethod: false, + IsResolver: false, + Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + return nil, errors.New("field of type PortfolioEventType does not have child fields") + }, + } + return fc, nil +} + func (ec *executionContext) ___Directive_name(ctx context.Context, field graphql.CollectedField, obj *introspection.Directive) (ret graphql.Marshaler) { fc, err := ec.fieldContext___Directive_name(ctx, field) if err != nil { @@ -7496,6 +7604,11 @@ func (ec *executionContext) _Transaction(ctx context.Context, sel ast.SelectionS switch field.Name { case "__typename": out.Values[i] = graphql.MarshalString("Transaction") + case "id": + out.Values[i] = ec._Transaction_id(ctx, field, obj) + if out.Values[i] == graphql.Null { + atomic.AddUint32(&out.Invalids, 1) + } case "time": field := field @@ -7655,6 +7768,11 @@ func (ec *executionContext) _Transaction(ctx context.Context, sel ast.SelectionS if out.Values[i] == graphql.Null { atomic.AddUint32(&out.Invalids, 1) } + case "type": + out.Values[i] = ec._Transaction_type(ctx, field, obj) + if out.Values[i] == graphql.Null { + atomic.AddUint32(&out.Invalids, 1) + } default: panic("unknown field " + strconv.Quote(field.Name)) } @@ -8364,14 +8482,22 @@ func (ec *executionContext) marshalNPortfolioEventType2githubᚗcomᚋoxistoᚋm var ( unmarshalNPortfolioEventType2githubᚗcomᚋoxistoᚋmoneyᚑgopherᚋportfolioᚋeventsᚐPortfolioEventType = map[string]events.PortfolioEventType{ - "BUY": events.PortfolioEventTypeBuy, - "SELL": events.PortfolioEventTypeSell, - "DIVIDEND": events.PortfolioEventTypeDividend, + "BUY": events.PortfolioEventTypeBuy, + "SELL": events.PortfolioEventTypeSell, + "DIVIDEND": events.PortfolioEventTypeDividend, + "DELIVERY_INBOUND": events.PortfolioEventTypeDeliveryInbound, + "DELIVERY_OUTBOUND": events.PortfolioEventTypeDeliveryOutbound, + "DEPOSIT_CASH": events.PortfolioEventTypeDepositCash, + "WITHDRAW_CASH": events.PortfolioEventTypeWithdrawCash, } marshalNPortfolioEventType2githubᚗcomᚋoxistoᚋmoneyᚑgopherᚋportfolioᚋeventsᚐPortfolioEventType = map[events.PortfolioEventType]string{ - events.PortfolioEventTypeBuy: "BUY", - events.PortfolioEventTypeSell: "SELL", - events.PortfolioEventTypeDividend: "DIVIDEND", + events.PortfolioEventTypeBuy: "BUY", + events.PortfolioEventTypeSell: "SELL", + events.PortfolioEventTypeDividend: "DIVIDEND", + events.PortfolioEventTypeDeliveryInbound: "DELIVERY_INBOUND", + events.PortfolioEventTypeDeliveryOutbound: "DELIVERY_OUTBOUND", + events.PortfolioEventTypeDepositCash: "DEPOSIT_CASH", + events.PortfolioEventTypeWithdrawCash: "WITHDRAW_CASH", } ) diff --git a/graph/schema.graphqls b/graph/schema.graphqls index 80beeddd..ab2e1b5d 100644 --- a/graph/schema.graphqls +++ b/graph/schema.graphqls @@ -51,6 +51,22 @@ enum PortfolioEventType @goEnum( value: "github.com/oxisto/money-gopher/portfolio/events.PortfolioEventTypeDividend" ) + DELIVERY_INBOUND + @goEnum( + value: "github.com/oxisto/money-gopher/portfolio/events.PortfolioEventTypeDeliveryInbound" + ) + DELIVERY_OUTBOUND + @goEnum( + value: "github.com/oxisto/money-gopher/portfolio/events.PortfolioEventTypeDeliveryOutbound" + ) + DEPOSIT_CASH + @goEnum( + value: "github.com/oxisto/money-gopher/portfolio/events.PortfolioEventTypeDepositCash" + ) + WITHDRAW_CASH + @goEnum( + value: "github.com/oxisto/money-gopher/portfolio/events.PortfolioEventTypeWithdrawCash" + ) } enum AccountType @@ -78,6 +94,7 @@ type PortfolioEvent { } type Transaction { + id: String! time: Date! sourceAccount: Account! destinationAccount: Account! @@ -85,6 +102,7 @@ type Transaction { amount: Float! price: Currency! fees: Currency! + type: PortfolioEventType! } type PortfolioSnapshot { diff --git a/graph/schema.resolvers.go b/graph/schema.resolvers.go index 9bcfc5b8..f5932fbb 100644 --- a/graph/schema.resolvers.go +++ b/graph/schema.resolvers.go @@ -257,7 +257,7 @@ func (r *securityResolver) ListedAs(ctx context.Context, obj *persistence.Securi // Time is the resolver for the time field. func (r *transactionResolver) Time(ctx context.Context, obj *persistence.Transaction) (string, error) { - panic(fmt.Errorf("not implemented: Time - time")) + return obj.Time.Format(time.RFC3339), nil } // SourceAccount is the resolver for the sourceAccount field. diff --git a/portfolio/events/type.go b/portfolio/events/type.go index b6435364..f2c60e1a 100644 --- a/portfolio/events/type.go +++ b/portfolio/events/type.go @@ -1,23 +1,49 @@ package events +import "github.com/oxisto/money-gopher/internal/enum" + +//go:generate stringer -linecomment -type=PortfolioEventType -output=type_string.go + // PortfolioEventType is the type of a portfolio event. type PortfolioEventType int const ( // PortfolioEventTypeBuy represents a buy event. - PortfolioEventTypeBuy PortfolioEventType = iota + 1 + PortfolioEventTypeBuy PortfolioEventType = iota + 1 // BUY // PortfolioEventTypeSell represents a sell event. - PortfolioEventTypeSell + PortfolioEventTypeSell // SELL // PortfolioEventTypeDividend represents a dividend event. - PortfolioEventTypeDividend + PortfolioEventTypeDividend // DIVIDEND // PortfolioEventTypeTax represents the inbound delivery of a security or cash from another account. - PortfolioEventTypeDeliveryInbound + PortfolioEventTypeDeliveryInbound // DELIVERY_INBOUND // PortfolioEventTypeTax represents the outbound delivery of a security or cash to another account. - PortfolioEventTypeDeliveryOutbound + PortfolioEventTypeDeliveryOutbound // DELIVERY_OUTBOUND // PortfolioEventTypeDepositCash represents a deposit of cash. - PortfolioEventTypeDepositCash + PortfolioEventTypeDepositCash // DEPOSIT_CASH // PortfolioEventTypeWithdrawCash represents a withdrawal of cash. - PortfolioEventTypeWithdrawCash + PortfolioEventTypeWithdrawCash // WITHDRAW_CASH // PortfolioEventTypeUnknown represents an unknown event type. - PortfolioEventTypeUnknown + PortfolioEventTypeUnknown // UNKNOWN ) + +// Get implements [flag.Getter]. +func (t *PortfolioEventType) Get() any { + return t +} + +// Set implements [flag.Value]. +func (t *PortfolioEventType) Set(v string) error { + return enum.Set(t, v, _PortfolioEventType_name, _PortfolioEventType_index[:]) +} + +// MarshalJSON marshals the account type to JSON using the string +// representation. +func (t PortfolioEventType) MarshalJSON() ([]byte, error) { + return enum.MarshalJSON(t) +} + +// UnmarshalJSON unmarshals the account type from JSON. It expects a string +// representation. +func (t *PortfolioEventType) UnmarshalJSON(data []byte) error { + return enum.UnmarshalJSON(t, data) +} From 2627c27c8acb9ebe9ca400bf7ce1cdbcb14162f0 Mon Sep 17 00:00:00 2001 From: Christian Banse Date: Sun, 12 Jan 2025 21:11:28 +0100 Subject: [PATCH 33/35] Fixed tests --- cli/commands/portfolio.go | 80 ++++++++++++++------------------- cli/commands/portfolio_test.go | 8 +++- finance/snapshot.go | 12 +++-- graph/generated.go | 40 ++--------------- graph/schema.resolvers.go | 28 +++++++----- import/csv/csv_importer.go | 17 +++---- import/csv/csv_importer_test.go | 26 +++++------ internal/testdata/securities.go | 12 ++--- persistence/accounts.sql.go | 11 +++-- persistence/extra.go | 6 +-- persistence/models.go | 13 +++--- persistence/securities.sql.go | 8 ++-- securities/quote/quote.go | 9 ++-- securities/quote/quote_test.go | 4 +- sqlc.yaml | 1 + 15 files changed, 122 insertions(+), 153 deletions(-) diff --git a/cli/commands/portfolio.go b/cli/commands/portfolio.go index fb22db70..01be5bb9 100644 --- a/cli/commands/portfolio.go +++ b/cli/commands/portfolio.go @@ -19,9 +19,12 @@ package commands import ( "context" "fmt" + "strings" + "time" mcli "github.com/oxisto/money-gopher/cli" "github.com/oxisto/money-gopher/models" + "github.com/oxisto/money-gopher/portfolio/events" "github.com/fatih/color" "github.com/urfave/cli/v3" @@ -96,59 +99,44 @@ func ListPortfolio(ctx context.Context, cmd *cli.Command) (err error) { var query struct { Portfolios []struct { - ID string `json:"id"` - DisplayName string `json:"displayName"` + ID string `json:"id"` + DisplayName string `json:"displayName"` + Snapshot models.PortfolioSnapshot `graphql:"snapshot(when: $when)" json:"snapshot"` } `json:"portfolios"` } - err = s.GraphQL.Query(context.Background(), &query, nil) + err = s.GraphQL.Query(context.Background(), &query, map[string]any{ + "when": events.Date(time.Now().Format(time.RFC3339)), + }) if err != nil { return err } - s.WriteJSON(cmd.Writer, query) - /* - s := mcli.FromContext(ctx) - res, err := s.PortfolioClient.ListPortfolios( - context.Background(), - connect.NewRequest(&portfoliov1.ListPortfoliosRequest{}), - ) - if err != nil { - return err - } else { - in := `This is a list of all portfolios. - ` - - for _, portfolio := range res.Msg.Portfolios { - snapshot, _ := s.PortfolioClient.GetPortfolioSnapshot( - context.Background(), - connect.NewRequest(&portfoliov1.GetPortfolioSnapshotRequest{ - PortfolioId: portfolio.Id, - }), - ) - - in += fmt.Sprintf(` - | %-*s | - | %s | %s | - | %-*s | %*s | - | %-*s | %*s | - `, - 15+15+3, color.New(color.FgWhite, color.Bold).Sprint(portfolio.DisplayName), - strings.Repeat("-", 15), - strings.Repeat("-", 15), - 15, "Market Value", - 15, snapshot.Msg.TotalMarketValue.Pretty(), - 15, "Performance", - 15, fmt.Sprintf("%s € (%s %%)", - greenOrRed(float64(snapshot.Msg.TotalProfitOrLoss.Value/100)), - greenOrRed(snapshot.Msg.TotalGains*100), - ), - ) - } - - //out, _ := glamour.Render(in, "dark") - fmt.Println(in) - }*/ + var in string + + for _, portfolio := range query.Portfolios { + snapshot := portfolio.Snapshot + + in += fmt.Sprintf(` +| %-*s | +| %s | %s | +| %-*s | %*s | +| %-*s | %*s | +`, + 15+15+3, color.New(color.FgWhite, color.Bold).Sprint(portfolio.DisplayName), + strings.Repeat("-", 15), + strings.Repeat("-", 15), + 15, "Market Value", + 15, snapshot.TotalMarketValue.Pretty(), + 15, "Performance", + 15, fmt.Sprintf("%s € (%s %%)", + greenOrRed(float64(snapshot.TotalProfitOrLoss.Amount/100)), + greenOrRed(snapshot.TotalGains*100), + ), + ) + } + + fmt.Fprintln(cmd.Writer, in) return nil } diff --git a/cli/commands/portfolio_test.go b/cli/commands/portfolio_test.go index b9e14521..d1833e23 100644 --- a/cli/commands/portfolio_test.go +++ b/cli/commands/portfolio_test.go @@ -20,12 +20,13 @@ import ( "context" "testing" - "github.com/oxisto/assert" "github.com/oxisto/money-gopher/internal" "github.com/oxisto/money-gopher/internal/testdata" "github.com/oxisto/money-gopher/internal/testing/clitest" "github.com/oxisto/money-gopher/internal/testing/servertest" "github.com/oxisto/money-gopher/persistence" + + "github.com/oxisto/assert" "github.com/urfave/cli/v3" ) @@ -40,7 +41,10 @@ func TestListPortfolio(t *testing.T) { _, err = db.Queries.CreatePortfolio(context.Background(), testdata.TestCreatePortfolioParams) assert.NoError(t, err) - db.Queries.AddAccountToPortfolio(context.Background(), testdata.TestAddAccountToPortfolioParams) + err = db.Queries.AddAccountToPortfolio(context.Background(), testdata.TestAddAccountToPortfolioParams) + assert.NoError(t, err) + + _, err = db.Queries.CreateTransaction(context.Background(), testdata.TestCreateBuyTransactionParams) assert.NoError(t, err) })) defer srv.Close() diff --git a/finance/snapshot.go b/finance/snapshot.go index eff5bb6b..6064e836 100644 --- a/finance/snapshot.go +++ b/finance/snapshot.go @@ -122,8 +122,12 @@ func BuildSnapshot( snap.Positions = append(snap.Positions, pos) } - // Calculate total gains - snap.TotalGains = float64(currency.Minus(snap.TotalMarketValue, snap.TotalPurchaseValue).Amount) / float64(snap.TotalPurchaseValue.Amount) + // Calculate total gains. We try to avoid division by zero. + if snap.TotalPurchaseValue.Amount == 0 { + snap.TotalGains = 0 + } else { + snap.TotalGains = float64(currency.Minus(snap.TotalMarketValue, snap.TotalPurchaseValue).Amount) / float64(snap.TotalPurchaseValue.Amount) + } // Calculate total portfolio value snap.TotalPortfolioValue = snap.TotalMarketValue.Plus(snap.Cash) @@ -153,8 +157,8 @@ func groupByPortfolio(events []*persistence.Transaction) (m map[string][]*persis for _, event := range events { name := event.SecurityID - if name.Valid { - m[name.String] = append(m[name.String], event) + if name != nil { + m[*name] = append(m[*name], event) } else { // a little bit of a hack m["cash"] = append(m["cash"], event) diff --git a/graph/generated.go b/graph/generated.go index fbfdd444..adff5189 100644 --- a/graph/generated.go +++ b/graph/generated.go @@ -186,7 +186,6 @@ type QueryResolver interface { Transactions(ctx context.Context, accountID string) ([]*persistence.Transaction, error) } type SecurityResolver interface { - QuoteProvider(ctx context.Context, obj *persistence.Security) (*string, error) ListedAs(ctx context.Context, obj *persistence.Security) ([]*persistence.ListedSecurity, error) } type TransactionResolver interface { @@ -4150,7 +4149,7 @@ func (ec *executionContext) _Security_quoteProvider(ctx context.Context, field g }() resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (any, error) { ctx = rctx // use context from middleware stack in children - return ec.resolvers.Security().QuoteProvider(rctx, obj) + return obj.QuoteProvider, nil }) if err != nil { ec.Error(ctx, err) @@ -4168,8 +4167,8 @@ func (ec *executionContext) fieldContext_Security_quoteProvider(_ context.Contex fc = &graphql.FieldContext{ Object: "Security", Field: field, - IsMethod: true, - IsResolver: true, + IsMethod: false, + IsResolver: false, Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { return nil, errors.New("field of type String does not have child fields") }, @@ -7505,38 +7504,7 @@ func (ec *executionContext) _Security(ctx context.Context, sel ast.SelectionSet, atomic.AddUint32(&out.Invalids, 1) } case "quoteProvider": - field := field - - innerFunc := func(ctx context.Context, _ *graphql.FieldSet) (res graphql.Marshaler) { - defer func() { - if r := recover(); r != nil { - ec.Error(ctx, ec.Recover(ctx, r)) - } - }() - res = ec._Security_quoteProvider(ctx, field, obj) - return res - } - - if field.Deferrable != nil { - dfs, ok := deferred[field.Deferrable.Label] - di := 0 - if ok { - dfs.AddField(field) - di = len(dfs.Values) - 1 - } else { - dfs = graphql.NewFieldSet([]graphql.CollectedField{field}) - deferred[field.Deferrable.Label] = dfs - } - dfs.Concurrently(di, func(ctx context.Context) graphql.Marshaler { - return innerFunc(ctx, dfs) - }) - - // don't run the out.Concurrently() call below - out.Values[i] = graphql.Null - continue - } - - out.Concurrently(i, func(ctx context.Context) graphql.Marshaler { return innerFunc(ctx, out) }) + out.Values[i] = ec._Security_quoteProvider(ctx, field, obj) case "listedAs": field := field diff --git a/graph/schema.resolvers.go b/graph/schema.resolvers.go index f5932fbb..a3bf867d 100644 --- a/graph/schema.resolvers.go +++ b/graph/schema.resolvers.go @@ -6,7 +6,6 @@ package graph import ( "context" - "database/sql" "fmt" "slices" "time" @@ -238,16 +237,7 @@ func (r *queryResolver) Accounts(ctx context.Context) ([]*persistence.Account, e // Transactions is the resolver for the transactions field. func (r *queryResolver) Transactions(ctx context.Context, accountID string) ([]*persistence.Transaction, error) { - return r.DB.ListTransactionsByAccountID(ctx, sql.NullString{String: accountID, Valid: true}) -} - -// QuoteProvider is the resolver for the quoteProvider field. -func (r *securityResolver) QuoteProvider(ctx context.Context, obj *persistence.Security) (*string, error) { - if obj.QuoteProvider.Valid { - return &obj.QuoteProvider.String, nil - } - - return nil, nil + return r.DB.ListTransactionsByAccountID(ctx, &accountID) } // ListedAs is the resolver for the listedAs field. @@ -303,3 +293,19 @@ type portfolioResolver struct{ *Resolver } type queryResolver struct{ *Resolver } type securityResolver struct{ *Resolver } type transactionResolver struct{ *Resolver } + +// !!! WARNING !!! +// The code below was going to be deleted when updating resolvers. It has been copied here so you have +// one last chance to move it out of harms way if you want. There are two reasons this happens: +// - When renaming or deleting a resolver the old code will be put in here. You can safely delete +// it when you're done. +// - You have helper methods in this file. Move them out to keep these resolver files clean. +/* + func (r *securityResolver) QuoteProvider(ctx context.Context, obj *persistence.Security) (*string, error) { + if obj.QuoteProvider.Valid { + return &obj.QuoteProvider.String, nil + } + + return nil, nil +} +*/ diff --git a/import/csv/csv_importer.go b/import/csv/csv_importer.go index 52302ac0..d8ff4046 100644 --- a/import/csv/csv_importer.go +++ b/import/csv/csv_importer.go @@ -29,7 +29,6 @@ package csv import ( - "database/sql" "encoding/csv" "errors" "fmt" @@ -156,13 +155,13 @@ func readLine( if tx.Type == events.PortfolioEventTypeBuy || tx.Type == events.PortfolioEventTypeDeliveryInbound { tx.Price = currency.Divide(currency.Minus(value, tx.Fees), tx.Amount) - tx.SourceAccountID = sql.NullString{String: bankAccountID, Valid: true} - tx.DestinationAccountID = sql.NullString{String: brokerageAccountID, Valid: true} + tx.SourceAccountID = &bankAccountID + tx.DestinationAccountID = &brokerageAccountID } else if tx.Type == events.PortfolioEventTypeSell || tx.Type == events.PortfolioEventTypeDeliveryOutbound { tx.Price = currency.Times(currency.Divide(currency.Minus(currency.Minus(value, tx.Fees), tx.Taxes), tx.Amount), -1) - tx.SourceAccountID = sql.NullString{String: brokerageAccountID, Valid: true} - tx.DestinationAccountID = sql.NullString{String: bankAccountID, Valid: true} + tx.SourceAccountID = &brokerageAccountID + tx.DestinationAccountID = &bankAccountID } sec = new(persistence.Security) @@ -176,13 +175,15 @@ func readLine( }) // Default to YF, but only if we have a ticker symbol, otherwise, let's try ING + var qp string if len(ls) >= 0 && len(ls[0].Ticker) > 0 { - sec.QuoteProvider = sql.NullString{String: quote.QuoteProviderYF, Valid: true} + qp = quote.QuoteProviderYF } else { - sec.QuoteProvider = sql.NullString{String: quote.QuoteProviderING, Valid: true} + qp = quote.QuoteProviderING } + sec.QuoteProvider = &qp - tx.SecurityID = sql.NullString{String: sec.ID, Valid: true} + tx.SecurityID = &sec.ID tx.MakeUniqueID() return diff --git a/import/csv/csv_importer_test.go b/import/csv/csv_importer_test.go index 987af397..26008da0 100644 --- a/import/csv/csv_importer_test.go +++ b/import/csv/csv_importer_test.go @@ -18,12 +18,12 @@ package csv import ( "bytes" - "database/sql" "encoding/csv" "io" "testing" "time" + moneygopher "github.com/oxisto/money-gopher" "github.com/oxisto/money-gopher/currency" "github.com/oxisto/money-gopher/internal/testdata" "github.com/oxisto/money-gopher/persistence" @@ -120,10 +120,10 @@ func Test_readLine(t *testing.T) { }, wantTx: &persistence.Transaction{ ID: "670240c1f8373a3f", - SecurityID: sql.NullString{String: "US0378331005", Valid: true}, + SecurityID: moneygopher.Ref("US0378331005"), Type: events.PortfolioEventTypeBuy, - SourceAccountID: sql.NullString{String: testdata.TestBankAccount.ID, Valid: true}, - DestinationAccountID: sql.NullString{String: testdata.TestBrokerageAccount.ID, Valid: true}, + SourceAccountID: &testdata.TestBankAccount.ID, + DestinationAccountID: &testdata.TestBrokerageAccount.ID, Time: time.Date(2020, 1, 1, 0, 0, 0, 0, time.Local), Amount: 20, Fees: currency.Value(1025), @@ -133,7 +133,7 @@ func Test_readLine(t *testing.T) { wantSec: &persistence.Security{ ID: "US0378331005", DisplayName: "Apple Inc.", - QuoteProvider: sql.NullString{String: quote.QuoteProviderYF, Valid: true}, + QuoteProvider: moneygopher.Ref(quote.QuoteProviderYF), }, wantLs: []*persistence.ListedSecurity{ { @@ -156,10 +156,10 @@ func Test_readLine(t *testing.T) { }, wantTx: &persistence.Transaction{ ID: "dbddb1c7b1ce1375", - SecurityID: sql.NullString{String: "US00827B1061", Valid: true}, + SecurityID: moneygopher.Ref("US00827B1061"), Type: events.PortfolioEventTypeBuy, - SourceAccountID: sql.NullString{String: testdata.TestBankAccount.ID, Valid: true}, - DestinationAccountID: sql.NullString{String: testdata.TestBrokerageAccount.ID, Valid: true}, + SourceAccountID: &testdata.TestBankAccount.ID, + DestinationAccountID: &testdata.TestBrokerageAccount.ID, Time: time.Date(2022, 1, 1, 9, 0, 0, 0, time.Local), Amount: 20, Price: currency.Value(6040), @@ -169,7 +169,7 @@ func Test_readLine(t *testing.T) { wantSec: &persistence.Security{ ID: "US00827B1061", DisplayName: "Affirm Holdings Inc.", - QuoteProvider: sql.NullString{String: quote.QuoteProviderYF, Valid: true}, + QuoteProvider: moneygopher.Ref(quote.QuoteProviderYF), }, wantLs: []*persistence.ListedSecurity{ { @@ -192,9 +192,9 @@ func Test_readLine(t *testing.T) { }, wantTx: &persistence.Transaction{ ID: "4201924709e1f078", - SecurityID: sql.NullString{String: "DE0005557508", Valid: true}, - SourceAccountID: sql.NullString{String: testdata.TestBrokerageAccount.ID, Valid: true}, - DestinationAccountID: sql.NullString{String: testdata.TestBankAccount.ID, Valid: true}, + SecurityID: moneygopher.Ref("DE0005557508"), + SourceAccountID: &testdata.TestBrokerageAccount.ID, + DestinationAccountID: &testdata.TestBankAccount.ID, Type: events.PortfolioEventTypeSell, Time: time.Date(2022, 1, 1, 8, 0, 6, 0, time.Local), Amount: 103, @@ -205,7 +205,7 @@ func Test_readLine(t *testing.T) { wantSec: &persistence.Security{ ID: "DE0005557508", DisplayName: "Deutsche Telekom AG", - QuoteProvider: sql.NullString{String: quote.QuoteProviderYF, Valid: true}, + QuoteProvider: moneygopher.Ref(quote.QuoteProviderYF), }, wantLs: []*persistence.ListedSecurity{ { diff --git a/internal/testdata/securities.go b/internal/testdata/securities.go index 960e257a..45380f27 100644 --- a/internal/testdata/securities.go +++ b/internal/testdata/securities.go @@ -1,10 +1,10 @@ package testdata import ( - "database/sql" "time" "github.com/google/uuid" + moneygopher "github.com/oxisto/money-gopher" "github.com/oxisto/money-gopher/currency" "github.com/oxisto/money-gopher/internal/testing/quotetest" "github.com/oxisto/money-gopher/persistence" @@ -16,7 +16,7 @@ import ( var TestSecurity = &persistence.Security{ ID: "DE1234567890", DisplayName: "My Security", - QuoteProvider: sql.NullString{String: quotetest.QuoteProviderStatic, Valid: true}, + QuoteProvider: moneygopher.Ref(quotetest.QuoteProviderStatic), } // TestListedSecurity is a listed security for [TestSecurity] that has a ticker @@ -76,12 +76,12 @@ var TestCreateBrokerageAccountParams = persistence.CreateAccountParams{ // [TestBrokerageAccount]. var TestBuyTransaction = &persistence.Transaction{ ID: uuid.NewString(), - SourceAccountID: sql.NullString{String: TestBankAccount.ID, Valid: true}, - DestinationAccountID: sql.NullString{String: TestBrokerageAccount.ID, Valid: true}, + SourceAccountID: &TestBankAccount.ID, + DestinationAccountID: &TestBrokerageAccount.ID, Time: time.Now(), Type: events.PortfolioEventTypeBuy, Amount: 100, - SecurityID: sql.NullString{String: TestSecurity.ID, Valid: true}, + SecurityID: &TestSecurity.ID, Price: currency.Value(100), } @@ -102,7 +102,7 @@ var TestCreateBuyTransactionParams = persistence.CreateTransactionParams{ // [TestBankAccount]. var TestDepositTransaction = &persistence.Transaction{ ID: uuid.NewString(), - DestinationAccountID: sql.NullString{String: TestBankAccount.ID, Valid: true}, + DestinationAccountID: &TestBankAccount.ID, Time: time.Now(), Type: events.PortfolioEventTypeBuy, Amount: 1, diff --git a/persistence/accounts.sql.go b/persistence/accounts.sql.go index 37c4b6a9..b30036e7 100644 --- a/persistence/accounts.sql.go +++ b/persistence/accounts.sql.go @@ -7,7 +7,6 @@ package persistence import ( "context" - "database/sql" "time" currency "github.com/oxisto/money-gopher/currency" @@ -43,7 +42,7 @@ type CreateAccountParams struct { ID string DisplayName string Type accounts.AccountType - ReferenceAccountID sql.NullInt64 + ReferenceAccountID *int64 } func (q *Queries) CreateAccount(ctx context.Context, arg CreateAccountParams) (*Account, error) { @@ -102,11 +101,11 @@ VALUES type CreateTransactionParams struct { ID string - SourceAccountID sql.NullString - DestinationAccountID sql.NullString + SourceAccountID *string + DestinationAccountID *string Time time.Time Type events.PortfolioEventType - SecurityID sql.NullString + SecurityID *string Amount float64 Price *currency.Currency Fees *currency.Currency @@ -314,7 +313,7 @@ WHERE OR destination_account_id = ?1 ` -func (q *Queries) ListTransactionsByAccountID(ctx context.Context, accountID sql.NullString) ([]*Transaction, error) { +func (q *Queries) ListTransactionsByAccountID(ctx context.Context, accountID *string) ([]*Transaction, error) { rows, err := q.db.QueryContext(ctx, listTransactionsByAccountID, accountID) if err != nil { return nil, err diff --git a/persistence/extra.go b/persistence/extra.go index 12171697..9ebd50a3 100644 --- a/persistence/extra.go +++ b/persistence/extra.go @@ -20,9 +20,9 @@ func (s *Security) ListedAs(ctx context.Context, db *DB) ([]*ListedSecurity, err // - amount func (tx *Transaction) MakeUniqueID() { h := fnv.New64a() - h.Write([]byte(tx.SecurityID.String)) - h.Write([]byte(tx.SourceAccountID.String)) - h.Write([]byte(tx.DestinationAccountID.String)) + h.Write([]byte(*tx.SecurityID)) + h.Write([]byte(*tx.SourceAccountID)) + h.Write([]byte(*tx.DestinationAccountID)) h.Write([]byte(tx.Time.Format(time.DateTime))) h.Write([]byte(strconv.FormatInt(int64(tx.Type), 10))) h.Write([]byte(strconv.FormatInt(int64(tx.Amount), 10))) diff --git a/persistence/models.go b/persistence/models.go index 8aa14d72..ff45e158 100644 --- a/persistence/models.go +++ b/persistence/models.go @@ -5,7 +5,6 @@ package persistence import ( - "database/sql" "time" currency "github.com/oxisto/money-gopher/currency" @@ -22,7 +21,7 @@ type Account struct { // Type is the type of the account. Type accounts.AccountType // ReferenceAccountID is the ID of the account that this account is related to. For example, if this is a brokerage account, the reference account could be a bank account. - ReferenceAccountID sql.NullInt64 + ReferenceAccountID *int64 } // ListedSecurity represents a security that is listed on a particular exchange. @@ -36,7 +35,7 @@ type ListedSecurity struct { // LatestQuote is the latest quote for the security as a [currency.Currency]. LatestQuote *currency.Currency // LatestQuoteTimestamp is the timestamp of the latest quote. - LatestQuoteTimestamp sql.NullTime + LatestQuoteTimestamp *time.Time } // Portfolios represent a collection of securities and other positions. @@ -60,7 +59,7 @@ type Security struct { // DisplayName is the human-readable name of the security. DisplayName string // QuoteProvider is the name of the provider that provides quotes for this security. - QuoteProvider sql.NullString + QuoteProvider *string } // Transactions represents a transaction in an account. @@ -68,15 +67,15 @@ type Transaction struct { // ID is the primary identifier for a transaction. ID string // SourceAccountID is the ID of the account that the transaction originated from. - SourceAccountID sql.NullString + SourceAccountID *string // DestinationAccountID is the ID of the account that the transaction is destined for. - DestinationAccountID sql.NullString + DestinationAccountID *string // Time is the time that the transaction occurred. Time time.Time // Type is the type of the transaction. Depending on the type, different fields (source, destination) will be used. Type events.PortfolioEventType // SecurityID is the ID of the security that the transaction is related to. Can be empty if the transaction is not related to a security. - SecurityID sql.NullString + SecurityID *string // Amount is the amount of the transaction. Amount float64 // Price is the price of the transaction. diff --git a/persistence/securities.sql.go b/persistence/securities.sql.go index dbaaa68b..72432ca6 100644 --- a/persistence/securities.sql.go +++ b/persistence/securities.sql.go @@ -7,8 +7,8 @@ package persistence import ( "context" - "database/sql" "strings" + "time" currency "github.com/oxisto/money-gopher/currency" ) @@ -23,7 +23,7 @@ VALUES type CreateSecurityParams struct { ID string DisplayName string - QuoteProvider sql.NullString + QuoteProvider *string } func (q *Queries) CreateSecurity(ctx context.Context, arg CreateSecurityParams) (*Security, error) { @@ -201,7 +201,7 @@ WHERE type UpdateSecurityParams struct { DisplayName string - QuoteProvider sql.NullString + QuoteProvider *string ID string } @@ -236,7 +236,7 @@ type UpsertListedSecurityParams struct { Ticker string Currency string LatestQuote *currency.Currency - LatestQuoteTimestamp sql.NullTime + LatestQuoteTimestamp *time.Time } func (q *Queries) UpsertListedSecurity(ctx context.Context, arg UpsertListedSecurityParams) (*ListedSecurity, error) { diff --git a/securities/quote/quote.go b/securities/quote/quote.go index 2a101893..22301d7c 100644 --- a/securities/quote/quote.go +++ b/securities/quote/quote.go @@ -21,7 +21,6 @@ package quote import ( "context" - "database/sql" "log/slog" "time" @@ -71,14 +70,14 @@ func (qu *qu) UpdateQuotes(ctx context.Context, secIDs []string) (updated []*per } for _, sec := range secs { - if !sec.QuoteProvider.Valid { + if sec.QuoteProvider == nil { slog.Warn("No quote provider configured for security", "security", sec.ID) return } - qp, ok = providers[sec.QuoteProvider.String] + qp, ok = providers[*sec.QuoteProvider] if !ok { - slog.Error("Quote provider not found", "provider", sec.QuoteProvider.String) + slog.Error("Quote provider not found", "provider", *sec.QuoteProvider) return } @@ -133,7 +132,7 @@ func (qu *qu) updateQuote(qp QuoteProvider, ls *persistence.ListedSecurity) (err } ls.LatestQuote = quote - ls.LatestQuoteTimestamp = sql.NullTime{Time: t, Valid: true} + ls.LatestQuoteTimestamp = &t _, err = qu.db.UpsertListedSecurity(ctx, persistence.UpsertListedSecurityParams{ SecurityID: ls.SecurityID, diff --git a/securities/quote/quote_test.go b/securities/quote/quote_test.go index c6f6f169..f90b897d 100644 --- a/securities/quote/quote_test.go +++ b/securities/quote/quote_test.go @@ -2,10 +2,10 @@ package quote import ( "context" - "database/sql" "testing" "github.com/oxisto/assert" + moneygopher "github.com/oxisto/money-gopher" "github.com/oxisto/money-gopher/currency" "github.com/oxisto/money-gopher/internal" "github.com/oxisto/money-gopher/internal/testing/quotetest" @@ -33,7 +33,7 @@ func Test_qu_updateQuote(t *testing.T) { db: internal.NewTestDB(t, func(db *persistence.DB) { _, err := db.CreateSecurity(context.Background(), persistence.CreateSecurityParams{ ID: "My Security", - QuoteProvider: sql.NullString{String: quotetest.QuoteProviderStatic, Valid: true}, + QuoteProvider: moneygopher.Ref(quotetest.QuoteProviderStatic), }) assert.NoError(t, err) _, err = db.UpsertListedSecurity(context.Background(), persistence.UpsertListedSecurityParams{ diff --git a/sqlc.yaml b/sqlc.yaml index be451618..9a0d4fa1 100644 --- a/sqlc.yaml +++ b/sqlc.yaml @@ -8,6 +8,7 @@ sql: package: "persistence" out: "persistence" emit_result_struct_pointers: true + emit_pointers_for_null_types: true overrides: - column: "accounts.type" go_type: github.com/oxisto/money-gopher/portfolio/accounts.AccountType From e232cc2f1be9febbeaacb8b17475f794253981b9 Mon Sep 17 00:00:00 2001 From: Christian Banse Date: Mon, 13 Jan 2025 21:26:13 +0100 Subject: [PATCH 34/35] Create transaction works but some other test fails --- cli/commands/account.go | 77 +++ cli/commands/account_test.go | 52 ++ cli/commands/portfolio.go | 51 +- cli/commands/portfolio_test.go | 44 +- finance/snapshot.go | 4 +- graph/generated.go | 592 ++++++++++++++---- graph/schema.graphqls | 34 +- graph/schema.resolvers.go | 57 +- models/models_gen.go | 25 +- .../sql/migrations/0002_create_accounts.sql | 3 +- portfolio/events/type_string.go | 31 + 11 files changed, 708 insertions(+), 262 deletions(-) create mode 100644 portfolio/events/type_string.go diff --git a/cli/commands/account.go b/cli/commands/account.go index 1fb49401..2e06cf4e 100644 --- a/cli/commands/account.go +++ b/cli/commands/account.go @@ -76,6 +76,25 @@ var AccountCmd = &cli.Command{ &cli.StringFlag{Name: "account-id", Usage: "The ID of the account the transaction is coming from or is destined to", Required: true}, }, }, + { + Name: "create", + Usage: "Creates a transaction. Defaults to a \"buy\" transaction", + Action: CreateTransaction, + Flags: []cli.Flag{ + &cli.StringFlag{Name: "source-account-id", Usage: "The ID of the account the transaction is coming from", Required: true}, + &cli.StringFlag{Name: "destination-account-id", Usage: "The ID of the account the transaction is destined to", Required: true}, + &cli.StringFlag{Name: "security-id", Usage: "The ID of the security this transaction belongs to (its ISIN)", Required: true}, + &cli.GenericFlag{Name: "type", Usage: "The type of the transaction", Required: true, DefaultText: "BUY", Value: func() *events.PortfolioEventType { + var typ events.PortfolioEventType = events.PortfolioEventTypeBuy + return &typ + }()}, + &cli.FloatFlag{Name: "amount", Usage: "The amount of securities involved in the transaction", Required: true}, + &cli.FloatFlag{Name: "price", Usage: "The price without fees or taxes", Required: true}, + &cli.FloatFlag{Name: "fees", Usage: "Any fees that applied to the transaction"}, + &cli.FloatFlag{Name: "taxes", Usage: "Any taxes that applied to the transaction"}, + &cli.StringFlag{Name: "time", Usage: "The time of the transaction. Defaults to 'now'", DefaultText: "now"}, + }, + }, }, }, }, @@ -181,3 +200,61 @@ func ListTransactions(ctx context.Context, cmd *cli.Command) (err error) { return nil } + +// CreateTransaction creates a transaction. +func CreateTransaction(ctx context.Context, cmd *cli.Command) (err error) { + s := mcli.FromContext(ctx) + + var query struct { + CreateTransaction struct { + ID string `json:"id"` + Time string `json:"time"` + } `graphql:"createTransaction(input: $input)" json:"account"` + } + + err = s.GraphQL.Mutate(context.Background(), &query, map[string]interface{}{ + "input": models.TransactionInput{ + Time: time.Now(), + SourceAccountID: cmd.String("source-account-id"), + DestinationAccountID: cmd.String("destination-account-id"), + Type: *cmd.Generic("type").(*events.PortfolioEventType), + SecurityID: cmd.String("security-id"), + Price: &models.CurrencyInput{Amount: int(cmd.Float("price") * 100), Symbol: "EUR"}, + Fees: &models.CurrencyInput{Amount: int(cmd.Float("fees") * 100), Symbol: "EUR"}, + Taxes: &models.CurrencyInput{Amount: int(cmd.Float("taxes") * 100), Symbol: "EUR"}, + Amount: cmd.Float("amount"), + }, + }) + if err != nil { + return err + } + /* + + s := mcli.FromContext(ctx) + var req = connect.NewRequest(&portfoliov1.CreatePortfolioTransactionRequest{ + Transaction: &portfoliov1.PortfolioEvent{ + PortfolioId: cmd.String("portfolio-id"), + SecurityId: cmd.String("security-id"), + Type: eventTypeFrom(cmd.String("type")), + Amount: cmd.Float("amount"), + Time: timeOrNow(cmd.Timestamp("time")), + Price: portfoliov1.Value(int32(cmd.Float("price") * 100)), + Fees: portfoliov1.Value(int32(cmd.Float("fees") * 100)), + Taxes: portfoliov1.Value(int32(cmd.Float("taxes") * 100)), + }, + }) + + res, err := s.PortfolioClient.CreatePortfolioTransaction(context.Background(), req) + if err != nil { + return err + } + + fmt.Printf("Successfully created a %s transaction (%s) for security %s in %s.\n", + color.CyanString(cmd.String("type")), + color.GreenString(res.Msg.Id), + color.CyanString(res.Msg.SecurityId), + color.CyanString(res.Msg.PortfolioId), + )*/ + + return nil +} diff --git a/cli/commands/account_test.go b/cli/commands/account_test.go index 82bbce6f..5089b5da 100644 --- a/cli/commands/account_test.go +++ b/cli/commands/account_test.go @@ -19,6 +19,7 @@ package commands import ( "context" "testing" + "time" "github.com/oxisto/assert" "github.com/oxisto/money-gopher/internal" @@ -197,3 +198,54 @@ func TestListTransactions(t *testing.T) { }) } } + +func TestCreateTransaction(t *testing.T) { + srv := servertest.NewServer(internal.NewTestDB(t, func(db *persistence.DB) { + _, err := db.Queries.CreateAccount(context.Background(), testdata.TestCreateBankAccountParams) + assert.NoError(t, err) + + _, err = db.Queries.CreateAccount(context.Background(), testdata.TestCreateBrokerageAccountParams) + assert.NoError(t, err) + + _, err = db.Queries.CreateSecurity(context.Background(), testdata.TestCreateSecurityParams) + assert.NoError(t, err) + })) + defer srv.Close() + + type args struct { + ctx context.Context + cmd *cli.Command + } + tests := []struct { + name string + args args + wantErr bool + }{ + { + name: "happy path", + args: args{ + ctx: clitest.NewSessionContext(t, srv), + cmd: clitest.MockCommand(t, + AccountCmd.Command("transactions").Command("create").Flags, + "--source-account-id", testdata.TestBankAccount.ID, + "--destination-account-id", testdata.TestBrokerageAccount.ID, + "--security-id", testdata.TestSecurity.ID, + "--type", "BUY", + "--amount", "10", + "--price", "10", + "--fees", "0", + "--taxes", "0", + "--time", time.Now().Format(time.RFC3339), + ), + }, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if err := CreateTransaction(tt.args.ctx, tt.args.cmd); (err != nil) != tt.wantErr { + t.Errorf("CreateTransaction() error = %v, wantErr %v", err, tt.wantErr) + } + }) + } +} diff --git a/cli/commands/portfolio.go b/cli/commands/portfolio.go index 01be5bb9..b4ef30e5 100644 --- a/cli/commands/portfolio.go +++ b/cli/commands/portfolio.go @@ -24,7 +24,6 @@ import ( mcli "github.com/oxisto/money-gopher/cli" "github.com/oxisto/money-gopher/models" - "github.com/oxisto/money-gopher/portfolio/events" "github.com/fatih/color" "github.com/urfave/cli/v3" @@ -64,21 +63,6 @@ var PortfolioCmd = &cli.Command{ Name: "transactions", Usage: "Subcommands supporting transactions within one portfolio", Commands: []*cli.Command{ - { - Name: "create", - Usage: "Creates a transaction. Defaults to a \"buy\" transaction", - Action: CreateTransaction, - Flags: []cli.Flag{ - &cli.StringFlag{Name: "portfolio-id", Usage: "The name of the portfolio where the transaction will be created in", Required: true}, - &cli.StringFlag{Name: "security-id", Usage: "The ID of the security this transaction belongs to (its ISIN)", Required: true}, - &cli.StringFlag{Name: "type", Usage: "The type of the transaction", Required: true, DefaultText: "buy"}, - &cli.FloatFlag{Name: "amount", Usage: "The amount of securities involved in the transaction", Required: true}, - &cli.FloatFlag{Name: "price", Usage: "The price without fees or taxes", Required: true}, - &cli.FloatFlag{Name: "fees", Usage: "Any fees that applied to the transaction"}, - &cli.FloatFlag{Name: "taxes", Usage: "Any taxes that applied to the transaction"}, - &cli.StringFlag{Name: "time", Usage: "The time of the transaction. Defaults to 'now'", DefaultText: "now"}, - }, - }, { Name: "import", Usage: "Imports transactions from CSV", @@ -106,7 +90,7 @@ func ListPortfolio(ctx context.Context, cmd *cli.Command) (err error) { } err = s.GraphQL.Query(context.Background(), &query, map[string]any{ - "when": events.Date(time.Now().Format(time.RFC3339)), + "when": time.Now(), }) if err != nil { return err @@ -199,39 +183,6 @@ func greenOrRed(f float64) string { } } -// CreateTransaction creates a transaction. -func CreateTransaction(ctx context.Context, cmd *cli.Command) error { - /* - - s := mcli.FromContext(ctx) - var req = connect.NewRequest(&portfoliov1.CreatePortfolioTransactionRequest{ - Transaction: &portfoliov1.PortfolioEvent{ - PortfolioId: cmd.String("portfolio-id"), - SecurityId: cmd.String("security-id"), - Type: eventTypeFrom(cmd.String("type")), - Amount: cmd.Float("amount"), - Time: timeOrNow(cmd.Timestamp("time")), - Price: portfoliov1.Value(int32(cmd.Float("price") * 100)), - Fees: portfoliov1.Value(int32(cmd.Float("fees") * 100)), - Taxes: portfoliov1.Value(int32(cmd.Float("taxes") * 100)), - }, - }) - - res, err := s.PortfolioClient.CreatePortfolioTransaction(context.Background(), req) - if err != nil { - return err - } - - fmt.Printf("Successfully created a %s transaction (%s) for security %s in %s.\n", - color.CyanString(cmd.String("type")), - color.GreenString(res.Msg.Id), - color.CyanString(res.Msg.SecurityId), - color.CyanString(res.Msg.PortfolioId), - )*/ - - return nil -} - /* func eventTypeFrom(typ string) portfoliov1.PortfolioEventType { if typ == "buy" { diff --git a/cli/commands/portfolio_test.go b/cli/commands/portfolio_test.go index d1833e23..4d17e3c2 100644 --- a/cli/commands/portfolio_test.go +++ b/cli/commands/portfolio_test.go @@ -44,6 +44,9 @@ func TestListPortfolio(t *testing.T) { err = db.Queries.AddAccountToPortfolio(context.Background(), testdata.TestAddAccountToPortfolioParams) assert.NoError(t, err) + _, err = db.Queries.CreateTransaction(context.Background(), testdata.TestCreateDepositTransactionParams) + assert.NoError(t, err) + _, err = db.Queries.CreateTransaction(context.Background(), testdata.TestCreateBuyTransactionParams) assert.NoError(t, err) })) @@ -158,47 +161,6 @@ func TestShowPortfolio(t *testing.T) { } } -func TestCreateTransaction(t *testing.T) { - srv := servertest.NewServer(internal.NewTestDB(t)) - defer srv.Close() - - type args struct { - ctx context.Context - cmd *cli.Command - } - tests := []struct { - name string - args args - wantErr bool - }{ - { - name: "happy path", - args: args{ - ctx: clitest.NewSessionContext(t, srv), - cmd: clitest.MockCommand(t, - PortfolioCmd.Commands[3].Commands[0].Flags, - "--portfolio-id", "myportfolio", - "--security-id", "mysecurity", - "--type", "buy", - "--amount", "10", - "--price", "10", - "--fees", "0", - "--taxes", "0", - "--time", "2023-01-01", - ), - }, - }, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - if err := CreateTransaction(tt.args.ctx, tt.args.cmd); (err != nil) != tt.wantErr { - t.Errorf("CreateTransaction() error = %v, wantErr %v", err, tt.wantErr) - } - }) - } -} - func TestImportTransactions(t *testing.T) { srv := servertest.NewServer(internal.NewTestDB(t)) defer srv.Close() diff --git a/finance/snapshot.go b/finance/snapshot.go index 6064e836..386db23a 100644 --- a/finance/snapshot.go +++ b/finance/snapshot.go @@ -51,7 +51,7 @@ func BuildSnapshot( // Set up the snapshot snap = &models.PortfolioSnapshot{ - Time: timestamp.Format(time.RFC3339), + Time: timestamp, Positions: make([]*models.PortfolioPosition, 0), TotalPurchaseValue: currency.Zero(), TotalMarketValue: currency.Zero(), @@ -61,7 +61,7 @@ func BuildSnapshot( // Record the first transaction time if len(events) > 0 { - snap.FirstTransactionTime = events[0].Time.Format(time.RFC3339) + snap.FirstTransactionTime = events[0].Time } // Retrieve the event map; a map of events indexed by their security ID diff --git a/graph/generated.go b/graph/generated.go index adff5189..80942d1e 100644 --- a/graph/generated.go +++ b/graph/generated.go @@ -11,6 +11,7 @@ import ( "strconv" "sync" "sync/atomic" + "time" "github.com/99designs/gqlgen/graphql" "github.com/99designs/gqlgen/graphql/introspection" @@ -80,10 +81,12 @@ type ComplexityRoot struct { CreateAccount func(childComplexity int, input models.AccountInput) int CreatePortfolio func(childComplexity int, input models.PortfolioInput) int CreateSecurity func(childComplexity int, input models.SecurityInput) int + CreateTransaction func(childComplexity int, input models.TransactionInput) int DeleteAccount func(childComplexity int, id string) int TriggerQuoteUpdate func(childComplexity int, securityIDs []string) int UpdatePortfolio func(childComplexity int, id string, input models.PortfolioInput) int UpdateSecurity func(childComplexity int, id string, input models.SecurityInput) int + UpdateTransaction func(childComplexity int, id string, input models.TransactionInput) int } Portfolio struct { @@ -91,7 +94,7 @@ type ComplexityRoot struct { DisplayName func(childComplexity int) int Events func(childComplexity int) int ID func(childComplexity int) int - Snapshot func(childComplexity int, when string) int + Snapshot func(childComplexity int, when *time.Time) int } PortfolioEvent struct { @@ -159,8 +162,6 @@ type AccountResolver interface { } type ListedSecurityResolver interface { Security(ctx context.Context, obj *persistence.ListedSecurity) (*persistence.Security, error) - - LatestQuoteTimestamp(ctx context.Context, obj *persistence.ListedSecurity) (*string, error) } type MutationResolver interface { CreateSecurity(ctx context.Context, input models.SecurityInput) (*persistence.Security, error) @@ -169,11 +170,13 @@ type MutationResolver interface { UpdatePortfolio(ctx context.Context, id string, input models.PortfolioInput) (*persistence.Portfolio, error) CreateAccount(ctx context.Context, input models.AccountInput) (*persistence.Account, error) DeleteAccount(ctx context.Context, id string) (*persistence.Account, error) + CreateTransaction(ctx context.Context, input models.TransactionInput) (*persistence.Transaction, error) + UpdateTransaction(ctx context.Context, id string, input models.TransactionInput) (*persistence.Transaction, error) TriggerQuoteUpdate(ctx context.Context, securityIDs []string) ([]*persistence.ListedSecurity, error) } type PortfolioResolver interface { Accounts(ctx context.Context, obj *persistence.Portfolio) ([]*persistence.Account, error) - Snapshot(ctx context.Context, obj *persistence.Portfolio, when string) (*models.PortfolioSnapshot, error) + Snapshot(ctx context.Context, obj *persistence.Portfolio, when *time.Time) (*models.PortfolioSnapshot, error) Events(ctx context.Context, obj *persistence.Portfolio) ([]*models.PortfolioEvent, error) } type QueryResolver interface { @@ -189,7 +192,6 @@ type SecurityResolver interface { ListedAs(ctx context.Context, obj *persistence.Security) ([]*persistence.ListedSecurity, error) } type TransactionResolver interface { - Time(ctx context.Context, obj *persistence.Transaction) (string, error) SourceAccount(ctx context.Context, obj *persistence.Transaction) (*persistence.Account, error) DestinationAccount(ctx context.Context, obj *persistence.Transaction) (*persistence.Account, error) Security(ctx context.Context, obj *persistence.Transaction) (*persistence.Security, error) @@ -327,6 +329,18 @@ func (e *executableSchema) Complexity(typeName, field string, childComplexity in return e.complexity.Mutation.CreateSecurity(childComplexity, args["input"].(models.SecurityInput)), true + case "Mutation.createTransaction": + if e.complexity.Mutation.CreateTransaction == nil { + break + } + + args, err := ec.field_Mutation_createTransaction_args(context.TODO(), rawArgs) + if err != nil { + return 0, false + } + + return e.complexity.Mutation.CreateTransaction(childComplexity, args["input"].(models.TransactionInput)), true + case "Mutation.deleteAccount": if e.complexity.Mutation.DeleteAccount == nil { break @@ -375,6 +389,18 @@ func (e *executableSchema) Complexity(typeName, field string, childComplexity in return e.complexity.Mutation.UpdateSecurity(childComplexity, args["id"].(string), args["input"].(models.SecurityInput)), true + case "Mutation.updateTransaction": + if e.complexity.Mutation.UpdateTransaction == nil { + break + } + + args, err := ec.field_Mutation_updateTransaction_args(context.TODO(), rawArgs) + if err != nil { + return 0, false + } + + return e.complexity.Mutation.UpdateTransaction(childComplexity, args["id"].(string), args["input"].(models.TransactionInput)), true + case "Portfolio.accounts": if e.complexity.Portfolio.Accounts == nil { break @@ -413,7 +439,7 @@ func (e *executableSchema) Complexity(typeName, field string, childComplexity in return 0, false } - return e.complexity.Portfolio.Snapshot(childComplexity, args["when"].(string)), true + return e.complexity.Portfolio.Snapshot(childComplexity, args["when"].(*time.Time)), true case "PortfolioEvent.security": if e.complexity.PortfolioEvent.Security == nil { @@ -731,9 +757,11 @@ func (e *executableSchema) Exec(ctx context.Context) graphql.ResponseHandler { ec := executionContext{opCtx, e, 0, 0, make(chan graphql.DeferredResult)} inputUnmarshalMap := graphql.BuildUnmarshalerMap( ec.unmarshalInputAccountInput, + ec.unmarshalInputCurrencyInput, ec.unmarshalInputListedSecurityInput, ec.unmarshalInputPortfolioInput, ec.unmarshalInputSecurityInput, + ec.unmarshalInputTransactionInput, ) first := true @@ -919,6 +947,29 @@ func (ec *executionContext) field_Mutation_createSecurity_argsInput( return zeroVal, nil } +func (ec *executionContext) field_Mutation_createTransaction_args(ctx context.Context, rawArgs map[string]any) (map[string]any, error) { + var err error + args := map[string]any{} + arg0, err := ec.field_Mutation_createTransaction_argsInput(ctx, rawArgs) + if err != nil { + return nil, err + } + args["input"] = arg0 + return args, nil +} +func (ec *executionContext) field_Mutation_createTransaction_argsInput( + ctx context.Context, + rawArgs map[string]any, +) (models.TransactionInput, error) { + ctx = graphql.WithPathContext(ctx, graphql.NewPathWithField("input")) + if tmp, ok := rawArgs["input"]; ok { + return ec.unmarshalNTransactionInput2githubᚗcomᚋoxistoᚋmoneyᚑgopherᚋmodelsᚐTransactionInput(ctx, tmp) + } + + var zeroVal models.TransactionInput + return zeroVal, nil +} + func (ec *executionContext) field_Mutation_deleteAccount_args(ctx context.Context, rawArgs map[string]any) (map[string]any, error) { var err error args := map[string]any{} @@ -1047,6 +1098,47 @@ func (ec *executionContext) field_Mutation_updateSecurity_argsInput( return zeroVal, nil } +func (ec *executionContext) field_Mutation_updateTransaction_args(ctx context.Context, rawArgs map[string]any) (map[string]any, error) { + var err error + args := map[string]any{} + arg0, err := ec.field_Mutation_updateTransaction_argsID(ctx, rawArgs) + if err != nil { + return nil, err + } + args["id"] = arg0 + arg1, err := ec.field_Mutation_updateTransaction_argsInput(ctx, rawArgs) + if err != nil { + return nil, err + } + args["input"] = arg1 + return args, nil +} +func (ec *executionContext) field_Mutation_updateTransaction_argsID( + ctx context.Context, + rawArgs map[string]any, +) (string, error) { + ctx = graphql.WithPathContext(ctx, graphql.NewPathWithField("id")) + if tmp, ok := rawArgs["id"]; ok { + return ec.unmarshalNString2string(ctx, tmp) + } + + var zeroVal string + return zeroVal, nil +} + +func (ec *executionContext) field_Mutation_updateTransaction_argsInput( + ctx context.Context, + rawArgs map[string]any, +) (models.TransactionInput, error) { + ctx = graphql.WithPathContext(ctx, graphql.NewPathWithField("input")) + if tmp, ok := rawArgs["input"]; ok { + return ec.unmarshalNTransactionInput2githubᚗcomᚋoxistoᚋmoneyᚑgopherᚋmodelsᚐTransactionInput(ctx, tmp) + } + + var zeroVal models.TransactionInput + return zeroVal, nil +} + func (ec *executionContext) field_Portfolio_snapshot_args(ctx context.Context, rawArgs map[string]any) (map[string]any, error) { var err error args := map[string]any{} @@ -1060,13 +1152,13 @@ func (ec *executionContext) field_Portfolio_snapshot_args(ctx context.Context, r func (ec *executionContext) field_Portfolio_snapshot_argsWhen( ctx context.Context, rawArgs map[string]any, -) (string, error) { +) (*time.Time, error) { ctx = graphql.WithPathContext(ctx, graphql.NewPathWithField("when")) if tmp, ok := rawArgs["when"]; ok { - return ec.unmarshalNDate2string(ctx, tmp) + return ec.unmarshalOTime2ᚖtimeᚐTime(ctx, tmp) } - var zeroVal string + var zeroVal *time.Time return zeroVal, nil } @@ -1713,7 +1805,7 @@ func (ec *executionContext) _ListedSecurity_latestQuoteTimestamp(ctx context.Con }() resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (any, error) { ctx = rctx // use context from middleware stack in children - return ec.resolvers.ListedSecurity().LatestQuoteTimestamp(rctx, obj) + return obj.LatestQuoteTimestamp, nil }) if err != nil { ec.Error(ctx, err) @@ -1722,19 +1814,19 @@ func (ec *executionContext) _ListedSecurity_latestQuoteTimestamp(ctx context.Con if resTmp == nil { return graphql.Null } - res := resTmp.(*string) + res := resTmp.(*time.Time) fc.Result = res - return ec.marshalODate2ᚖstring(ctx, field.Selections, res) + return ec.marshalOTime2ᚖtimeᚐTime(ctx, field.Selections, res) } func (ec *executionContext) fieldContext_ListedSecurity_latestQuoteTimestamp(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { fc = &graphql.FieldContext{ Object: "ListedSecurity", Field: field, - IsMethod: true, - IsResolver: true, + IsMethod: false, + IsResolver: false, Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { - return nil, errors.New("field of type Date does not have child fields") + return nil, errors.New("field of type Time does not have child fields") }, } return fc, nil @@ -2134,6 +2226,156 @@ func (ec *executionContext) fieldContext_Mutation_deleteAccount(ctx context.Cont return fc, nil } +func (ec *executionContext) _Mutation_createTransaction(ctx context.Context, field graphql.CollectedField) (ret graphql.Marshaler) { + fc, err := ec.fieldContext_Mutation_createTransaction(ctx, field) + if err != nil { + return graphql.Null + } + ctx = graphql.WithFieldContext(ctx, fc) + defer func() { + if r := recover(); r != nil { + ec.Error(ctx, ec.Recover(ctx, r)) + ret = graphql.Null + } + }() + resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (any, error) { + ctx = rctx // use context from middleware stack in children + return ec.resolvers.Mutation().CreateTransaction(rctx, fc.Args["input"].(models.TransactionInput)) + }) + if err != nil { + ec.Error(ctx, err) + return graphql.Null + } + if resTmp == nil { + if !graphql.HasFieldError(ctx, fc) { + ec.Errorf(ctx, "must not be null") + } + return graphql.Null + } + res := resTmp.(*persistence.Transaction) + fc.Result = res + return ec.marshalNTransaction2ᚖgithubᚗcomᚋoxistoᚋmoneyᚑgopherᚋpersistenceᚐTransaction(ctx, field.Selections, res) +} + +func (ec *executionContext) fieldContext_Mutation_createTransaction(ctx context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { + fc = &graphql.FieldContext{ + Object: "Mutation", + Field: field, + IsMethod: true, + IsResolver: true, + Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + switch field.Name { + case "id": + return ec.fieldContext_Transaction_id(ctx, field) + case "time": + return ec.fieldContext_Transaction_time(ctx, field) + case "sourceAccount": + return ec.fieldContext_Transaction_sourceAccount(ctx, field) + case "destinationAccount": + return ec.fieldContext_Transaction_destinationAccount(ctx, field) + case "security": + return ec.fieldContext_Transaction_security(ctx, field) + case "amount": + return ec.fieldContext_Transaction_amount(ctx, field) + case "price": + return ec.fieldContext_Transaction_price(ctx, field) + case "fees": + return ec.fieldContext_Transaction_fees(ctx, field) + case "type": + return ec.fieldContext_Transaction_type(ctx, field) + } + return nil, fmt.Errorf("no field named %q was found under type Transaction", field.Name) + }, + } + defer func() { + if r := recover(); r != nil { + err = ec.Recover(ctx, r) + ec.Error(ctx, err) + } + }() + ctx = graphql.WithFieldContext(ctx, fc) + if fc.Args, err = ec.field_Mutation_createTransaction_args(ctx, field.ArgumentMap(ec.Variables)); err != nil { + ec.Error(ctx, err) + return fc, err + } + return fc, nil +} + +func (ec *executionContext) _Mutation_updateTransaction(ctx context.Context, field graphql.CollectedField) (ret graphql.Marshaler) { + fc, err := ec.fieldContext_Mutation_updateTransaction(ctx, field) + if err != nil { + return graphql.Null + } + ctx = graphql.WithFieldContext(ctx, fc) + defer func() { + if r := recover(); r != nil { + ec.Error(ctx, ec.Recover(ctx, r)) + ret = graphql.Null + } + }() + resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (any, error) { + ctx = rctx // use context from middleware stack in children + return ec.resolvers.Mutation().UpdateTransaction(rctx, fc.Args["id"].(string), fc.Args["input"].(models.TransactionInput)) + }) + if err != nil { + ec.Error(ctx, err) + return graphql.Null + } + if resTmp == nil { + if !graphql.HasFieldError(ctx, fc) { + ec.Errorf(ctx, "must not be null") + } + return graphql.Null + } + res := resTmp.(*persistence.Transaction) + fc.Result = res + return ec.marshalNTransaction2ᚖgithubᚗcomᚋoxistoᚋmoneyᚑgopherᚋpersistenceᚐTransaction(ctx, field.Selections, res) +} + +func (ec *executionContext) fieldContext_Mutation_updateTransaction(ctx context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { + fc = &graphql.FieldContext{ + Object: "Mutation", + Field: field, + IsMethod: true, + IsResolver: true, + Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + switch field.Name { + case "id": + return ec.fieldContext_Transaction_id(ctx, field) + case "time": + return ec.fieldContext_Transaction_time(ctx, field) + case "sourceAccount": + return ec.fieldContext_Transaction_sourceAccount(ctx, field) + case "destinationAccount": + return ec.fieldContext_Transaction_destinationAccount(ctx, field) + case "security": + return ec.fieldContext_Transaction_security(ctx, field) + case "amount": + return ec.fieldContext_Transaction_amount(ctx, field) + case "price": + return ec.fieldContext_Transaction_price(ctx, field) + case "fees": + return ec.fieldContext_Transaction_fees(ctx, field) + case "type": + return ec.fieldContext_Transaction_type(ctx, field) + } + return nil, fmt.Errorf("no field named %q was found under type Transaction", field.Name) + }, + } + defer func() { + if r := recover(); r != nil { + err = ec.Recover(ctx, r) + ec.Error(ctx, err) + } + }() + ctx = graphql.WithFieldContext(ctx, fc) + if fc.Args, err = ec.field_Mutation_updateTransaction_args(ctx, field.ArgumentMap(ec.Variables)); err != nil { + ec.Error(ctx, err) + return fc, err + } + return fc, nil +} + func (ec *executionContext) _Mutation_triggerQuoteUpdate(ctx context.Context, field graphql.CollectedField) (ret graphql.Marshaler) { fc, err := ec.fieldContext_Mutation_triggerQuoteUpdate(ctx, field) if err != nil { @@ -2357,7 +2599,7 @@ func (ec *executionContext) _Portfolio_snapshot(ctx context.Context, field graph }() resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (any, error) { ctx = rctx // use context from middleware stack in children - return ec.resolvers.Portfolio().Snapshot(rctx, obj, fc.Args["when"].(string)) + return ec.resolvers.Portfolio().Snapshot(rctx, obj, fc.Args["when"].(*time.Time)) }) if err != nil { ec.Error(ctx, err) @@ -2493,9 +2735,9 @@ func (ec *executionContext) _PortfolioEvent_time(ctx context.Context, field grap } return graphql.Null } - res := resTmp.(string) + res := resTmp.(time.Time) fc.Result = res - return ec.marshalNDate2string(ctx, field.Selections, res) + return ec.marshalNTime2timeᚐTime(ctx, field.Selections, res) } func (ec *executionContext) fieldContext_PortfolioEvent_time(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { @@ -2505,7 +2747,7 @@ func (ec *executionContext) fieldContext_PortfolioEvent_time(_ context.Context, IsMethod: false, IsResolver: false, Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { - return nil, errors.New("field of type Date does not have child fields") + return nil, errors.New("field of type Time does not have child fields") }, } return fc, nil @@ -3074,9 +3316,9 @@ func (ec *executionContext) _PortfolioSnapshot_time(ctx context.Context, field g } return graphql.Null } - res := resTmp.(string) + res := resTmp.(time.Time) fc.Result = res - return ec.marshalNDate2string(ctx, field.Selections, res) + return ec.marshalNTime2timeᚐTime(ctx, field.Selections, res) } func (ec *executionContext) fieldContext_PortfolioSnapshot_time(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { @@ -3086,7 +3328,7 @@ func (ec *executionContext) fieldContext_PortfolioSnapshot_time(_ context.Contex IsMethod: false, IsResolver: false, Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { - return nil, errors.New("field of type Date does not have child fields") + return nil, errors.New("field of type Time does not have child fields") }, } return fc, nil @@ -3182,9 +3424,9 @@ func (ec *executionContext) _PortfolioSnapshot_firstTransactionTime(ctx context. } return graphql.Null } - res := resTmp.(string) + res := resTmp.(time.Time) fc.Result = res - return ec.marshalNDate2string(ctx, field.Selections, res) + return ec.marshalNTime2timeᚐTime(ctx, field.Selections, res) } func (ec *executionContext) fieldContext_PortfolioSnapshot_firstTransactionTime(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { @@ -3194,7 +3436,7 @@ func (ec *executionContext) fieldContext_PortfolioSnapshot_firstTransactionTime( IsMethod: false, IsResolver: false, Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { - return nil, errors.New("field of type Date does not have child fields") + return nil, errors.New("field of type Time does not have child fields") }, } return fc, nil @@ -4287,7 +4529,7 @@ func (ec *executionContext) _Transaction_time(ctx context.Context, field graphql }() resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (any, error) { ctx = rctx // use context from middleware stack in children - return ec.resolvers.Transaction().Time(rctx, obj) + return obj.Time, nil }) if err != nil { ec.Error(ctx, err) @@ -4299,19 +4541,19 @@ func (ec *executionContext) _Transaction_time(ctx context.Context, field graphql } return graphql.Null } - res := resTmp.(string) + res := resTmp.(time.Time) fc.Result = res - return ec.marshalNDate2string(ctx, field.Selections, res) + return ec.marshalNTime2timeᚐTime(ctx, field.Selections, res) } func (ec *executionContext) fieldContext_Transaction_time(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { fc = &graphql.FieldContext{ Object: "Transaction", Field: field, - IsMethod: true, - IsResolver: true, + IsMethod: false, + IsResolver: false, Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { - return nil, errors.New("field of type Date does not have child fields") + return nil, errors.New("field of type Time does not have child fields") }, } return fc, nil @@ -6481,6 +6723,40 @@ func (ec *executionContext) unmarshalInputAccountInput(ctx context.Context, obj return it, nil } +func (ec *executionContext) unmarshalInputCurrencyInput(ctx context.Context, obj any) (models.CurrencyInput, error) { + var it models.CurrencyInput + asMap := map[string]any{} + for k, v := range obj.(map[string]any) { + asMap[k] = v + } + + fieldsInOrder := [...]string{"amount", "symbol"} + for _, k := range fieldsInOrder { + v, ok := asMap[k] + if !ok { + continue + } + switch k { + case "amount": + ctx := graphql.WithPathContext(ctx, graphql.NewPathWithField("amount")) + data, err := ec.unmarshalNInt2int(ctx, v) + if err != nil { + return it, err + } + it.Amount = data + case "symbol": + ctx := graphql.WithPathContext(ctx, graphql.NewPathWithField("symbol")) + data, err := ec.unmarshalNString2string(ctx, v) + if err != nil { + return it, err + } + it.Symbol = data + } + } + + return it, nil +} + func (ec *executionContext) unmarshalInputListedSecurityInput(ctx context.Context, obj any) (models.ListedSecurityInput, error) { var it models.ListedSecurityInput asMap := map[string]any{} @@ -6597,6 +6873,89 @@ func (ec *executionContext) unmarshalInputSecurityInput(ctx context.Context, obj return it, nil } +func (ec *executionContext) unmarshalInputTransactionInput(ctx context.Context, obj any) (models.TransactionInput, error) { + var it models.TransactionInput + asMap := map[string]any{} + for k, v := range obj.(map[string]any) { + asMap[k] = v + } + + fieldsInOrder := [...]string{"time", "sourceAccountID", "destinationAccountID", "securityID", "amount", "price", "fees", "taxes", "type"} + for _, k := range fieldsInOrder { + v, ok := asMap[k] + if !ok { + continue + } + switch k { + case "time": + ctx := graphql.WithPathContext(ctx, graphql.NewPathWithField("time")) + data, err := ec.unmarshalNTime2timeᚐTime(ctx, v) + if err != nil { + return it, err + } + it.Time = data + case "sourceAccountID": + ctx := graphql.WithPathContext(ctx, graphql.NewPathWithField("sourceAccountID")) + data, err := ec.unmarshalNString2string(ctx, v) + if err != nil { + return it, err + } + it.SourceAccountID = data + case "destinationAccountID": + ctx := graphql.WithPathContext(ctx, graphql.NewPathWithField("destinationAccountID")) + data, err := ec.unmarshalNString2string(ctx, v) + if err != nil { + return it, err + } + it.DestinationAccountID = data + case "securityID": + ctx := graphql.WithPathContext(ctx, graphql.NewPathWithField("securityID")) + data, err := ec.unmarshalNString2string(ctx, v) + if err != nil { + return it, err + } + it.SecurityID = data + case "amount": + ctx := graphql.WithPathContext(ctx, graphql.NewPathWithField("amount")) + data, err := ec.unmarshalNFloat2float64(ctx, v) + if err != nil { + return it, err + } + it.Amount = data + case "price": + ctx := graphql.WithPathContext(ctx, graphql.NewPathWithField("price")) + data, err := ec.unmarshalNCurrencyInput2ᚖgithubᚗcomᚋoxistoᚋmoneyᚑgopherᚋmodelsᚐCurrencyInput(ctx, v) + if err != nil { + return it, err + } + it.Price = data + case "fees": + ctx := graphql.WithPathContext(ctx, graphql.NewPathWithField("fees")) + data, err := ec.unmarshalNCurrencyInput2ᚖgithubᚗcomᚋoxistoᚋmoneyᚑgopherᚋmodelsᚐCurrencyInput(ctx, v) + if err != nil { + return it, err + } + it.Fees = data + case "taxes": + ctx := graphql.WithPathContext(ctx, graphql.NewPathWithField("taxes")) + data, err := ec.unmarshalNCurrencyInput2ᚖgithubᚗcomᚋoxistoᚋmoneyᚑgopherᚋmodelsᚐCurrencyInput(ctx, v) + if err != nil { + return it, err + } + it.Taxes = data + case "type": + ctx := graphql.WithPathContext(ctx, graphql.NewPathWithField("type")) + data, err := ec.unmarshalNPortfolioEventType2githubᚗcomᚋoxistoᚋmoneyᚑgopherᚋportfolioᚋeventsᚐPortfolioEventType(ctx, v) + if err != nil { + return it, err + } + it.Type = data + } + } + + return it, nil +} + // endregion **************************** input.gotpl ***************************** // region ************************** interface.gotpl *************************** @@ -6791,38 +7150,7 @@ func (ec *executionContext) _ListedSecurity(ctx context.Context, sel ast.Selecti case "latestQuote": out.Values[i] = ec._ListedSecurity_latestQuote(ctx, field, obj) case "latestQuoteTimestamp": - field := field - - innerFunc := func(ctx context.Context, _ *graphql.FieldSet) (res graphql.Marshaler) { - defer func() { - if r := recover(); r != nil { - ec.Error(ctx, ec.Recover(ctx, r)) - } - }() - res = ec._ListedSecurity_latestQuoteTimestamp(ctx, field, obj) - return res - } - - if field.Deferrable != nil { - dfs, ok := deferred[field.Deferrable.Label] - di := 0 - if ok { - dfs.AddField(field) - di = len(dfs.Values) - 1 - } else { - dfs = graphql.NewFieldSet([]graphql.CollectedField{field}) - deferred[field.Deferrable.Label] = dfs - } - dfs.Concurrently(di, func(ctx context.Context) graphql.Marshaler { - return innerFunc(ctx, dfs) - }) - - // don't run the out.Concurrently() call below - out.Values[i] = graphql.Null - continue - } - - out.Concurrently(i, func(ctx context.Context) graphql.Marshaler { return innerFunc(ctx, out) }) + out.Values[i] = ec._ListedSecurity_latestQuoteTimestamp(ctx, field, obj) default: panic("unknown field " + strconv.Quote(field.Name)) } @@ -6907,6 +7235,20 @@ func (ec *executionContext) _Mutation(ctx context.Context, sel ast.SelectionSet) if out.Values[i] == graphql.Null { out.Invalids++ } + case "createTransaction": + out.Values[i] = ec.OperationContext.RootResolverMiddleware(innerCtx, func(ctx context.Context) (res graphql.Marshaler) { + return ec._Mutation_createTransaction(ctx, field) + }) + if out.Values[i] == graphql.Null { + out.Invalids++ + } + case "updateTransaction": + out.Values[i] = ec.OperationContext.RootResolverMiddleware(innerCtx, func(ctx context.Context) (res graphql.Marshaler) { + return ec._Mutation_updateTransaction(ctx, field) + }) + if out.Values[i] == graphql.Null { + out.Invalids++ + } case "triggerQuoteUpdate": out.Values[i] = ec.OperationContext.RootResolverMiddleware(innerCtx, func(ctx context.Context) (res graphql.Marshaler) { return ec._Mutation_triggerQuoteUpdate(ctx, field) @@ -7578,41 +7920,10 @@ func (ec *executionContext) _Transaction(ctx context.Context, sel ast.SelectionS atomic.AddUint32(&out.Invalids, 1) } case "time": - field := field - - innerFunc := func(ctx context.Context, fs *graphql.FieldSet) (res graphql.Marshaler) { - defer func() { - if r := recover(); r != nil { - ec.Error(ctx, ec.Recover(ctx, r)) - } - }() - res = ec._Transaction_time(ctx, field, obj) - if res == graphql.Null { - atomic.AddUint32(&fs.Invalids, 1) - } - return res - } - - if field.Deferrable != nil { - dfs, ok := deferred[field.Deferrable.Label] - di := 0 - if ok { - dfs.AddField(field) - di = len(dfs.Values) - 1 - } else { - dfs = graphql.NewFieldSet([]graphql.CollectedField{field}) - deferred[field.Deferrable.Label] = dfs - } - dfs.Concurrently(di, func(ctx context.Context) graphql.Marshaler { - return innerFunc(ctx, dfs) - }) - - // don't run the out.Concurrently() call below - out.Values[i] = graphql.Null - continue + out.Values[i] = ec._Transaction_time(ctx, field, obj) + if out.Values[i] == graphql.Null { + atomic.AddUint32(&out.Invalids, 1) } - - out.Concurrently(i, func(ctx context.Context) graphql.Marshaler { return innerFunc(ctx, out) }) case "sourceAccount": field := field @@ -8207,19 +8518,9 @@ func (ec *executionContext) marshalNCurrency2ᚖgithubᚗcomᚋoxistoᚋmoneyᚑ return ec._Currency(ctx, sel, v) } -func (ec *executionContext) unmarshalNDate2string(ctx context.Context, v any) (string, error) { - res, err := graphql.UnmarshalString(v) - return res, graphql.ErrorOnPath(ctx, err) -} - -func (ec *executionContext) marshalNDate2string(ctx context.Context, sel ast.SelectionSet, v string) graphql.Marshaler { - res := graphql.MarshalString(v) - if res == graphql.Null { - if !graphql.HasFieldError(ctx, graphql.GetFieldContext(ctx)) { - ec.Errorf(ctx, "the requested element is null which the schema does not allow") - } - } - return res +func (ec *executionContext) unmarshalNCurrencyInput2ᚖgithubᚗcomᚋoxistoᚋmoneyᚑgopherᚋmodelsᚐCurrencyInput(ctx context.Context, v any) (*models.CurrencyInput, error) { + res, err := ec.unmarshalInputCurrencyInput(ctx, v) + return &res, graphql.ErrorOnPath(ctx, err) } func (ec *executionContext) unmarshalNFloat2float64(ctx context.Context, v any) (float64, error) { @@ -8252,6 +8553,21 @@ func (ec *executionContext) marshalNID2string(ctx context.Context, sel ast.Selec return res } +func (ec *executionContext) unmarshalNInt2int(ctx context.Context, v any) (int, error) { + res, err := graphql.UnmarshalInt(v) + return res, graphql.ErrorOnPath(ctx, err) +} + +func (ec *executionContext) marshalNInt2int(ctx context.Context, sel ast.SelectionSet, v int) graphql.Marshaler { + res := graphql.MarshalInt(v) + if res == graphql.Null { + if !graphql.HasFieldError(ctx, graphql.GetFieldContext(ctx)) { + ec.Errorf(ctx, "the requested element is null which the schema does not allow") + } + } + return res +} + func (ec *executionContext) unmarshalNInt2int32(ctx context.Context, v any) (int32, error) { res, err := graphql.UnmarshalInt32(v) return res, graphql.ErrorOnPath(ctx, err) @@ -8638,6 +8954,25 @@ func (ec *executionContext) marshalNString2ᚕstringᚄ(ctx context.Context, sel return ret } +func (ec *executionContext) unmarshalNTime2timeᚐTime(ctx context.Context, v any) (time.Time, error) { + res, err := graphql.UnmarshalTime(v) + return res, graphql.ErrorOnPath(ctx, err) +} + +func (ec *executionContext) marshalNTime2timeᚐTime(ctx context.Context, sel ast.SelectionSet, v time.Time) graphql.Marshaler { + res := graphql.MarshalTime(v) + if res == graphql.Null { + if !graphql.HasFieldError(ctx, graphql.GetFieldContext(ctx)) { + ec.Errorf(ctx, "the requested element is null which the schema does not allow") + } + } + return res +} + +func (ec *executionContext) marshalNTransaction2githubᚗcomᚋoxistoᚋmoneyᚑgopherᚋpersistenceᚐTransaction(ctx context.Context, sel ast.SelectionSet, v persistence.Transaction) graphql.Marshaler { + return ec._Transaction(ctx, sel, &v) +} + func (ec *executionContext) marshalNTransaction2ᚕᚖgithubᚗcomᚋoxistoᚋmoneyᚑgopherᚋpersistenceᚐTransactionᚄ(ctx context.Context, sel ast.SelectionSet, v []*persistence.Transaction) graphql.Marshaler { ret := make(graphql.Array, len(v)) var wg sync.WaitGroup @@ -8692,6 +9027,11 @@ func (ec *executionContext) marshalNTransaction2ᚖgithubᚗcomᚋoxistoᚋmoney return ec._Transaction(ctx, sel, v) } +func (ec *executionContext) unmarshalNTransactionInput2githubᚗcomᚋoxistoᚋmoneyᚑgopherᚋmodelsᚐTransactionInput(ctx context.Context, v any) (models.TransactionInput, error) { + res, err := ec.unmarshalInputTransactionInput(ctx, v) + return res, graphql.ErrorOnPath(ctx, err) +} + func (ec *executionContext) marshalN__Directive2githubᚗcomᚋ99designsᚋgqlgenᚋgraphqlᚋintrospectionᚐDirective(ctx context.Context, sel ast.SelectionSet, v introspection.Directive) graphql.Marshaler { return ec.___Directive(ctx, sel, &v) } @@ -8985,22 +9325,6 @@ func (ec *executionContext) marshalOCurrency2ᚖgithubᚗcomᚋoxistoᚋmoneyᚑ return ec._Currency(ctx, sel, v) } -func (ec *executionContext) unmarshalODate2ᚖstring(ctx context.Context, v any) (*string, error) { - if v == nil { - return nil, nil - } - res, err := graphql.UnmarshalString(v) - return &res, graphql.ErrorOnPath(ctx, err) -} - -func (ec *executionContext) marshalODate2ᚖstring(ctx context.Context, sel ast.SelectionSet, v *string) graphql.Marshaler { - if v == nil { - return graphql.Null - } - res := graphql.MarshalString(*v) - return res -} - func (ec *executionContext) marshalOListedSecurity2ᚕᚖgithubᚗcomᚋoxistoᚋmoneyᚑgopherᚋpersistenceᚐListedSecurityᚄ(ctx context.Context, sel ast.SelectionSet, v []*persistence.ListedSecurity) graphql.Marshaler { if v == nil { return graphql.Null @@ -9150,6 +9474,22 @@ func (ec *executionContext) marshalOString2ᚖstring(ctx context.Context, sel as return res } +func (ec *executionContext) unmarshalOTime2ᚖtimeᚐTime(ctx context.Context, v any) (*time.Time, error) { + if v == nil { + return nil, nil + } + res, err := graphql.UnmarshalTime(v) + return &res, graphql.ErrorOnPath(ctx, err) +} + +func (ec *executionContext) marshalOTime2ᚖtimeᚐTime(ctx context.Context, sel ast.SelectionSet, v *time.Time) graphql.Marshaler { + if v == nil { + return graphql.Null + } + res := graphql.MarshalTime(*v) + return res +} + func (ec *executionContext) marshalO__EnumValue2ᚕgithubᚗcomᚋ99designsᚋgqlgenᚋgraphqlᚋintrospectionᚐEnumValueᚄ(ctx context.Context, sel ast.SelectionSet, v []introspection.EnumValue) graphql.Marshaler { if v == nil { return graphql.Null diff --git a/graph/schema.graphqls b/graph/schema.graphqls index ab2e1b5d..b2237336 100644 --- a/graph/schema.graphqls +++ b/graph/schema.graphqls @@ -5,7 +5,7 @@ directive @goModel( directive @goEnum(value: String) on ENUM_VALUE -scalar Date +scalar Time type Currency { amount: Int! @@ -24,14 +24,14 @@ type ListedSecurity { currency: String! security: Security! latestQuote: Currency - latestQuoteTimestamp: Date + latestQuoteTimestamp: Time } type Portfolio { id: String! displayName: String! accounts: [Account!]! - snapshot(when: Date!): PortfolioSnapshot + snapshot(when: Time): PortfolioSnapshot events: [PortfolioEvent!]! } @@ -88,14 +88,14 @@ enum AccountType } type PortfolioEvent { - time: Date! + time: Time! type: PortfolioEventType! security: Security } type Transaction { id: String! - time: Date! + time: Time! sourceAccount: Account! destinationAccount: Account! security: Security! @@ -106,9 +106,9 @@ type Transaction { } type PortfolioSnapshot { - time: Date! + time: Time! positions: [PortfolioPosition!]! - firstTransactionTime: Date! + firstTransactionTime: Time! totalPurchaseValue: Currency! totalMarketValue: Currency! totalProfitOrLoss: Currency! @@ -192,6 +192,23 @@ input ListedSecurityInput { currency: String! } +input TransactionInput { + time: Time! + sourceAccountID: String! + destinationAccountID: String! + securityID: String! + amount: Float! + price: CurrencyInput! + fees: CurrencyInput! + taxes: CurrencyInput! + type: PortfolioEventType! +} + +input CurrencyInput { + amount: Int! + symbol: String! +} + type Mutation { createSecurity(input: SecurityInput!): Security! updateSecurity(id: ID!, input: SecurityInput!): Security! @@ -202,6 +219,9 @@ type Mutation { createAccount(input: AccountInput!): Account! deleteAccount(id: String!): Account! + createTransaction(input: TransactionInput!): Transaction! + updateTransaction(id: String!, input: TransactionInput!): Transaction! + """ Triggers a quote update for the given security IDs. If no security IDs are provided, all securities will be updated. diff --git a/graph/schema.resolvers.go b/graph/schema.resolvers.go index a3bf867d..536c057b 100644 --- a/graph/schema.resolvers.go +++ b/graph/schema.resolvers.go @@ -10,6 +10,7 @@ import ( "slices" "time" + "github.com/oxisto/money-gopher/currency" "github.com/oxisto/money-gopher/finance" "github.com/oxisto/money-gopher/models" "github.com/oxisto/money-gopher/persistence" @@ -25,11 +26,6 @@ func (r *listedSecurityResolver) Security(ctx context.Context, obj *persistence. panic(fmt.Errorf("not implemented: Security - security")) } -// LatestQuoteTimestamp is the resolver for the latestQuoteTimestamp field. -func (r *listedSecurityResolver) LatestQuoteTimestamp(ctx context.Context, obj *persistence.ListedSecurity) (*string, error) { - panic(fmt.Errorf("not implemented: LatestQuoteTimestamp - latestQuoteTimestamp")) -} - // CreateSecurity is the resolver for the createSecurity field. func (r *mutationResolver) CreateSecurity(ctx context.Context, input models.SecurityInput) (*persistence.Security, error) { return withTx(r.Resolver, func(qtx *persistence.Queries) (*persistence.Security, error) { @@ -169,6 +165,27 @@ func (r *mutationResolver) DeleteAccount(ctx context.Context, id string) (*persi return r.DB.DeleteAccount(ctx, id) } +// CreateTransaction is the resolver for the createTransaction field. +func (r *mutationResolver) CreateTransaction(ctx context.Context, input models.TransactionInput) (*persistence.Transaction, error) { + return r.DB.CreateTransaction(ctx, persistence.CreateTransactionParams{ + ID: "newID", + Time: input.Time, + SourceAccountID: &input.SourceAccountID, + DestinationAccountID: &input.DestinationAccountID, + SecurityID: &input.SecurityID, + Type: input.Type, + Amount: input.Amount, + Price: ¤cy.Currency{Amount: int32(input.Price.Amount), Symbol: input.Price.Symbol}, + Fees: ¤cy.Currency{Amount: int32(input.Fees.Amount), Symbol: input.Fees.Symbol}, + Taxes: ¤cy.Currency{Amount: int32(input.Taxes.Amount), Symbol: input.Taxes.Symbol}, + }) +} + +// UpdateTransaction is the resolver for the updateTransaction field. +func (r *mutationResolver) UpdateTransaction(ctx context.Context, id string, input models.TransactionInput) (*persistence.Transaction, error) { + panic(fmt.Errorf("not implemented: UpdateTransaction - updateTransaction")) +} + // TriggerQuoteUpdate is the resolver for the triggerQuoteUpdate field. func (r *mutationResolver) TriggerQuoteUpdate(ctx context.Context, securityIDs []string) (updated []*persistence.ListedSecurity, err error) { updated, err = r.QuoteUpdater.UpdateQuotes(ctx, securityIDs) @@ -185,16 +202,13 @@ func (r *portfolioResolver) Accounts(ctx context.Context, obj *persistence.Portf } // Snapshot is the resolver for the snapshot field. -func (r *portfolioResolver) Snapshot(ctx context.Context, obj *persistence.Portfolio, when string) (snap *models.PortfolioSnapshot, err error) { +func (r *portfolioResolver) Snapshot(ctx context.Context, obj *persistence.Portfolio, when *time.Time) (snap *models.PortfolioSnapshot, err error) { var t time.Time - if when == "" { + if when == nil { t = time.Now() } else { - t, err = time.Parse(time.RFC3339, when) - if err != nil { - return nil, err - } + t = *when } return finance.BuildSnapshot(ctx, t, obj.ID, r.DB) @@ -245,11 +259,6 @@ func (r *securityResolver) ListedAs(ctx context.Context, obj *persistence.Securi return obj.ListedAs(ctx, r.DB) } -// Time is the resolver for the time field. -func (r *transactionResolver) Time(ctx context.Context, obj *persistence.Transaction) (string, error) { - return obj.Time.Format(time.RFC3339), nil -} - // SourceAccount is the resolver for the sourceAccount field. func (r *transactionResolver) SourceAccount(ctx context.Context, obj *persistence.Transaction) (*persistence.Account, error) { panic(fmt.Errorf("not implemented: SourceAccount - sourceAccount")) @@ -293,19 +302,3 @@ type portfolioResolver struct{ *Resolver } type queryResolver struct{ *Resolver } type securityResolver struct{ *Resolver } type transactionResolver struct{ *Resolver } - -// !!! WARNING !!! -// The code below was going to be deleted when updating resolvers. It has been copied here so you have -// one last chance to move it out of harms way if you want. There are two reasons this happens: -// - When renaming or deleting a resolver the old code will be put in here. You can safely delete -// it when you're done. -// - You have helper methods in this file. Move them out to keep these resolver files clean. -/* - func (r *securityResolver) QuoteProvider(ctx context.Context, obj *persistence.Security) (*string, error) { - if obj.QuoteProvider.Valid { - return &obj.QuoteProvider.String, nil - } - - return nil, nil -} -*/ diff --git a/models/models_gen.go b/models/models_gen.go index 35a72cac..b4dfa943 100644 --- a/models/models_gen.go +++ b/models/models_gen.go @@ -3,6 +3,8 @@ package models import ( + "time" + "github.com/oxisto/money-gopher/currency" "github.com/oxisto/money-gopher/persistence" "github.com/oxisto/money-gopher/portfolio/accounts" @@ -15,6 +17,11 @@ type AccountInput struct { Type accounts.AccountType `json:"type"` } +type CurrencyInput struct { + Amount int `json:"amount"` + Symbol string `json:"symbol"` +} + type ListedSecurityInput struct { Ticker string `json:"ticker"` Currency string `json:"currency"` @@ -24,7 +31,7 @@ type Mutation struct { } type PortfolioEvent struct { - Time string `json:"time"` + Time time.Time `json:"time"` Type events.PortfolioEventType `json:"type"` Security *persistence.Security `json:"security,omitempty"` } @@ -60,9 +67,9 @@ type PortfolioPosition struct { } type PortfolioSnapshot struct { - Time string `json:"time"` + Time time.Time `json:"time"` Positions []*PortfolioPosition `json:"positions"` - FirstTransactionTime string `json:"firstTransactionTime"` + FirstTransactionTime time.Time `json:"firstTransactionTime"` TotalPurchaseValue *currency.Currency `json:"totalPurchaseValue"` TotalMarketValue *currency.Currency `json:"totalMarketValue"` TotalProfitOrLoss *currency.Currency `json:"totalProfitOrLoss"` @@ -79,3 +86,15 @@ type SecurityInput struct { DisplayName string `json:"displayName"` ListedAs []*ListedSecurityInput `json:"listedAs,omitempty"` } + +type TransactionInput struct { + Time time.Time `json:"time"` + SourceAccountID string `json:"sourceAccountID"` + DestinationAccountID string `json:"destinationAccountID"` + SecurityID string `json:"securityID"` + Amount float64 `json:"amount"` + Price *CurrencyInput `json:"price"` + Fees *CurrencyInput `json:"fees"` + Taxes *CurrencyInput `json:"taxes"` + Type events.PortfolioEventType `json:"type"` +} diff --git a/persistence/sql/migrations/0002_create_accounts.sql b/persistence/sql/migrations/0002_create_accounts.sql index f357246d..f14867af 100644 --- a/persistence/sql/migrations/0002_create_accounts.sql +++ b/persistence/sql/migrations/0002_create_accounts.sql @@ -2,7 +2,8 @@ CREATE TABLE IF NOT EXISTS portfolios ( -- Portfolios represent a collection of securities and other positions. - -- held by a user. It can be seen as view over multiple accounts. + -- held by a user. It can be seen as read-only view over multiple + -- accounts. id TEXT PRIMARY KEY, -- ID is the primary identifier for a portfolio. display_name TEXT NOT NULL -- DisplayName is the human-readable name of the portfolio. ); diff --git a/portfolio/events/type_string.go b/portfolio/events/type_string.go new file mode 100644 index 00000000..a11f1ea4 --- /dev/null +++ b/portfolio/events/type_string.go @@ -0,0 +1,31 @@ +// Code generated by "stringer -linecomment -type=PortfolioEventType -output=type_string.go"; DO NOT EDIT. + +package events + +import "strconv" + +func _() { + // An "invalid array index" compiler error signifies that the constant values have changed. + // Re-run the stringer command to generate them again. + var x [1]struct{} + _ = x[PortfolioEventTypeBuy-1] + _ = x[PortfolioEventTypeSell-2] + _ = x[PortfolioEventTypeDividend-3] + _ = x[PortfolioEventTypeDeliveryInbound-4] + _ = x[PortfolioEventTypeDeliveryOutbound-5] + _ = x[PortfolioEventTypeDepositCash-6] + _ = x[PortfolioEventTypeWithdrawCash-7] + _ = x[PortfolioEventTypeUnknown-8] +} + +const _PortfolioEventType_name = "BUYSELLDIVIDENDDELIVERY_INBOUNDDELIVERY_OUTBOUNDDEPOSIT_CASHWITHDRAW_CASHUNKNOWN" + +var _PortfolioEventType_index = [...]uint8{0, 3, 7, 15, 31, 48, 60, 73, 80} + +func (i PortfolioEventType) String() string { + i -= 1 + if i < 0 || i >= PortfolioEventType(len(_PortfolioEventType_index)-1) { + return "PortfolioEventType(" + strconv.FormatInt(int64(i+1), 10) + ")" + } + return _PortfolioEventType_name[_PortfolioEventType_index[i]:_PortfolioEventType_index[i+1]] +} From e68319ed5fb3d9444f275499af62bbf8e05b609c Mon Sep 17 00:00:00 2001 From: Christian Banse Date: Tue, 14 Jan 2025 00:10:00 +0100 Subject: [PATCH 35/35] Fixed unit tests --- cli/commands/portfolio.go | 22 ++++++++++++++-------- 1 file changed, 14 insertions(+), 8 deletions(-) diff --git a/cli/commands/portfolio.go b/cli/commands/portfolio.go index b4ef30e5..8005131f 100644 --- a/cli/commands/portfolio.go +++ b/cli/commands/portfolio.go @@ -23,6 +23,7 @@ import ( "time" mcli "github.com/oxisto/money-gopher/cli" + "github.com/oxisto/money-gopher/currency" "github.com/oxisto/money-gopher/models" "github.com/fatih/color" @@ -83,9 +84,14 @@ func ListPortfolio(ctx context.Context, cmd *cli.Command) (err error) { var query struct { Portfolios []struct { - ID string `json:"id"` - DisplayName string `json:"displayName"` - Snapshot models.PortfolioSnapshot `graphql:"snapshot(when: $when)" json:"snapshot"` + ID string `json:"id"` + DisplayName string `json:"displayName"` + //Snapshot models.PortfolioSnapshot `graphql:"snapshot(when: $when)" json:"snapshot"` + Snapshot struct { + TotalMarketValue currency.Currency `json:"totalMarketValue"` + TotalProfitOrLoss currency.Currency `json:"totalProfitOrLoss"` + TotalGains float64 `json:"totalGains"` + } `graphql:"snapshot(when: $when)" json:"snapshot"` } `json:"portfolios"` } @@ -102,11 +108,11 @@ func ListPortfolio(ctx context.Context, cmd *cli.Command) (err error) { snapshot := portfolio.Snapshot in += fmt.Sprintf(` -| %-*s | -| %s | %s | -| %-*s | %*s | -| %-*s | %*s | -`, + | %-*s | + | %s | %s | + | %-*s | %*s | + | %-*s | %*s | + `, 15+15+3, color.New(color.FgWhite, color.Bold).Sprint(portfolio.DisplayName), strings.Repeat("-", 15), strings.Repeat("-", 15),