diff --git a/pkg/blueprint_client/get_by_id.go b/pkg/blueprint_client/get_by_id.go new file mode 100644 index 0000000..191fa1d --- /dev/null +++ b/pkg/blueprint_client/get_by_id.go @@ -0,0 +1,47 @@ +package bpc + +import ( + "context" + "fmt" + + "connectrpc.com/connect" + kvv1 "github.com/steady-bytes/draft/api/core/registry/key_value/v1" + kvv1Connect "github.com/steady-bytes/draft/api/core/registry/key_value/v1/v1connect" + "google.golang.org/protobuf/reflect/protoreflect" + "google.golang.org/protobuf/types/known/anypb" +) + +func GetById[T protoreflect.ProtoMessage]( + ctx context.Context, + client kvv1Connect.KeyValueServiceClient, + id string, + factory func() T) (T, error) { + if id == "" { + return factory(), fmt.Errorf("ID cannot be empty") + } + + t := factory() + + val, err := anypb.New(*new(T)) + if err != nil { + return t, err + } + + res, err := client.Get(ctx, connect.NewRequest(&kvv1.GetRequest{ + Key: id, + Value: val, + })) + if err != nil { + return t, err + } + + if res.Msg == nil || res.Msg.GetValue() == nil { + return t, fmt.Errorf("item with ID %s not found", id) + } + + if err := res.Msg.GetValue().UnmarshalTo(t); err != nil { + return t, fmt.Errorf("failed to unmarshal item: %v", err) + } + + return t, nil +} diff --git a/pkg/blueprint_client/go.mod b/pkg/blueprint_client/go.mod new file mode 100644 index 0000000..4affcd4 --- /dev/null +++ b/pkg/blueprint_client/go.mod @@ -0,0 +1,11 @@ +module github.com/steady-bytes/bpc/pkg/blueprint_client + +go 1.24.0 + +require ( + connectrpc.com/connect v1.18.1 + github.com/steady-bytes/draft/api v1.1.0 + google.golang.org/protobuf v1.36.7 +) + +require github.com/envoyproxy/protoc-gen-validate v1.2.1 // indirect diff --git a/pkg/blueprint_client/go.sum b/pkg/blueprint_client/go.sum new file mode 100644 index 0000000..619fc37 --- /dev/null +++ b/pkg/blueprint_client/go.sum @@ -0,0 +1,14 @@ +connectrpc.com/connect v1.18.1 h1:PAg7CjSAGvscaf6YZKUefjoih5Z/qYkyaTrBW8xvYPw= +connectrpc.com/connect v1.18.1/go.mod h1:0292hj1rnx8oFrStN7cB4jjVBeqs+Yx5yDIC2prWDO8= +github.com/envoyproxy/protoc-gen-validate v1.2.1 h1:DEo3O99U8j4hBFwbJfrz9VtgcDfUKS7KJ7spH3d86P8= +github.com/envoyproxy/protoc-gen-validate v1.2.1/go.mod h1:d/C80l/jxXLdfEIhX1W2TmLfsJ31lvEjwamM4DxlWXU= +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/steady-bytes/draft/api v1.1.0 h1:nOPLbpkWrdGAWFz22SPsSO4HR8IlckvUq7ZD5l0jn4A= +github.com/steady-bytes/draft/api v1.1.0/go.mod h1:zwZNNg8uIoQb3OqibvvcdgJPymawy2rUWh6M6Nk43a0= +golang.org/x/net v0.34.0 h1:Mb7Mrk043xzHgnRM88suvJFwzVrRfHEHJEl5/71CKw0= +golang.org/x/net v0.34.0/go.mod h1:di0qlW3YNM5oh6GqDGQr92MyTozJPmybPK4Ev/Gm31k= +golang.org/x/text v0.21.0 h1:zyQAAkrwaneQ066sspRyJaG9VNi/YJ1NfzcGB3hZ/qo= +golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ= +google.golang.org/protobuf v1.36.7 h1:IgrO7UwFQGJdRNXH/sQux4R1Dj1WAKcLElzeeRaXV2A= +google.golang.org/protobuf v1.36.7/go.mod h1:jduwjTPXsFjZGTmRluh+L6NjiWu7pchiJ2/5YcXBHnY= diff --git a/pkg/blueprint_client/list.go b/pkg/blueprint_client/list.go new file mode 100644 index 0000000..fffd8ab --- /dev/null +++ b/pkg/blueprint_client/list.go @@ -0,0 +1,47 @@ +package bpc + +import ( + "context" + "fmt" + + "connectrpc.com/connect" + kvv1 "github.com/steady-bytes/draft/api/core/registry/key_value/v1" + kvv1Connect "github.com/steady-bytes/draft/api/core/registry/key_value/v1/v1connect" + "google.golang.org/protobuf/reflect/protoreflect" + "google.golang.org/protobuf/types/known/anypb" +) + +// list retrieves all items of type T from the key-value store. +// It's understood that the response of this is not limited in size +// so use with caution in production environments. +// Blueprint needs some additional logic to handle pagination or limits. +// I don't think this can actually be improved without Blueprint having +// indexing and pagination capabilities. +func List[T protoreflect.ProtoMessage](ctx context.Context, client kvv1Connect.KeyValueServiceClient, factory func() T) ([]T, error) { + val, err := anypb.New(*new(T)) + if err != nil { + return nil, err + } + + res, err := client.List(ctx, connect.NewRequest(&kvv1.ListRequest{ + Value: val, + })) + if err != nil { + return nil, err + } + + if res.Msg == nil || len(res.Msg.GetValues()) == 0 { + return nil, fmt.Errorf("no items found") + } + + var items []T + for _, v := range res.Msg.GetValues() { + item := factory() + if err := v.UnmarshalTo(item); err != nil { + return nil, fmt.Errorf("failed to unmarshal item: %v", err) + } + items = append(items, item) + } + + return items, nil +} diff --git a/pkg/blueprint_client/list_and_filter.go b/pkg/blueprint_client/list_and_filter.go new file mode 100644 index 0000000..9372ec9 --- /dev/null +++ b/pkg/blueprint_client/list_and_filter.go @@ -0,0 +1,46 @@ +package bpc + +import ( + "context" + + kvv1 "github.com/steady-bytes/draft/api/core/registry/key_value/v1" + kvv1Connect "github.com/steady-bytes/draft/api/core/registry/key_value/v1/v1connect" + "google.golang.org/protobuf/reflect/protoreflect" +) + +func ListAndFilter[T protoreflect.ProtoMessage](ctx context.Context, client kvv1Connect.KeyValueServiceClient, filter *kvv1.Statement, factory func() T) ([]T, error) { + // This function can be used to list and filter items based on the provided filter. + // For now, it just calls the list function. + items, err := List[T](ctx, client, factory) + if err != nil { + return nil, err + } + + // filter items based on the key/value pairs in the filter + filters := filter.GetWhere().(*kvv1.Statement_KeyVal).KeyVal.GetMatch() + + // the new slice of T that will hold the filtered items + var filtered []T + + // use proto reflect to filter items based on the the key/value pairs in the filter + // NOTE: This is a simple filtering logic that checks if the field value matches the filter value in + // In the request. It's also understood this is no the most efficient way to filter items, + // but it serves as a starting point for filtering logic. + for _, item := range items { + itemValue := item.ProtoReflect() + matched := true + for key, value := range filters { + field := itemValue.Descriptor().Fields().ByName(protoreflect.Name(key)) + if field == nil || itemValue.Get(field).String() != value { + matched = false + break + } + } + + if matched { + filtered = append(filtered, item) + } + } + + return filtered, nil +} diff --git a/pkg/blueprint_client/save.go b/pkg/blueprint_client/save.go new file mode 100644 index 0000000..016694b --- /dev/null +++ b/pkg/blueprint_client/save.go @@ -0,0 +1,41 @@ +package bpc + +import ( + "context" + "fmt" + + "connectrpc.com/connect" + "google.golang.org/protobuf/reflect/protoreflect" + "google.golang.org/protobuf/types/known/anypb" + + kvv1 "github.com/steady-bytes/draft/api/core/registry/key_value/v1" + kvv1Connect "github.com/steady-bytes/draft/api/core/registry/key_value/v1/v1connect" +) + +func Save[T protoreflect.ProtoMessage]( + ctx context.Context, + client kvv1Connect.KeyValueServiceClient, + key string, + item T, +) (T, error) { + if key == "" { + // I am starting to consider that I should make a generic error type for the repository lib + return item, fmt.Errorf("key cannot be empty") + } + + val, err := anypb.New(item) + if err != nil { + return item, err + } + + req := connect.NewRequest(&kvv1.SetRequest{ + Key: key, + Value: val, + }) + + if _, err := client.Set(ctx, req); err != nil { + return item, err + } + + return item, nil +}