From d2c2017d7fbc833d56392d1b237644e8257496b3 Mon Sep 17 00:00:00 2001 From: Geoffrey Ragot Date: Mon, 16 Jun 2025 14:01:33 +0200 Subject: [PATCH 1/2] refactor: introduce entity schemas --- internal/controller/ledger/mocks_test.go | 94 +++++- internal/storage/common/resource.go | 290 +++++++++++++++--- internal/storage/ledger/resource_accounts.go | 47 +-- .../ledger/resource_aggregated_balances.go | 39 +-- internal/storage/ledger/resource_logs.go | 15 +- .../storage/ledger/resource_transactions.go | 70 +---- internal/storage/ledger/resource_volumes.go | 52 +--- internal/storage/ledger/store.go | 11 - internal/storage/system/resource_ledgers.go | 40 +-- 9 files changed, 384 insertions(+), 274 deletions(-) diff --git a/internal/controller/ledger/mocks_test.go b/internal/controller/ledger/mocks_test.go index d1a0a6c764..348a878fce 100644 --- a/internal/controller/ledger/mocks_test.go +++ b/internal/controller/ledger/mocks_test.go @@ -110,20 +110,6 @@ func (mr *MockRepositoryHandlerMockRecorder[Opts]) Expand(query, property any) * return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Expand", reflect.TypeOf((*MockRepositoryHandler[Opts])(nil).Expand), query, property) } -// Filters mocks base method. -func (m *MockRepositoryHandler[Opts]) Filters() []common.Filter { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "Filters") - ret0, _ := ret[0].([]common.Filter) - return ret0 -} - -// Filters indicates an expected call of Filters. -func (mr *MockRepositoryHandlerMockRecorder[Opts]) Filters() *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Filters", reflect.TypeOf((*MockRepositoryHandler[Opts])(nil).Filters)) -} - // Project mocks base method. func (m *MockRepositoryHandler[Opts]) Project(query common.ResourceQuery[Opts], selectQuery *bun.SelectQuery) (*bun.SelectQuery, error) { m.ctrl.T.Helper() @@ -155,6 +141,20 @@ func (mr *MockRepositoryHandlerMockRecorder[Opts]) ResolveFilter(query, operator return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ResolveFilter", reflect.TypeOf((*MockRepositoryHandler[Opts])(nil).ResolveFilter), query, operator, property, value) } +// Schema mocks base method. +func (m *MockRepositoryHandler[Opts]) Schema() common.EntitySchema { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "Schema") + ret0, _ := ret[0].(common.EntitySchema) + return ret0 +} + +// Schema indicates an expected call of Schema. +func (mr *MockRepositoryHandlerMockRecorder[Opts]) Schema() *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Schema", reflect.TypeOf((*MockRepositoryHandler[Opts])(nil).Schema)) +} + // MockResource is a mock of Resource interface. type MockResource[ResourceType any, OptionsType any] struct { ctrl *gomock.Controller @@ -277,3 +277,69 @@ func (mr *MockPaginatedResourceMockRecorder[ResourceType, OptionsType, Paginatio mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Paginate", reflect.TypeOf((*MockPaginatedResource[ResourceType, OptionsType, PaginationQueryType])(nil).Paginate), ctx, paginationOptions) } + +// MockFieldType is a mock of FieldType interface. +type MockFieldType struct { + ctrl *gomock.Controller + recorder *MockFieldTypeMockRecorder + isgomock struct{} +} + +// MockFieldTypeMockRecorder is the mock recorder for MockFieldType. +type MockFieldTypeMockRecorder struct { + mock *MockFieldType +} + +// NewMockFieldType creates a new mock instance. +func NewMockFieldType(ctrl *gomock.Controller) *MockFieldType { + mock := &MockFieldType{ctrl: ctrl} + mock.recorder = &MockFieldTypeMockRecorder{mock} + return mock +} + +// EXPECT returns an object that allows the caller to indicate expected use. +func (m *MockFieldType) EXPECT() *MockFieldTypeMockRecorder { + return m.recorder +} + +// IsIndexable mocks base method. +func (m *MockFieldType) IsIndexable() bool { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "IsIndexable") + ret0, _ := ret[0].(bool) + return ret0 +} + +// IsIndexable indicates an expected call of IsIndexable. +func (mr *MockFieldTypeMockRecorder) IsIndexable() *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "IsIndexable", reflect.TypeOf((*MockFieldType)(nil).IsIndexable)) +} + +// Operators mocks base method. +func (m *MockFieldType) Operators() []string { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "Operators") + ret0, _ := ret[0].([]string) + return ret0 +} + +// Operators indicates an expected call of Operators. +func (mr *MockFieldTypeMockRecorder) Operators() *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Operators", reflect.TypeOf((*MockFieldType)(nil).Operators)) +} + +// ValidateValue mocks base method. +func (m *MockFieldType) ValidateValue(value any) error { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "ValidateValue", value) + ret0, _ := ret[0].(error) + return ret0 +} + +// ValidateValue indicates an expected call of ValidateValue. +func (mr *MockFieldTypeMockRecorder) ValidateValue(value any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ValidateValue", reflect.TypeOf((*MockFieldType)(nil).ValidateValue), value) +} diff --git a/internal/storage/common/resource.go b/internal/storage/common/resource.go index 45ccb3b52b..75199d4b67 100644 --- a/internal/storage/common/resource.go +++ b/internal/storage/common/resource.go @@ -11,23 +11,23 @@ import ( "github.com/formancehq/go-libs/v3/time" "github.com/uptrace/bun" "math/big" - "regexp" "slices" + "strings" ) func ConvertOperatorToSQL(operator string) string { switch operator { - case "$match": + case OperatorMatch: return "=" - case "$lt": + case OperatorLT: return "<" - case "$gt": + case OperatorGT: return ">" - case "$lte": + case OperatorLTE: return "<=" - case "$gte": + case OperatorGTE: return ">=" - case "$like": + case OperatorLike: return "like" } panic("unreachable") @@ -56,11 +56,8 @@ func AcceptOperators(operators ...string) PropertyValidator { }) } -type Filter struct { - Name string - Aliases []string - Matchers []func(key string) bool - Validators []PropertyValidator +type EntitySchema struct { + Fields map[string]Field } type RepositoryHandlerBuildContext[Opts any] struct { @@ -83,7 +80,7 @@ func (ctx RepositoryHandlerBuildContext[Opts]) UseFilter(v string, matchers ...f } type RepositoryHandler[Opts any] interface { - Filters() []Filter + Schema() EntitySchema BuildDataset(query RepositoryHandlerBuildContext[Opts]) (*bun.SelectQuery, error) ResolveFilter(query ResourceQuery[Opts], operator, property string, value any) (string, []any, error) Project(query ResourceQuery[Opts], selectQuery *bun.SelectQuery) (*bun.SelectQuery, error) @@ -100,45 +97,44 @@ func (r *ResourceRepository[ResourceType, OptionsType]) validateFilters(builder } ret := make(map[string]any) - properties := r.resourceHandler.Filters() + properties := r.resourceHandler.Schema().Fields if err := builder.Walk(func(operator string, key string, value any) (err error) { - found := false - for _, property := range properties { - if len(property.Matchers) > 0 { - for _, matcher := range property.Matchers { - if found = matcher(key); found { - break - } + for name, property := range properties { + key := key + if property.Type.IsIndexable() { + key = strings.Split(key, "[")[0] + } + match := func() bool { + if key == name { + return true } - } else { - options := append([]string{property.Name}, property.Aliases...) - for _, option := range options { - if found, err = regexp.MatchString("^"+option+"$", key); err != nil { - return fmt.Errorf("failed to match regex for key '%s': %w", key, err) - } else if found { - break + for _, alias := range property.Aliases { + if key == alias { + return true } } - } - if !found { + + return false + }() + if !match { continue } - for _, validator := range property.Validators { - if err := validator.Validate(operator, key, value); err != nil { - return err - } + if !slices.Contains(property.Type.Operators(), operator) { + return NewErrInvalidQuery("operator '%s' is not allowed for property '%s'", operator, name) } - ret[property.Name] = value - break - } - if !found { - return NewErrInvalidQuery("unknown key '%s' when building query", key) + if err := property.Type.ValidateValue(value); err != nil { + return NewErrInvalidQuery("invalid value '%v' for property '%s': %s", value, name, err) + } + + ret[name] = value + + return nil } - return nil + return NewErrInvalidQuery("unknown key '%s' when building query", key) }); err != nil { return nil, err } @@ -422,3 +418,217 @@ type PaginatedResource[ResourceType, OptionsType any, PaginationQueryType Pagina Resource[ResourceType, OptionsType] Paginate(ctx context.Context, paginationOptions PaginationQueryType) (*bunpaginate.Cursor[ResourceType], error) } + +const ( + OperatorMatch = "$match" + OperatorExists = "$exists" + OperatorLike = "$like" + OperatorLT = "$lt" + OperatorGT = "$gt" + OperatorLTE = "$lte" + OperatorGTE = "$gte" +) + +type FieldType interface { + Operators() []string + ValidateValue(value any) error + IsIndexable() bool +} + +type Field struct { + Aliases []string + Type FieldType +} + +func (f Field) WithAliases(aliases ...string) Field { + f.Aliases = append(f.Aliases, aliases...) + return f +} + +func NewField(t FieldType) Field { + return Field{ + Aliases: []string{}, + Type: t, + } +} + +// NewStringField creates a new field with TypeString as its type. +func NewStringField() Field { + return NewField(NewTypeString()) +} + +// NewDateField creates a new field with TypeDate as its type. +func NewDateField() Field { + return NewField(NewTypeDate()) +} + +// NewMapField creates a new field with TypeMap as its type, using the provided underlying type. +func NewMapField(underlyingType FieldType) Field { + return NewField(NewTypeMap(underlyingType)) +} + +// NewNumericField creates a new field with TypeNumeric as its type. +func NewNumericField() Field { + return NewField(NewTypeNumeric()) +} + +// NewBooleanField creates a new field with TypeBoolean as its type. +func NewBooleanField() Field { + return NewField(NewTypeBoolean()) +} + +// NewStringMapField creates a new field with TypeMap as its type, using TypeString as the underlying type. +func NewStringMapField() Field { + return NewMapField(NewTypeString()) +} + +// NewNumericMapField creates a new field with TypeMap as its type, using TypeNumeric as the underlying type. +func NewNumericMapField() Field { + return NewMapField(NewTypeNumeric()) +} + +type TypeString struct{} + +func (t TypeString) IsIndexable() bool { + return false +} + +func (t TypeString) Operators() []string { + return []string{ + OperatorMatch, + OperatorLike, + } +} + +func (t TypeString) ValidateValue(value any) error { + _, ok := value.(string) + if !ok { + return fmt.Errorf("expected string value, got %T", value) + } + return nil +} + +var _ FieldType = (*TypeString)(nil) + +func NewTypeString() TypeString { + return TypeString{} +} + +type TypeDate struct{} + +func (t TypeDate) IsIndexable() bool { + return false +} + +func (t TypeDate) Operators() []string { + return []string{ + OperatorMatch, + OperatorLT, + OperatorGT, + OperatorLTE, + OperatorGTE, + } +} + +func (t TypeDate) ValidateValue(value any) error { + switch value := value.(type) { + case string: + _, err := time.ParseTime(value) + if err != nil { + return fmt.Errorf("invalid date value: %w", err) + } + case time.Time, *time.Time: + default: + return fmt.Errorf("expected string, time.Time, or *time.Time value, got %T", value) + } + return nil +} + +func NewTypeDate() TypeDate { + return TypeDate{} +} + +var _ FieldType = (*TypeDate)(nil) + +type TypeMap struct { + underlyingType FieldType +} + +func (t TypeMap) IsIndexable() bool { + return true +} + +func (t TypeMap) Operators() []string { + return append(t.underlyingType.Operators(), OperatorMatch, OperatorExists) +} + +func (t TypeMap) ValidateValue(value any) error { + return t.underlyingType.ValidateValue(value) +} + +func NewTypeMap(underlyingType FieldType) TypeMap { + return TypeMap{ + underlyingType: underlyingType, + } +} + +var _ FieldType = (*TypeMap)(nil) + +type TypeNumeric struct{} + +func (t TypeNumeric) IsIndexable() bool { + return false +} + +func (t TypeNumeric) Operators() []string { + return []string{ + OperatorMatch, + OperatorLT, + OperatorGT, + OperatorLTE, + OperatorGTE, + } +} + +func (t TypeNumeric) ValidateValue(value any) error { + switch value.(type) { + case int, int8, int16, int32, int64, uint, uint8, uint16, uint32, uint64, float64, float32, + *int, *int8, *int16, *int32, *int64, *uint, *uint8, *uint16, *uint32, *uint64, *float64, *float32: + return nil + default: + return fmt.Errorf("expected numeric value, got %T", value) + } +} + +func NewTypeNumeric() TypeNumeric { + return TypeNumeric{} +} + +var _ FieldType = (*TypeNumeric)(nil) + +type TypeBoolean struct{} + +func (t TypeBoolean) IsIndexable() bool { + return false +} + +func (t TypeBoolean) Operators() []string { + return []string{ + OperatorMatch, + } +} + +func (t TypeBoolean) ValidateValue(value any) error { + _, ok := value.(bool) + if !ok { + return fmt.Errorf("expected boolean value, got %T", value) + } + + return nil +} + +func NewTypeBoolean() TypeBoolean { + return TypeBoolean{} +} + +var _ FieldType = (*TypeBoolean)(nil) diff --git a/internal/storage/ledger/resource_accounts.go b/internal/storage/ledger/resource_accounts.go index afe7842406..236b2750d3 100644 --- a/internal/storage/ledger/resource_accounts.go +++ b/internal/storage/ledger/resource_accounts.go @@ -13,48 +13,19 @@ type accountsResourceHandler struct { store *Store } -func (h accountsResourceHandler) Filters() []common.Filter { - return []common.Filter{ - { - Name: "address", - Validators: []common.PropertyValidator{ - common.PropertyValidatorFunc(func(operator string, key string, value any) error { - return validateAddressFilter(operator, value) - }), - }, - }, - { - Name: "first_usage", - Validators: []common.PropertyValidator{ - common.AcceptOperators("$lt", "$gt", "$lte", "$gte", "$match"), - }, - }, - { - Name: `balance(\[.*])?`, - Validators: []common.PropertyValidator{ - common.AcceptOperators("$lt", "$gt", "$lte", "$gte", "$match"), - }, - }, - { - Name: "metadata", - Validators: []common.PropertyValidator{ - common.AcceptOperators("$exists"), - }, - }, - { - Name: `metadata\[.*]`, - Validators: []common.PropertyValidator{ - common.AcceptOperators("$match"), - }, +func (h accountsResourceHandler) Schema() common.EntitySchema { + return common.EntitySchema{ + Fields: map[string]common.Field{ + "address": common.NewStringField(), + "first_usage": common.NewDateField(), + "balance": common.NewNumericMapField(), + "metadata": common.NewStringMapField(), }, } } func (h accountsResourceHandler) BuildDataset(opts common.RepositoryHandlerBuildContext[any]) (*bun.SelectQuery, error) { - ret := h.store.db.NewSelect() - - // Build the query - ret = ret. + ret := h.store.db.NewSelect(). ModelTableExpr(h.store.GetPrefixedRelationName("accounts")). Column("address", "address_array", "first_usage", "insertion_date", "updated_at"). Where("ledger = ?", h.store.ledger.Name) @@ -134,7 +105,7 @@ func (h accountsResourceHandler) ResolveFilter(opts common.ResourceQuery[any], o } } -func (h accountsResourceHandler) Project(query common.ResourceQuery[any], selectQuery *bun.SelectQuery) (*bun.SelectQuery, error) { +func (h accountsResourceHandler) Project(_ common.ResourceQuery[any], selectQuery *bun.SelectQuery) (*bun.SelectQuery, error) { return selectQuery.ColumnExpr("*"), nil } diff --git a/internal/storage/ledger/resource_aggregated_balances.go b/internal/storage/ledger/resource_aggregated_balances.go index d97ee27303..db9c1f1d83 100644 --- a/internal/storage/ledger/resource_aggregated_balances.go +++ b/internal/storage/ledger/resource_aggregated_balances.go @@ -2,7 +2,6 @@ package ledger import ( "errors" - "fmt" ledgercontroller "github.com/formancehq/ledger/internal/controller/ledger" "github.com/formancehq/ledger/internal/storage/common" "github.com/formancehq/ledger/pkg/features" @@ -13,37 +12,11 @@ type aggregatedBalancesResourceRepositoryHandler struct { store *Store } -func (h aggregatedBalancesResourceRepositoryHandler) Filters() []common.Filter { - return []common.Filter{ - { - Name: "address", - Validators: []common.PropertyValidator{ - common.PropertyValidatorFunc(func(operator string, key string, value any) error { - return validateAddressFilter(operator, value) - }), - }, - }, - { - Name: "metadata", - Matchers: []func(string) bool{ - func(key string) bool { - return key == "metadata" || common.MetadataRegex.Match([]byte(key)) - }, - }, - Validators: []common.PropertyValidator{ - common.PropertyValidatorFunc(func(operator string, key string, value any) error { - if key == "metadata" { - if operator != "$exists" { - return fmt.Errorf("unsupported operator %s for metadata", operator) - } - return nil - } - if operator != "$match" { - return fmt.Errorf("unsupported operator %s for metadata", operator) - } - return nil - }), - }, +func (h aggregatedBalancesResourceRepositoryHandler) Schema() common.EntitySchema { + return common.EntitySchema{ + Fields: map[string]common.Field{ + "address": common.NewStringField(), + "metadata": common.NewStringMapField(), }, } } @@ -136,7 +109,7 @@ func (h aggregatedBalancesResourceRepositoryHandler) BuildDataset(query common.R } } -func (h aggregatedBalancesResourceRepositoryHandler) ResolveFilter(_ common.ResourceQuery[ledgercontroller.GetAggregatedVolumesOptions], operator, property string, value any) (string, []any, error) { +func (h aggregatedBalancesResourceRepositoryHandler) ResolveFilter(_ common.ResourceQuery[ledgercontroller.GetAggregatedVolumesOptions], _, property string, value any) (string, []any, error) { switch { case property == "address": return filterAccountAddress(value.(string), "accounts_address"), nil, nil diff --git a/internal/storage/ledger/resource_logs.go b/internal/storage/ledger/resource_logs.go index eaf0db30e4..9107be1bda 100644 --- a/internal/storage/ledger/resource_logs.go +++ b/internal/storage/ledger/resource_logs.go @@ -11,14 +11,11 @@ type logsResourceHandler struct { store *Store } -func (h logsResourceHandler) Filters() []common.Filter { - return []common.Filter{ - { - // todo: add validators - Name: "date", - }, - { - Name: "id", +func (h logsResourceHandler) Schema() common.EntitySchema { + return common.EntitySchema{ + Fields: map[string]common.Field{ + "date": common.NewDateField(), + "id": common.NewNumericField(), }, } } @@ -43,7 +40,7 @@ func (h logsResourceHandler) Expand(_ common.ResourceQuery[any], _ string) (*bun return nil, nil, errors.New("no expand supported") } -func (h logsResourceHandler) Project(query common.ResourceQuery[any], selectQuery *bun.SelectQuery) (*bun.SelectQuery, error) { +func (h logsResourceHandler) Project(_ common.ResourceQuery[any], selectQuery *bun.SelectQuery) (*bun.SelectQuery, error) { return selectQuery.ColumnExpr("*"), nil } diff --git a/internal/storage/ledger/resource_transactions.go b/internal/storage/ledger/resource_transactions.go index 03d0303031..8a627a053d 100644 --- a/internal/storage/ledger/resource_transactions.go +++ b/internal/storage/ledger/resource_transactions.go @@ -12,59 +12,17 @@ type transactionsResourceHandler struct { store *Store } -func (h transactionsResourceHandler) Filters() []common.Filter { - return []common.Filter{ - { - Name: "reverted", - Validators: []common.PropertyValidator{ - common.AcceptOperators("$match"), - }, - }, - { - Name: "account", - Validators: []common.PropertyValidator{ - common.PropertyValidatorFunc(func(operator string, key string, value any) error { - return validateAddressFilter(operator, value) - }), - }, - }, - { - Name: "source", - Validators: []common.PropertyValidator{ - common.PropertyValidatorFunc(func(operator string, key string, value any) error { - return validateAddressFilter(operator, value) - }), - }, - }, - { - Name: "destination", - Validators: []common.PropertyValidator{ - common.PropertyValidatorFunc(func(operator string, key string, value any) error { - return validateAddressFilter(operator, value) - }), - }, - }, - { - // todo: add validators - Name: "timestamp", - }, - { - Name: "metadata", - Validators: []common.PropertyValidator{ - common.AcceptOperators("$exists"), - }, - }, - { - Name: `metadata\[.*]`, - Validators: []common.PropertyValidator{ - common.AcceptOperators("$match"), - }, - }, - { - Name: "id", - }, - { - Name: "reference", +func (h transactionsResourceHandler) Schema() common.EntitySchema { + return common.EntitySchema{ + Fields: map[string]common.Field{ + "reverted": common.NewBooleanField(), + "account": common.NewStringField(), + "source": common.NewStringField(), + "destination": common.NewStringField(), + "timestamp": common.NewDateField(), + "metadata": common.NewStringMapField(), + "id": common.NewNumericField(), + "reference": common.NewStringField(), }, } } @@ -123,7 +81,7 @@ func (h transactionsResourceHandler) BuildDataset(opts common.RepositoryHandlerB return ret, nil } -func (h transactionsResourceHandler) ResolveFilter(opts common.ResourceQuery[any], operator, property string, value any) (string, []any, error) { +func (h transactionsResourceHandler) ResolveFilter(_ common.ResourceQuery[any], operator, property string, value any) (string, []any, error) { switch { case property == "id": return fmt.Sprintf("id %s ?", common.ConvertOperatorToSQL(operator)), []any{value}, nil @@ -155,11 +113,11 @@ func (h transactionsResourceHandler) ResolveFilter(opts common.ResourceQuery[any } } -func (h transactionsResourceHandler) Project(query common.ResourceQuery[any], selectQuery *bun.SelectQuery) (*bun.SelectQuery, error) { +func (h transactionsResourceHandler) Project(_ common.ResourceQuery[any], selectQuery *bun.SelectQuery) (*bun.SelectQuery, error) { return selectQuery.ColumnExpr("*"), nil } -func (h transactionsResourceHandler) Expand(opts common.ResourceQuery[any], property string) (*bun.SelectQuery, *common.JoinCondition, error) { +func (h transactionsResourceHandler) Expand(_ common.ResourceQuery[any], property string) (*bun.SelectQuery, *common.JoinCondition, error) { if property != "effectiveVolumes" { return nil, nil, nil } diff --git a/internal/storage/ledger/resource_volumes.go b/internal/storage/ledger/resource_volumes.go index c019700472..afff4b004b 100644 --- a/internal/storage/ledger/resource_volumes.go +++ b/internal/storage/ledger/resource_volumes.go @@ -14,50 +14,14 @@ type volumesResourceHandler struct { store *Store } -func (h volumesResourceHandler) Filters() []common.Filter { - return []common.Filter{ - { - Name: "address", - Aliases: []string{"account"}, - Validators: []common.PropertyValidator{ - common.PropertyValidatorFunc(func(operator string, key string, value any) error { - return validateAddressFilter(operator, value) - }), - }, - }, - { - Name: `balance(\[.*])?`, - Validators: []common.PropertyValidator{ - common.AcceptOperators("$lt", "$gt", "$lte", "$gte", "$match"), - }, - }, - { - Name: "first_usage", - Validators: []common.PropertyValidator{ - common.AcceptOperators("$lt", "$gt", "$lte", "$gte", "$match"), - }, - }, - { - Name: "metadata", - Matchers: []func(string) bool{ - func(key string) bool { - return key == "metadata" || common.MetadataRegex.Match([]byte(key)) - }, - }, - Validators: []common.PropertyValidator{ - common.PropertyValidatorFunc(func(operator string, key string, value any) error { - if key == "metadata" { - if operator != "$exists" { - return fmt.Errorf("unsupported operator %s for metadata", operator) - } - return nil - } - if operator != "$match" { - return fmt.Errorf("unsupported operator %s for metadata", operator) - } - return nil - }), - }, +func (h volumesResourceHandler) Schema() common.EntitySchema { + return common.EntitySchema{ + Fields: map[string]common.Field{ + "address": common.NewStringField(). + WithAliases("account"), + "balance": common.NewNumericMapField(), + "first_usage": common.NewDateField(), + "metadata": common.NewStringMapField(), }, } } diff --git a/internal/storage/ledger/store.go b/internal/storage/ledger/store.go index e8882a68a2..2d6f917cc6 100644 --- a/internal/storage/ledger/store.go +++ b/internal/storage/ledger/store.go @@ -141,17 +141,6 @@ func (store *Store) GetPrefixedRelationName(v string) string { return fmt.Sprintf(`"%s".%s`, store.ledger.Bucket, v) } -func validateAddressFilter(operator string, value any) error { - if operator != "$match" { - return fmt.Errorf("'address' column can only be used with $match, operator used is: %s", operator) - } - if _, ok := value.(string); !ok { - return fmt.Errorf("invalid 'address' filter") - } - - return nil -} - func (store *Store) LockLedger(ctx context.Context) (*Store, bun.IDB, func() error, error) { storeCp := *store switch db := store.db.(type) { diff --git a/internal/storage/system/resource_ledgers.go b/internal/storage/system/resource_ledgers.go index cecd700b8c..a7f6d25a42 100644 --- a/internal/storage/system/resource_ledgers.go +++ b/internal/storage/system/resource_ledgers.go @@ -16,42 +16,24 @@ type ledgersResourceHandler struct { store *DefaultStore } -func (h ledgersResourceHandler) Filters() []common.Filter { - return []common.Filter{ - { - Name: "bucket", - Validators: []common.PropertyValidator{ - common.AcceptOperators("$match"), - }, - }, - { - Name: `features\[.*]`, - Validators: []common.PropertyValidator{ - common.AcceptOperators("$match"), - }, - }, - { - Name: `metadata\[.*]`, - Validators: []common.PropertyValidator{ - common.AcceptOperators("$match"), - }, - }, - { - Name: `name`, - Validators: []common.PropertyValidator{ - common.AcceptOperators("$match", "$like"), - }, +func (h ledgersResourceHandler) Schema() common.EntitySchema { + return common.EntitySchema{ + Fields: map[string]common.Field{ + "bucket": common.NewStringField(), + "features": common.NewStringMapField(), + "metadata": common.NewStringMapField(), + "name": common.NewStringField(), }, } } -func (h ledgersResourceHandler) BuildDataset(opts common.RepositoryHandlerBuildContext[any]) (*bun.SelectQuery, error) { +func (h ledgersResourceHandler) BuildDataset(_ common.RepositoryHandlerBuildContext[any]) (*bun.SelectQuery, error) { return h.store.db.NewSelect(). Model(&ledger.Ledger{}). Column("*"), nil } -func (h ledgersResourceHandler) ResolveFilter(opts common.ResourceQuery[any], operator, property string, value any) (string, []any, error) { +func (h ledgersResourceHandler) ResolveFilter(_ common.ResourceQuery[any], operator, property string, value any) (string, []any, error) { switch { case property == "bucket": return "bucket = ?", []any{value}, nil @@ -77,11 +59,11 @@ func (h ledgersResourceHandler) ResolveFilter(opts common.ResourceQuery[any], op } } -func (h ledgersResourceHandler) Project(query common.ResourceQuery[any], selectQuery *bun.SelectQuery) (*bun.SelectQuery, error) { +func (h ledgersResourceHandler) Project(_ common.ResourceQuery[any], selectQuery *bun.SelectQuery) (*bun.SelectQuery, error) { return selectQuery.ColumnExpr("*"), nil } -func (h ledgersResourceHandler) Expand(opts common.ResourceQuery[any], property string) (*bun.SelectQuery, *common.JoinCondition, error) { +func (h ledgersResourceHandler) Expand(_ common.ResourceQuery[any], _ string) (*bun.SelectQuery, *common.JoinCondition, error) { return nil, nil, errors.New("no expansion available") } From 088987048ce2c8974d62aa0484aa9fb5f758f3eb Mon Sep 17 00:00:00 2001 From: Geoffrey Ragot Date: Tue, 17 Jun 2025 11:56:24 +0200 Subject: [PATCH 2/2] chore: extract function --- internal/storage/common/resource.go | 27 ++++++++++++++------------- 1 file changed, 14 insertions(+), 13 deletions(-) diff --git a/internal/storage/common/resource.go b/internal/storage/common/resource.go index 75199d4b67..404a83c669 100644 --- a/internal/storage/common/resource.go +++ b/internal/storage/common/resource.go @@ -105,19 +105,7 @@ func (r *ResourceRepository[ResourceType, OptionsType]) validateFilters(builder if property.Type.IsIndexable() { key = strings.Split(key, "[")[0] } - match := func() bool { - if key == name { - return true - } - for _, alias := range property.Aliases { - if key == alias { - return true - } - } - - return false - }() - if !match { + if !property.matchKey(name, key) { continue } @@ -445,6 +433,19 @@ func (f Field) WithAliases(aliases ...string) Field { return f } +func (f Field) matchKey(name, key string) bool { + if key == name { + return true + } + for _, alias := range f.Aliases { + if key == alias { + return true + } + } + + return false +} + func NewField(t FieldType) Field { return Field{ Aliases: []string{},