From 10f94a4b336f958b084ddad0ba0a2236d55728cb Mon Sep 17 00:00:00 2001 From: Damian Orzepowski Date: Fri, 23 Aug 2024 16:57:29 +0200 Subject: [PATCH] feat(SPV-793): add function that allows parsing query params into new search params struct. --- internal/query/parse.go | 37 ++++++++ internal/query/parse_test.go | 122 ++++++++++++++++++++++++++ mappings/{filter.go => filter_old.go} | 0 mappings/search.go | 45 ++++++++++ models/filter/access_key_filter.go | 2 +- models/filter/contact_filter.go | 2 +- models/filter/destination_filter.go | 2 +- models/filter/paymail_filter.go | 2 +- models/filter/search_params.go | 14 +++ models/filter/transaction_filter.go | 2 +- models/filter/utxo_filter.go | 2 +- models/filter/xpub_filter.go | 2 +- 12 files changed, 225 insertions(+), 7 deletions(-) create mode 100644 internal/query/parse.go create mode 100644 internal/query/parse_test.go rename mappings/{filter.go => filter_old.go} (100%) create mode 100644 mappings/search.go create mode 100644 models/filter/search_params.go diff --git a/internal/query/parse.go b/internal/query/parse.go new file mode 100644 index 00000000..fb6bdb3b --- /dev/null +++ b/internal/query/parse.go @@ -0,0 +1,37 @@ +package query + +import ( + "time" + + "github.com/bitcoin-sv/spv-wallet/models/filter" + "github.com/gin-gonic/gin" + "github.com/mitchellh/mapstructure" +) + +func ParseSearchParams[T any](c *gin.Context, _ T) (*filter.SearchParams[T], error) { + var params filter.SearchParams[T] + + dicts, err := ShouldGetQueryNestedMap(c) + if err != nil { + return nil, err + } + + config := mapstructure.DecoderConfig{ + DecodeHook: mapstructure.StringToTimeHookFunc(time.RFC3339), + WeaklyTypedInput: true, + Result: ¶ms, + TagName: "json", // Small hax to reuse json tags which we have already defined + } + + decoder, err := mapstructure.NewDecoder(&config) + if err != nil { + return nil, err + } + + err = decoder.Decode(dicts) + if err != nil { + return nil, err + } + + return ¶ms, nil +} diff --git a/internal/query/parse_test.go b/internal/query/parse_test.go new file mode 100644 index 00000000..ac08200d --- /dev/null +++ b/internal/query/parse_test.go @@ -0,0 +1,122 @@ +package query + +import ( + "net/http" + "net/url" + "testing" + "time" + + "github.com/bitcoin-sv/spv-wallet/models/filter" + "github.com/gin-gonic/gin" + "github.com/stretchr/testify/require" +) + +func TestParseSearchParamsSuccessfully(t *testing.T) { + tests := map[string]struct { + url string + expectedResult filter.SearchParams[ExampleConditionsForTests] + }{ + "empty query": { + url: "", + expectedResult: filter.SearchParams[ExampleConditionsForTests]{}, + }, + "query page": { + url: "?page=2&size=200&order=asc&sortBy=id", + expectedResult: filter.SearchParams[ExampleConditionsForTests]{ + Page: filter.Page{ + Number: 2, + Size: 200, + Order: "asc", + SortBy: "id", + }, + }, + }, + "query conditions model filter": { + url: "?includeDeleted=true&createdRange[from]=2021-01-01T00:00:00Z&createdRange[to]=2021-01-02T00:00:00Z&updatedRange[from]=2021-02-01T00:00:00Z&updatedRange[to]=2021-02-02T00:00:00Z", + expectedResult: filter.SearchParams[ExampleConditionsForTests]{ + Conditions: ExampleConditionsForTests{ + ModelFilter: filter.ModelFilter{ + IncludeDeleted: ptr(true), + CreatedRange: &filter.TimeRange{ + From: ptr(time.Date(2021, 1, 1, 0, 0, 0, 0, time.UTC)), + To: ptr(time.Date(2021, 1, 2, 0, 0, 0, 0, time.UTC)), + }, + UpdatedRange: &filter.TimeRange{ + From: ptr(time.Date(2021, 2, 1, 0, 0, 0, 0, time.UTC)), + To: ptr(time.Date(2021, 2, 2, 0, 0, 0, 0, time.UTC)), + }, + }, + }, + }, + }, + "query conditions without nested structs": { + url: "?xBoolean=true&xString=some%20string&xInt=5", + expectedResult: filter.SearchParams[ExampleConditionsForTests]{ + Conditions: ExampleConditionsForTests{ + XBoolean: ptr(true), + XString: ptr("some string"), + XInt: ptr(5), + }, + }, + }, + "query conditions nested struct": { + url: "?nested[isNested]=true&nested[name]=some%20name&nested[number]=10", + expectedResult: filter.SearchParams[ExampleConditionsForTests]{ + Conditions: ExampleConditionsForTests{ + Nested: &ExampleNestedConditionsForTests{ + IsNested: ptr(true), + Name: ptr("some name"), + Number: ptr(10), + }, + }, + }, + }, + "query metadata": { + url: "?metadata[key]=value1&metadata[key2][nested]=value2&metadata[key3][nested][]=value3&metadata[key3][nested][]=value4", + expectedResult: filter.SearchParams[ExampleConditionsForTests]{ + Metadata: map[string]interface{}{ + "key": "value1", + "key2": map[string]interface{}{ + "nested": "value2", + }, + "key3": map[string]interface{}{ + "nested": []string{"value3", "value4"}, + }, + }, + }, + }, + } + for name, test := range tests { + t.Run(name, func(t *testing.T) { + u, err := url.Parse(test.url) + require.NoError(t, err) + + c := &gin.Context{ + Request: &http.Request{ + URL: u, + }, + } + + params, err := ParseSearchParams(c, ExampleConditionsForTests{}) + require.NoError(t, err) + require.EqualValues(t, test.expectedResult, *params) + }) + } +} + +type ExampleConditionsForTests struct { + filter.ModelFilter `json:",inline,squash"` + XBoolean *bool `json:"xBoolean,omitempty"` + XString *string `json:"xString,omitempty"` + XInt *int `json:"xInt,omitempty"` + Nested *ExampleNestedConditionsForTests `json:"nested,omitempty"` +} +type ExampleNestedConditionsForTests struct { + IsNested *bool `json:"isNested,omitempty"` + Name *string `json:"name,omitempty"` + Number *int `json:"number,omitempty"` +} + +func ptr[T any](value T) *T { + return &value +} diff --git a/mappings/filter.go b/mappings/filter_old.go similarity index 100% rename from mappings/filter.go rename to mappings/filter_old.go diff --git a/mappings/search.go b/mappings/search.go new file mode 100644 index 00000000..728087c4 --- /dev/null +++ b/mappings/search.go @@ -0,0 +1,45 @@ +package mappings + +import ( + "github.com/bitcoin-sv/spv-wallet/engine/datastore" + "github.com/bitcoin-sv/spv-wallet/models/filter" +) + +const ( + defaultPage = 1 + defaultPageSize = 50 + defaultSortBy = "created_at" + defaultOrder = "desc" +) + +// MapToDbQueryParams converts filter.QueryParams from models to matching datastore.QueryParams +func MapToDbQueryParams(model *filter.Page) *datastore.QueryParams { + if model == nil { + return &datastore.QueryParams{ + Page: defaultPage, + PageSize: defaultPageSize, + OrderByField: defaultOrder, + SortDirection: defaultOrder, + } + } + return &datastore.QueryParams{ + Page: getNumberOrDefault(model.Number, defaultPage), + PageSize: getNumberOrDefault(model.Size, defaultPageSize), + OrderByField: getStringOrDefalut(model.SortBy, defaultSortBy), + SortDirection: getStringOrDefalut(model.Order, defaultOrder), + } +} + +func getNumberOrDefault(value int, defaultValue int) int { + if value == 0 { + return defaultValue + } + return value +} + +func getStringOrDefalut(value string, defaultValue string) string { + if value == "" { + return defaultValue + } + return value +} diff --git a/models/filter/access_key_filter.go b/models/filter/access_key_filter.go index 93380093..f2fdc295 100644 --- a/models/filter/access_key_filter.go +++ b/models/filter/access_key_filter.go @@ -2,7 +2,7 @@ package filter // AccessKeyFilter is a struct for handling request parameters for destination search requests type AccessKeyFilter struct { - ModelFilter `json:",inline"` + ModelFilter `json:",inline,squash"` // RevokedRange specifies the time range when a record was revoked. RevokedRange *TimeRange `json:"revokedRange,omitempty"` diff --git a/models/filter/contact_filter.go b/models/filter/contact_filter.go index 003bd2b1..4418b1a7 100644 --- a/models/filter/contact_filter.go +++ b/models/filter/contact_filter.go @@ -2,7 +2,7 @@ package filter // ContactFilter is a struct for handling request parameters for contact search requests type ContactFilter struct { - ModelFilter `json:",inline"` + ModelFilter `json:",inline,squash"` ID *string `json:"id" example:"ffdbe74e-0700-4710-aac5-611a1f877c7f"` FullName *string `json:"fullName" example:"Alice"` Paymail *string `json:"paymail" example:"alice@example.com"` diff --git a/models/filter/destination_filter.go b/models/filter/destination_filter.go index 4af01789..e6f47aa9 100644 --- a/models/filter/destination_filter.go +++ b/models/filter/destination_filter.go @@ -2,7 +2,7 @@ package filter // DestinationFilter is a struct for handling request parameters for destination search requests type DestinationFilter struct { - ModelFilter `json:",inline"` + ModelFilter `json:",inline,squash"` LockingScript *string `json:"lockingScript,omitempty" example:"76a9147b05764a97f3b4b981471492aa703b188e45979b88ac"` Address *string `json:"address,omitempty" example:"1CDUf7CKu8ocTTkhcYUbq75t14Ft168K65"` DraftID *string `json:"draftId,omitempty" example:"b356f7fa00cd3f20cce6c21d704cd13e871d28d714a5ebd0532f5a0e0cde63f7"` diff --git a/models/filter/paymail_filter.go b/models/filter/paymail_filter.go index 094ecbaa..8ea8da3f 100644 --- a/models/filter/paymail_filter.go +++ b/models/filter/paymail_filter.go @@ -2,7 +2,7 @@ package filter // AdminPaymailFilter is a struct for handling request parameters for paymail_addresses search requests type AdminPaymailFilter struct { - ModelFilter `json:",inline"` + ModelFilter `json:",inline,squash"` ID *string `json:"id,omitempty" example:"ffb86c103d17d87c15aaf080aab6be5415c9fa885309a79b04c9910e39f2b542"` XpubID *string `json:"xpubId,omitempty" example:"79f90a6bab0a44402fc64828af820e9465645658aea2d138c5205b88e6dabd00"` diff --git a/models/filter/search_params.go b/models/filter/search_params.go new file mode 100644 index 00000000..c123bd8c --- /dev/null +++ b/models/filter/search_params.go @@ -0,0 +1,14 @@ +package filter + +type Page struct { + Number int `json:"page,omitempty"` + Size int `json:"size,omitempty"` + Order string `json:"order,omitempty"` + SortBy string `json:"sortBy,omitempty"` +} + +type SearchParams[T any] struct { + Page Page `json:"paging,squash"` + Conditions T `json:"conditions,squash"` + Metadata map[string]interface{} `json:"metadata,omitempty"` +} diff --git a/models/filter/transaction_filter.go b/models/filter/transaction_filter.go index 9422d954..8fc2861d 100644 --- a/models/filter/transaction_filter.go +++ b/models/filter/transaction_filter.go @@ -2,7 +2,7 @@ package filter // TransactionFilter is a struct for handling request parameters for transactions search requests type TransactionFilter struct { - ModelFilter `json:",inline"` + ModelFilter `json:",inline,squash"` Hex *string `json:"hex,omitempty"` BlockHash *string `json:"blockHash,omitempty" example:"0000000000000000031928c28075a82d7a00c2c90b489d1d66dc0afa3f8d26f8"` BlockHeight *uint64 `json:"blockHeight,omitempty" example:"839376"` diff --git a/models/filter/utxo_filter.go b/models/filter/utxo_filter.go index f953ee9f..dd2a9588 100644 --- a/models/filter/utxo_filter.go +++ b/models/filter/utxo_filter.go @@ -2,7 +2,7 @@ package filter // UtxoFilter is a struct for handling request parameters for utxo search requests type UtxoFilter struct { - ModelFilter `json:",inline"` + ModelFilter `json:",inline,squash"` TransactionID *string `json:"transactionId,omitempty" example:"5e17858ea0ca4155827754ba82bdcfcce108d5bb5b47fbb3aa54bd14540683c6"` OutputIndex *uint32 `json:"outputIndex,omitempty" example:"0"` diff --git a/models/filter/xpub_filter.go b/models/filter/xpub_filter.go index a2575038..fdec760f 100644 --- a/models/filter/xpub_filter.go +++ b/models/filter/xpub_filter.go @@ -2,7 +2,7 @@ package filter // XpubFilter is a struct for handling request parameters for utxo search requests type XpubFilter struct { - ModelFilter `json:",inline"` + ModelFilter `json:",inline,squash"` ID *string `json:"id,omitempty" example:"00b953624f78004a4c727cd28557475d5233c15f17aef545106639f4d71b712d"` CurrentBalance *uint64 `json:"currentBalance,omitempty" example:"1000"`