diff --git a/cmd/protoc-gen-go-psm/main.go b/cmd/protoc-gen-go-psm/main.go index e97a6fd..e5eb51c 100644 --- a/cmd/protoc-gen-go-psm/main.go +++ b/cmd/protoc-gen-go-psm/main.go @@ -42,7 +42,8 @@ func generateFile(gen *protogen.Plugin, file *protogen.File) { stateSets, err := state.WalkFile(file) if err != nil { - gen.Error(err) + + gen.Error(fmt.Errorf("walkFile: %w", err)) return } diff --git a/go.mod b/go.mod index 1e297c8..0fc0a6f 100644 --- a/go.mod +++ b/go.mod @@ -9,12 +9,11 @@ require ( github.com/elgris/sqrl v0.0.0-20210727210741-7e0198b30236 github.com/google/uuid v1.6.0 github.com/iancoleman/strcase v0.3.0 - github.com/lib/pq v1.10.9 - github.com/pentops/flowtest v0.0.0-20250611222350-b5c7162d9db1 + github.com/pentops/flowtest v0.0.0-20250716231535-9c97a48adf21 github.com/pentops/golib v0.0.0-20250326060930-8c83d58ddb63 - github.com/pentops/j5 v0.0.0-20250625220400-ddb847893140 + github.com/pentops/j5 v0.0.0-20250730174934-447885654c3d github.com/pentops/log.go v0.0.16 - github.com/pentops/o5-messaging v0.0.0-20250520213617-fba07334e9aa + github.com/pentops/o5-messaging v0.0.0-20250619024104-7e07c29129f0 github.com/pentops/pgtest.go v0.0.0-20241223222214-7638cc50e15b github.com/pentops/sqrlx.go v0.0.0-20250520210217-2f46de329c7a github.com/stretchr/testify v1.10.0 @@ -31,11 +30,9 @@ require ( github.com/bufbuild/protocompile v0.14.1 // indirect github.com/davecgh/go-spew v1.1.1 // indirect github.com/fatih/color v1.18.0 // indirect - github.com/golang/protobuf v1.5.4 // indirect github.com/google/cel-go v0.25.0 // indirect - github.com/google/go-cmp v0.7.0 // indirect github.com/grpc-ecosystem/go-grpc-middleware v1.4.0 // indirect - github.com/jhump/protoreflect v1.17.0 // indirect + github.com/lib/pq v1.10.9 // indirect github.com/mattn/go-colorable v0.1.14 // indirect github.com/mattn/go-isatty v0.0.20 // indirect github.com/pkg/errors v0.9.1 // indirect @@ -45,7 +42,6 @@ require ( github.com/stoewer/go-strcase v1.3.0 // indirect golang.org/x/exp v0.0.0-20250531010427-b6e5de432a8b // indirect golang.org/x/net v0.40.0 // indirect - golang.org/x/sync v0.14.0 // indirect golang.org/x/sys v0.33.0 // indirect golang.org/x/text v0.25.0 // indirect google.golang.org/genproto/googleapis/rpc v0.0.0-20250603155806-513f23925822 // indirect diff --git a/go.sum b/go.sum index cac2e12..4035a83 100644 --- a/go.sum +++ b/go.sum @@ -73,16 +73,16 @@ github.com/mattn/go-colorable v0.1.14/go.mod h1:6LmQG8QLFO4G5z1gPvYEzlUgJ2wF+stg 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/opentracing/opentracing-go v1.1.0/go.mod h1:UkNAQd3GIcIGf0SeVgPpRdFStlNbqXla1AfSYxPUl2o= -github.com/pentops/flowtest v0.0.0-20250611222350-b5c7162d9db1 h1:HFhoMQYMnf4rhXqFz9rJznYcLexvzA/tSoZW6024nUs= -github.com/pentops/flowtest v0.0.0-20250611222350-b5c7162d9db1/go.mod h1:vNp8crAKcH0f/sZU9frkmQLUeDsTIgMqV14kQtkAqC0= +github.com/pentops/flowtest v0.0.0-20250716231535-9c97a48adf21 h1:EIMCYtccNxV59aqpmPV/6iJcREagf5WiPg7KXLe3XPk= +github.com/pentops/flowtest v0.0.0-20250716231535-9c97a48adf21/go.mod h1:vNp8crAKcH0f/sZU9frkmQLUeDsTIgMqV14kQtkAqC0= github.com/pentops/golib v0.0.0-20250326060930-8c83d58ddb63 h1:s5qtWT2/s79gy/wm3/bwvKYLK6u2AkW05JiLPqxraP0= github.com/pentops/golib v0.0.0-20250326060930-8c83d58ddb63/go.mod h1:I58JIVvL1/nP4CEHGKGbBhvWIEA9mVkGeoviemaqanU= -github.com/pentops/j5 v0.0.0-20250625220400-ddb847893140 h1:TEvkzT43GUvXuTGC+8BOVWKLTmS33VncFP/EX4LL5gg= -github.com/pentops/j5 v0.0.0-20250625220400-ddb847893140/go.mod h1:DZbBKepsGataOEtfB8AjkRiejRtLGQcBejTUYJK5wlY= +github.com/pentops/j5 v0.0.0-20250730174934-447885654c3d h1:lORHJXvXiel67tT7sm+XfW7Ke2JCMXMLFAMyzhQi+mE= +github.com/pentops/j5 v0.0.0-20250730174934-447885654c3d/go.mod h1:i+eCdoCoDvULbEuPeoQm4q1YLw13oaJIlYMB+kaX6fM= github.com/pentops/log.go v0.0.16 h1:oxCuHSBOBPjfUVSXyOSEEdYUwytysj4T29/7T2FBp9Q= github.com/pentops/log.go v0.0.16/go.mod h1:yR34x8aMlvhdGvqgIU4+0MiLjJTKt0vpcgUnVN2nZV4= -github.com/pentops/o5-messaging v0.0.0-20250520213617-fba07334e9aa h1:Sdnc9mrRSefBbbrwmpq/31ABuXBwtch2KGd68ORJS44= -github.com/pentops/o5-messaging v0.0.0-20250520213617-fba07334e9aa/go.mod h1:HC8BSCybYzZGmN8x2dpbdxZCAZeziHfZPb4r8pzLUhc= +github.com/pentops/o5-messaging v0.0.0-20250619024104-7e07c29129f0 h1:aAt5rIhzFyF1RdT262LGHL0V4vBxcGCTWIeC1C0vZ20= +github.com/pentops/o5-messaging v0.0.0-20250619024104-7e07c29129f0/go.mod h1:kNBuV8uil9ZtX+eUqTbp/FkfUeK7Dkqxokfa6ASt04Y= github.com/pentops/pgtest.go v0.0.0-20241223222214-7638cc50e15b h1:UfL+A9/Nwi0QWrW7K06L8vYA+LsFRowWFtVYZICil+s= github.com/pentops/pgtest.go v0.0.0-20241223222214-7638cc50e15b/go.mod h1:U4AOJK8uL3IkJx1t5MBsOXudjZ111il31WE9UxL8Wz8= github.com/pentops/sqrlx.go v0.0.0-20250520210217-2f46de329c7a h1:v80copaLBILLt0Bozh6ZKNk8GeCNsblhB14d+ZIJUtc= diff --git a/internal/dbconvert/dbconvert.go b/internal/dbconvert/dbconvert.go deleted file mode 100644 index f5e3cf4..0000000 --- a/internal/dbconvert/dbconvert.go +++ /dev/null @@ -1,86 +0,0 @@ -package dbconvert - -import ( - "database/sql/driver" - "fmt" - "reflect" - - sq "github.com/elgris/sqrl" - "github.com/pentops/j5/j5types/date_j5t" - "google.golang.org/protobuf/encoding/protojson" - "google.golang.org/protobuf/proto" - "google.golang.org/protobuf/reflect/protoreflect" - "google.golang.org/protobuf/types/known/timestamppb" -) - -func FieldsToDBValues(m map[string]any) (map[string]any, error) { - out := map[string]any{} - for k, v := range m { - converted, err := interfaceToDBValue(v) - if err != nil { - return nil, fmt.Errorf("field values: %w", err) - } - - out[k] = converted - } - return out, nil -} - -func FieldsToEqMap(ofTable string, m map[string]any) (sq.Eq, error) { - out := sq.Eq{} - for k, v := range m { - converted, err := interfaceToDBValue(v) - if err != nil { - return nil, fmt.Errorf("eq map: %w", err) - } - - fullKey := fmt.Sprintf("%s.%s", ofTable, k) - out[fullKey] = converted - } - return out, nil -} - -func interfaceToDBValue(i any) (any, error) { - switch v := i.(type) { - case *timestamppb.Timestamp: - return v.AsTime(), nil - - case *date_j5t.Date: - return v.DateString(), nil - - case driver.Valuer: - return v, nil - - case proto.Message: - i, err := MarshalProto(v) - if err != nil { - return nil, fmt.Errorf("interface values: %w", err) - } - - return i, nil - case *string: - if v == nil { - return nil, nil - } - return v, nil - } - - switch reflect.TypeOf(i).Kind() { - case reflect.Ptr, reflect.Map, reflect.Chan, reflect.Slice, reflect.Array: - if reflect.ValueOf(i).IsNil() { - return nil, nil - } - } - - return i, nil -} - -func MarshalProto(state protoreflect.ProtoMessage) ([]byte, error) { - // EmitDefaultValues behaves similarly to EmitUnpopulated, but does not emit "null"-value fields, - // i.e. presence-sensing fields that are omitted will remain omitted to preserve presence-sensing. - b, err := protojson.MarshalOptions{EmitDefaultValues: true}.Marshal(state) - if err != nil { - return b, fmt.Errorf("custom protomarshal: %w", err) - } - return b, nil -} diff --git a/internal/integration/query_test/filter_test.go b/internal/integration/query_test/filter_test.go deleted file mode 100644 index 6f62967..0000000 --- a/internal/integration/query_test/filter_test.go +++ /dev/null @@ -1,872 +0,0 @@ -package integration - -import ( - "context" - "testing" - "time" - - "github.com/google/uuid" - "github.com/pentops/flowtest" - "github.com/pentops/j5/gen/j5/auth/v1/auth_j5pb" - "github.com/pentops/j5/gen/j5/list/v1/list_j5pb" - "github.com/pentops/protostate/internal/testproto/gen/test/v1/test_pb" - "github.com/pentops/protostate/internal/testproto/gen/test/v1/test_spb" - "google.golang.org/protobuf/proto" -) - -func TestDefaultFiltering(t *testing.T) { - ss, uu := NewFooUniverse(t) - sm := uu.SM - db := uu.DB - queryer := uu.Query - var err error - defer ss.RunSteps(t) - - tenants := []string{uuid.NewString()} - tenantIDs := setupFooListableData(ss, sm, tenants, 10) - - ss.Step("Setup Extra Statuses", func(ctx context.Context, t flowtest.Asserter) { - for _, id := range tenantIDs[tenants[0]][:2] { - event := newFooDeletedEvent(id, tenants[0]) - - _, err := sm.Transition(ctx, event) - if err != nil { - t.Fatal(err.Error()) - } - } - }) - - ss.Step("List Page", func(ctx context.Context, t flowtest.Asserter) { - req := &test_spb.FooListRequest{ - Page: &list_j5pb.PageRequest{ - PageSize: proto.Int64(10), - }, - } - res := &test_spb.FooListResponse{} - - err = queryer.List(ctx, db, req, res) - if err != nil { - t.Fatal(err.Error()) - } - - if len(res.Foo) != 8 { - t.Fatalf("expected %d states, got %d", 8, len(res.Foo)) - } - - for ii, state := range res.Foo { - t.Logf("%d: %s", ii, state.Data.Field) - } - - for _, state := range res.Foo { - if state.Status != test_pb.FooStatus_ACTIVE { - t.Fatalf("expected status %s, got %s", test_pb.FooStatus_ACTIVE, state.Status) - } - } - - if res.Page != nil { - t.Fatalf("page response should be empty") - } - }) -} - -func TestFilteringWithAuthScope(t *testing.T) { - ss, uu := NewFooUniverse(t) - sm := uu.SM - db := uu.DB - queryer := uu.Query - var err error - defer ss.RunSteps(t) - - tenantID1 := uuid.NewString() - tenantID2 := uuid.NewString() - - tenants := []string{tenantID1, tenantID2} - setupFooListableData(ss, sm, tenants, 10) - - tkn := &token{ - claim: &auth_j5pb.Claim{ - TenantType: "tenant", - TenantId: tenantID1, - }, - } - - ss.Step("List Page", func(ctx context.Context, t flowtest.Asserter) { - ctx = tkn.WithToken(ctx) - - req := &test_spb.FooListRequest{ - Page: &list_j5pb.PageRequest{ - PageSize: proto.Int64(5), - }, - Query: &list_j5pb.QueryRequest{ - Filters: []*list_j5pb.Filter{ - { - Type: &list_j5pb.Filter_Field{ - Field: &list_j5pb.Field{ - Name: "data.characteristics.weight", - Type: &list_j5pb.FieldType{ - Type: &list_j5pb.FieldType_Range{ - Range: &list_j5pb.Range{ - Min: "12", - Max: "15", - }, - }, - }, - }, - }, - }, - }, - }, - } - res := &test_spb.FooListResponse{} - - err = queryer.List(ctx, db, req, res) - if err != nil { - t.Fatal(err.Error()) - } - - if len(res.Foo) != 4 { - t.Fatalf("expected %d states, got %d", 4, len(res.Foo)) - } - - for ii, state := range res.Foo { - t.Logf("%d: %s (%s)", ii, state.Data.Field, state.Metadata.CreatedAt.AsTime().Format(time.RFC3339Nano)) - } - - for ii, state := range res.Foo { - if *state.Keys.TenantId != tenantID1 { - t.Fatalf("expected tenant ID %s, got %s", tenantID1, state.Keys.TenantId) - } - if state.Data.Characteristics.Weight != int64(15-ii) { - t.Fatalf("expected weight %d, got %d", 15-ii, state.Data.Characteristics.Weight) - } - - } - - if res.Page != nil { - t.Fatalf("page response should be empty") - } - }) -} - -func TestDynamicFiltering(t *testing.T) { - ss, uu := NewFooUniverse(t) - sm := uu.SM - db := uu.DB - queryer := uu.Query - var err error - defer ss.RunSteps(t) - - tenants := []string{uuid.NewString()} - ids := setupFooListableData(ss, sm, tenants, 60) - - t.Run("Single Range Filter", func(t *testing.T) { - ss.Step("List Page", func(ctx context.Context, t flowtest.Asserter) { - req := &test_spb.FooListRequest{ - Page: &list_j5pb.PageRequest{ - PageSize: proto.Int64(5), - }, - Query: &list_j5pb.QueryRequest{ - Filters: []*list_j5pb.Filter{ - { - Type: &list_j5pb.Filter_Field{ - Field: &list_j5pb.Field{ - Name: "data.characteristics.weight", - Type: &list_j5pb.FieldType{ - Type: &list_j5pb.FieldType_Range{ - Range: &list_j5pb.Range{ - Min: "12", - Max: "15", - }, - }, - }, - }, - }, - }, - }, - }, - } - res := &test_spb.FooListResponse{} - - err = queryer.List(ctx, db, req, res) - if err != nil { - t.Fatal(err.Error()) - } - - if len(res.Foo) != 4 { - t.Fatalf("expected %d states, got %d", 4, len(res.Foo)) - } - - for ii, state := range res.Foo { - t.Logf("%d: %s", ii, state.Data.Field) - } - - for ii, state := range res.Foo { - if state.Data.Characteristics.Weight != int64(15-ii) { - t.Fatalf("expected weight %d, got %d", 15-ii, state.Data.Characteristics.Weight) - } - } - - if res.Page != nil { - t.Fatalf("page response should be empty") - } - }) - }) - - t.Run("Min Range Filter", func(t *testing.T) { - ss.Step("List Page", func(ctx context.Context, t flowtest.Asserter) { - req := &test_spb.FooListRequest{ - Page: &list_j5pb.PageRequest{ - PageSize: proto.Int64(5), - }, - Query: &list_j5pb.QueryRequest{ - Filters: []*list_j5pb.Filter{ - { - Type: &list_j5pb.Filter_Field{ - Field: &list_j5pb.Field{ - Name: "data.characteristics.weight", - Type: &list_j5pb.FieldType{ - Type: &list_j5pb.FieldType_Range{ - Range: &list_j5pb.Range{ - Min: "12", - }, - }, - }, - }, - }, - }, - }, - }, - } - res := &test_spb.FooListResponse{} - - err = queryer.List(ctx, db, req, res) - if err != nil { - t.Fatal(err.Error()) - } - - if len(res.Foo) != 5 { - t.Fatalf("expected %d states, got %d", 5, len(res.Foo)) - } - - for ii, state := range res.Foo { - t.Logf("%d: %s", ii, state.Data.Field) - } - - for _, state := range res.Foo { - if state.Data.Characteristics.Weight < int64(12) { - t.Fatalf("expected weights greater than or equal to %d, got %d", 12, state.Data.Characteristics.Weight) - } - } - }) - }) - - t.Run("Max Range Filter", func(t *testing.T) { - ss.Step("List Page", func(ctx context.Context, t flowtest.Asserter) { - req := &test_spb.FooListRequest{ - Page: &list_j5pb.PageRequest{ - PageSize: proto.Int64(5), - }, - Query: &list_j5pb.QueryRequest{ - Filters: []*list_j5pb.Filter{ - { - Type: &list_j5pb.Filter_Field{ - Field: &list_j5pb.Field{ - Name: "data.characteristics.weight", - Type: &list_j5pb.FieldType{ - Type: &list_j5pb.FieldType_Range{ - Range: &list_j5pb.Range{ - Max: "15", - }, - }, - }, - }, - }, - }, - }, - }, - } - res := &test_spb.FooListResponse{} - - err = queryer.List(ctx, db, req, res) - if err != nil { - t.Fatal(err.Error()) - } - - if len(res.Foo) != 5 { - t.Fatalf("expected %d states, got %d", 5, len(res.Foo)) - } - - for ii, state := range res.Foo { - t.Logf("%d: %s", ii, state.Data.Field) - } - - for _, state := range res.Foo { - if state.Data.Characteristics.Weight > int64(15) { - t.Fatalf("expected weight less than or equal to %d, got %d", 15, state.Data.Characteristics.Weight) - } - } - }) - }) - - t.Run("Multi Range Filter", func(t *testing.T) { - nextToken := "" - ss.Step("List Page 1", func(ctx context.Context, t flowtest.Asserter) { - req := &test_spb.FooListRequest{ - Page: &list_j5pb.PageRequest{ - PageSize: proto.Int64(10), - }, - Query: &list_j5pb.QueryRequest{ - Filters: []*list_j5pb.Filter{ - { - Type: &list_j5pb.Filter_Or{ - Or: &list_j5pb.Or{ - Filters: []*list_j5pb.Filter{ - { - Type: &list_j5pb.Filter_Field{ - Field: &list_j5pb.Field{ - Name: "data.characteristics.weight", - Type: &list_j5pb.FieldType{ - Type: &list_j5pb.FieldType_Range{ - Range: &list_j5pb.Range{ - Min: "12", - Max: "20", - }, - }, - }, - }, - }, - }, - { - Type: &list_j5pb.Filter_Field{ - Field: &list_j5pb.Field{ - Name: "data.characteristics.height", - Type: &list_j5pb.FieldType{ - Type: &list_j5pb.FieldType_Range{ - Range: &list_j5pb.Range{ - Min: "16", - Max: "18", - }, - }, - }, - }, - }, - }, - }, - }, - }, - }, - }, - }, - } - res := &test_spb.FooListResponse{} - - err = queryer.List(ctx, db, req, res) - if err != nil { - t.Fatal(err.Error()) - } - - for ii, state := range res.Foo { - t.Logf("%d: %s", ii, state.Data.Field) - } - - if len(res.Foo) != 10 { - t.Fatalf("expected %d states, got %d", 10, len(res.Foo)) - } - - for ii, state := range res.Foo[:3] { - if state.Data.Characteristics.Weight != int64(44-ii) { - t.Fatalf("expected weight %d, got %d", 44-ii, state.Data.Characteristics.Weight) - } - } - - for ii, state := range res.Foo[3:] { - if state.Data.Characteristics.Weight != int64(20-ii) { - t.Fatalf("expected weight %d, got %d", 20-ii, state.Data.Characteristics.Weight) - } - } - - pageResp := res.Page - - if pageResp.GetNextToken() == "" { - t.Fatalf("NextToken should not be empty") - } - if pageResp.NextToken == nil { - t.Fatalf("Should not be the final page") - } - - nextToken = pageResp.GetNextToken() - }) - - ss.Step("List Page 2", func(ctx context.Context, t flowtest.Asserter) { - req := &test_spb.FooListRequest{ - Page: &list_j5pb.PageRequest{ - PageSize: proto.Int64(10), - Token: &nextToken, - }, - Query: &list_j5pb.QueryRequest{ - Filters: []*list_j5pb.Filter{ - { - Type: &list_j5pb.Filter_Or{ - Or: &list_j5pb.Or{ - Filters: []*list_j5pb.Filter{ - { - Type: &list_j5pb.Filter_Field{ - Field: &list_j5pb.Field{ - Name: "data.characteristics.weight", - Type: &list_j5pb.FieldType{ - Type: &list_j5pb.FieldType_Range{ - Range: &list_j5pb.Range{ - Min: "12", - Max: "20", - }, - }, - }, - }, - }, - }, - { - Type: &list_j5pb.Filter_Field{ - Field: &list_j5pb.Field{ - Name: "data.characteristics.height", - Type: &list_j5pb.FieldType{ - Type: &list_j5pb.FieldType_Range{ - Range: &list_j5pb.Range{ - Min: "16", - Max: "18", - }, - }, - }, - }, - }, - }, - }, - }, - }, - }, - }, - }, - } - res := &test_spb.FooListResponse{} - - err = queryer.List(ctx, db, req, res) - if err != nil { - t.Fatal(err.Error()) - } - - for ii, state := range res.Foo { - t.Logf("%d: %s", ii, state.Data.Field) - } - - if len(res.Foo) != 2 { - t.Fatalf("expected %d states, got %d", 2, len(res.Foo)) - } - - for ii, state := range res.Foo { - if state.Data.Characteristics.Weight != int64(13-ii) { - t.Fatalf("expected weight %d, got %d", 13-ii, state.Data.Characteristics.Weight) - } - } - - if res.Page != nil { - t.Fatalf("page response should be empty") - } - }) - }) - - t.Run("Flattened filterable fields", func(t *testing.T) { - ss.Step("List Page", func(ctx context.Context, t flowtest.Asserter) { - req := &test_spb.FooListRequest{ - Page: &list_j5pb.PageRequest{ - PageSize: proto.Int64(5), - }, - Query: &list_j5pb.QueryRequest{ - Filters: []*list_j5pb.Filter{ - { - Type: &list_j5pb.Filter_Field{ - Field: &list_j5pb.Field{ - Name: "tenantId", - Type: &list_j5pb.FieldType{ - Type: &list_j5pb.FieldType_Value{ - Value: tenants[0], - }, - }, - }, - }, - }, - }, - }, - } - res := &test_spb.FooListResponse{} - - err = queryer.List(ctx, db, req, res) - if err != nil { - t.Fatal(err) - } - - if len(res.Foo) == 0 { - t.Fatalf("expected to receive results filtered by tenantId, but got none") - } - - for _, foo := range res.Foo { - if *foo.Keys.TenantId != tenants[0] { - t.Fatalf("expected tenantId %s, got %s", tenants[0], *foo.Keys.TenantId) - } - } - }) - }) - - t.Run("Non filterable fields", func(t *testing.T) { - ss.Step("List Page", func(ctx context.Context, t flowtest.Asserter) { - req := &test_spb.FooListRequest{ - Page: &list_j5pb.PageRequest{ - PageSize: proto.Int64(5), - }, - Query: &list_j5pb.QueryRequest{ - Filters: []*list_j5pb.Filter{ - { - Type: &list_j5pb.Filter_Field{ - Field: &list_j5pb.Field{ - Name: "foo_id", - Type: &list_j5pb.FieldType{ - Type: &list_j5pb.FieldType_Value{ - Value: "d34d826f-afe3-410d-8326-4e9af3f09467", - }, - }, - }, - }, - }, - }, - }, - } - res := &test_spb.FooListResponse{} - - err = queryer.List(ctx, db, req, res) - if err == nil { - t.Fatalf("expected error, got nil") - } - }) - }) - - t.Run("Enum values", func(t *testing.T) { - ss.Step("List Page short enum name", func(ctx context.Context, t flowtest.Asserter) { - req := &test_spb.FooListRequest{ - Page: &list_j5pb.PageRequest{ - PageSize: proto.Int64(5), - }, - Query: &list_j5pb.QueryRequest{ - Filters: []*list_j5pb.Filter{ - { - Type: &list_j5pb.Filter_Field{ - Field: &list_j5pb.Field{ - Name: "status", - Type: &list_j5pb.FieldType{ - Type: &list_j5pb.FieldType_Value{ - Value: "active", - }, - }, - }, - }, - }, - }, - }, - } - res := &test_spb.FooListResponse{} - - err := queryer.List(ctx, db, req, res) - if err != nil { - t.Fatal(err) - } - - if len(res.Foo) != 5 { - t.Fatalf("expected %d states, got %d", 5, len(res.Foo)) - } - - for ii, state := range res.Foo { - t.Logf("%d: %s", ii, state.Data.Field) - } - - for _, state := range res.Foo { - if state.Status != test_pb.FooStatus_ACTIVE { - t.Fatalf("expected status %s, got %s", test_pb.FooStatus_ACTIVE, state.Status) - } - } - }) - - ss.Step("List Page full enum name", func(ctx context.Context, t flowtest.Asserter) { - req := &test_spb.FooListRequest{ - Page: &list_j5pb.PageRequest{ - PageSize: proto.Int64(5), - }, - Query: &list_j5pb.QueryRequest{ - Filters: []*list_j5pb.Filter{ - { - Type: &list_j5pb.Filter_Field{ - Field: &list_j5pb.Field{ - Name: "status", - Type: &list_j5pb.FieldType{ - Type: &list_j5pb.FieldType_Value{ - Value: "FOO_STATUS_ACTIVE", - }, - }, - }, - }, - }, - }, - }, - } - res := &test_spb.FooListResponse{} - - err := queryer.List(ctx, db, req, res) - if err != nil { - t.Fatal(err) - } - - if len(res.Foo) != 5 { - t.Fatalf("expected %d states, got %d", 5, len(res.Foo)) - } - - for ii, state := range res.Foo { - t.Logf("%d: %s", ii, state.Data.Field) - } - - for _, state := range res.Foo { - if state.Status != test_pb.FooStatus_ACTIVE { - t.Fatalf("expected status %s, got %s", test_pb.FooStatus_ACTIVE, state.Status) - } - } - }) - - ss.Step("List Page bad enum name", func(ctx context.Context, t flowtest.Asserter) { - req := &test_spb.FooListRequest{ - Page: &list_j5pb.PageRequest{ - PageSize: proto.Int64(5), - }, - Query: &list_j5pb.QueryRequest{ - Filters: []*list_j5pb.Filter{ - { - Type: &list_j5pb.Filter_Field{ - Field: &list_j5pb.Field{ - Name: "status", - Type: &list_j5pb.FieldType{ - Type: &list_j5pb.FieldType_Value{ - Value: "FOO_STATUS_UNUSED", - }, - }, - }, - }, - }, - }, - }, - } - res := &test_spb.FooListResponse{} - - err := queryer.List(ctx, db, req, res) - if err == nil { - t.Fatal("expected error, got nil") - } - }) - }) - - t.Run("Single Complex Range Filter", func(t *testing.T) { - nextToken := "" - ss.Step("List Page 1", func(ctx context.Context, t flowtest.Asserter) { - req := &test_spb.FooListRequest{ - Page: &list_j5pb.PageRequest{ - PageSize: proto.Int64(5), - }, - Query: &list_j5pb.QueryRequest{ - Filters: []*list_j5pb.Filter{ - { - Type: &list_j5pb.Filter_Field{ - Field: &list_j5pb.Field{ - Name: "data.profiles.place", - Type: &list_j5pb.FieldType{ - Type: &list_j5pb.FieldType_Range{ - Range: &list_j5pb.Range{ - Min: "15", - Max: "21", - }, - }, - }, - }, - }, - }, - }, - }, - } - res := &test_spb.FooListResponse{} - - err = queryer.List(ctx, db, req, res) - if err != nil { - t.Fatal(err.Error()) - } - - for ii, state := range res.Foo { - t.Logf("%d: %s", ii, state.Data.Profiles) - } - - if len(res.Foo) != 5 { - t.Fatalf("expected %d states, got %d", 5, len(res.Foo)) - } - - for _, state := range res.Foo { - matched := false - for _, profile := range state.Data.Profiles { - if profile.Place >= 17 && profile.Place <= 21 { - matched = true - break - } - } - - if !matched { - t.Fatalf("expected at least one profile to match the filter") - } - } - - pageResp := res.Page - - if pageResp.GetNextToken() == "" { - t.Fatalf("NextToken should not be empty") - } - if pageResp.NextToken == nil { - t.Fatalf("Should not be the final page") - } - - nextToken = pageResp.GetNextToken() - }) - - ss.Step("List Page 2", func(ctx context.Context, t flowtest.Asserter) { - req := &test_spb.FooListRequest{ - Page: &list_j5pb.PageRequest{ - PageSize: proto.Int64(5), - Token: &nextToken, - }, - Query: &list_j5pb.QueryRequest{ - Filters: []*list_j5pb.Filter{ - { - Type: &list_j5pb.Filter_Field{ - Field: &list_j5pb.Field{ - Name: "data.profiles.place", - Type: &list_j5pb.FieldType{ - Type: &list_j5pb.FieldType_Range{ - Range: &list_j5pb.Range{ - Min: "15", - Max: "21", - }, - }, - }, - }, - }, - }, - }, - }, - } - res := &test_spb.FooListResponse{} - - err = queryer.List(ctx, db, req, res) - if err != nil { - t.Fatal(err.Error()) - } - - for ii, state := range res.Foo { - t.Logf("%d: %s", ii, state.Data.Profiles) - } - - if len(res.Foo) != 2 { - t.Fatalf("expected %d states, got %d", 2, len(res.Foo)) - } - - for _, state := range res.Foo { - matched := false - for _, profile := range state.Data.Profiles { - if profile.Place >= 15 && profile.Place <= 16 { - matched = true - break - } - } - - if !matched { - t.Fatalf("expected at least one profile to match the filter") - } - } - }) - }) - - t.Run("Oneof filter", func(t *testing.T) { - ss.Step("List Page (created)", func(ctx context.Context, t flowtest.Asserter) { - req := &test_spb.FooEventsRequest{ - FooId: ids[tenants[0]][0], - Page: &list_j5pb.PageRequest{ - PageSize: proto.Int64(5), - }, - Query: &list_j5pb.QueryRequest{ - Filters: []*list_j5pb.Filter{ - { - Type: &list_j5pb.Filter_Field{ - Field: &list_j5pb.Field{ - Name: "event.type", - Type: &list_j5pb.FieldType{ - Type: &list_j5pb.FieldType_Value{ - Value: "created", - }, - }, - }, - }, - }, - }, - }, - } - res := &test_spb.FooEventsResponse{} - - err := queryer.EventLister.List(ctx, db, req, res) - if err != nil { - t.Fatal(err) - } - - if len(res.Events) != 1 { - t.Fatalf("expected %d states, got %d", 1, len(res.Events)) - } - - for ii, event := range res.Events { - switch event.Event.Type.(type) { - case *test_pb.FooEventType_Created_: - default: - t.Fatalf("expected event to be of type %T, got %T", &test_pb.FooEventType_Created_{}, event.Event.Type) - } - - t.Logf("%d: %s", ii, event.Event) - } - }) - - ss.Step("List Page bad name", func(ctx context.Context, t flowtest.Asserter) { - req := &test_spb.FooEventsRequest{ - FooId: ids[tenants[0]][0], - Page: &list_j5pb.PageRequest{ - PageSize: proto.Int64(5), - }, - Query: &list_j5pb.QueryRequest{ - Filters: []*list_j5pb.Filter{ - { - Type: &list_j5pb.Filter_Field{ - Field: &list_j5pb.Field{ - Name: "event.type", - Type: &list_j5pb.FieldType{ - Type: &list_j5pb.FieldType_Value{ - Value: "damaged", - }, - }, - }, - }, - }, - }, - }, - } - res := &test_spb.FooEventsResponse{} - - err := queryer.EventLister.List(ctx, db, req, res) - if err == nil { - t.Fatal("expected error, got nil") - } - }) - }) -} diff --git a/internal/integration/query_test/helpers.go b/internal/integration/query_test/helpers.go deleted file mode 100644 index c71e087..0000000 --- a/internal/integration/query_test/helpers.go +++ /dev/null @@ -1,132 +0,0 @@ -package integration - -import ( - "context" - "fmt" - "log/slog" - "testing" - "time" - - "github.com/elgris/sqrl" - "github.com/google/uuid" - "github.com/pentops/flowtest" - "github.com/pentops/j5/gen/j5/auth/v1/auth_j5pb" - "github.com/pentops/log.go/log" - "github.com/pentops/protostate/internal/testproto/gen/test/v1/test_pb" - "github.com/pentops/sqrlx.go/sqrlx" - "k8s.io/utils/ptr" -) - -func silenceLogger() func() { - defaultLogger := log.DefaultLogger - log.DefaultLogger = log.NewCallbackLogger(func(level string, msg string, fields []slog.Attr) { - }) - return func() { - log.DefaultLogger = defaultLogger - } -} - -func printQuery(t flowtest.TB, query *sqrl.SelectBuilder) { - stmt, args, err := query.ToSql() - if err != nil { - t.Fatal(err.Error()) - } - t.Log(stmt, args) -} - -func getRawState(db sqrlx.Transactor, id string) (string, error) { - var state []byte - err := db.Transact(context.Background(), nil, func(ctx context.Context, tx sqrlx.Transaction) error { - q := sqrl.Select("state").From("foo").Where("foo_id = ?", id) - err := tx.QueryRow(ctx, q).Scan(&state) - if err != nil { - return err - } - - return nil - }) - - if err != nil { - return "", err - } - - return string(state), nil -} - -func getRawEvent(db sqrlx.Transactor, id string) (string, error) { - var data []byte - err := db.Transact(context.Background(), nil, func(ctx context.Context, tx sqrlx.Transaction) error { - q := sqrl.Select("data").From("foo_event").Where("id = ?", id) - err := tx.QueryRow(ctx, q).Scan(&data) - if err != nil { - return err - } - - return nil - }) - - if err != nil { - return "", err - } - - return string(data), nil -} - -func setupFooListableData(ss *flowtest.Stepper[*testing.T], sm *test_pb.FooPSMDB, tenants []string, count int) map[string][]string { - ids := make(map[string][]string, len(tenants)) - - for ti := range tenants { - ids[tenants[ti]] = make([]string, 0, count) - for range count { - ids[tenants[ti]] = append(ids[tenants[ti]], uuid.NewString()) - } - } - - ss.Step("Create", func(ctx context.Context, t flowtest.Asserter) { - ti := 0 - for tenant, fooIDs := range ids { - tkn := &token{ - claim: &auth_j5pb.Claim{ - TenantType: "tenant", - TenantId: tenant, - }, - } - ctx = tkn.WithToken(ctx) - - restore := silenceLogger() - defer restore() - - for ii, fooID := range fooIDs { - tt := time.Now() - - event := newFooCreatedEvent(fooID, tenants[ti], func(c *test_pb.FooEventType_Created) { - c.Field = fmt.Sprintf("foo %d at %s (weighted %d, height %d, length %d)", ii, tt.Format(time.RFC3339Nano), (10+ii)*(ti+1), (50-ii)*(ti+1), (ii%2)*(ti+1)) - c.Weight = ptr.To((10 + int64(ii)) * (int64(ti) + 1)) - c.Height = ptr.To((50 - int64(ii)) * (int64(ti) + 1)) - c.Length = ptr.To((int64(ii%2) * (int64(ti) + 1))) - c.Profiles = []*test_pb.FooProfile{ - { - Name: fmt.Sprintf("profile %d", ii), - Place: int64(ii) + 50, - }, - { - Name: fmt.Sprintf("profile %d", ii), - Place: int64(ii) + 15, - }, - } - }) - - stateOut, err := sm.Transition(ctx, event) - if err != nil { - t.Fatalf("setup foo: %s (%#v)", err.Error(), event.Keys) - } - t.Equal(test_pb.FooStatus_ACTIVE, stateOut.Status) - t.Equal(tenants[ti], *stateOut.Keys.TenantId) - } - - ti++ - } - }) - - return ids -} diff --git a/internal/integration/query_test/marshalling_test.go b/internal/integration/query_test/marshalling_test.go deleted file mode 100644 index 9f009cb..0000000 --- a/internal/integration/query_test/marshalling_test.go +++ /dev/null @@ -1,263 +0,0 @@ -package integration - -import ( - "strings" - "testing" - - "github.com/google/uuid" - "github.com/pentops/protostate/internal/testproto/gen/test/v1/test_pb" - "github.com/pentops/protostate/internal/testproto/gen/test/v1/test_spb" - "k8s.io/utils/ptr" -) - -func TestMarshaling(t *testing.T) { - ss, uu := NewFooUniverse(t) - sm := uu.SM - db := uu.DB - queryer := uu.Query - defer ss.RunSteps(t) - - tenantID := uuid.NewString() - - t.Run("Optional field", func(t *testing.T) { - fooID := uuid.NewString() - - t.Run("Get with empty", func(t *testing.T) { - ctx := t.Context() - event := newFooCreatedEvent(fooID, tenantID, func(c *test_pb.FooEventType_Created) { - c.Description = ptr.To("") - }) - - _, err := sm.Transition(ctx, event) - if err != nil { - t.Fatal(err.Error()) - } - - req := &test_spb.FooGetRequest{ - FooId: fooID, - } - - res := &test_spb.FooGetResponse{} - - err = queryer.Get(ctx, db, req, res) - if err != nil { - t.Fatal(err.Error()) - } - - if res.Foo.Data.Description == nil { - t.Fatalf("expected description to be non nil") - } - - if *res.Foo.Data.Description != "" { - t.Fatalf("expected description to be empty, got %s", *res.Foo.Data.Description) - } - - stateJSON, err := getRawState(db, fooID) - if err != nil { - t.Fatal(err.Error()) - } - - if !strings.Contains(stateJSON, `"description": ""`) { - t.Fatalf("expected description to be present, but empty: %s", stateJSON) - } - - eventJSON, err := getRawEvent(db, res.Events[len(res.Events)-1].Metadata.EventId) - if err != nil { - t.Fatal(err.Error()) - } - - if !strings.Contains(eventJSON, `"description": ""`) { - t.Fatalf("expected description to be present, but empty: %s", eventJSON) - } - }) - - t.Run("Get with non empty", func(t *testing.T) { - ctx := t.Context() - event := newFooUpdatedEvent(fooID, tenantID, func(u *test_pb.FooEventType_Updated) { - u.Description = ptr.To("non blank description") - }) - _, err := sm.Transition(ctx, event) - if err != nil { - t.Fatal(err.Error()) - } - - req := &test_spb.FooGetRequest{ - FooId: fooID, - } - - res := &test_spb.FooGetResponse{} - - err = queryer.Get(ctx, db, req, res) - if err != nil { - t.Fatal(err.Error()) - } - - if res.Foo.Data.Description == nil { - t.Fatalf("expected description to be non nil") - } - - if *res.Foo.Data.Description == "" { - t.Fatalf("expected description to be empty, got %s", *res.Foo.Data.Description) - } - - stateJSON, err := getRawState(db, fooID) - if err != nil { - t.Fatal(err.Error()) - } - - if !strings.Contains(stateJSON, `"description": "non blank description"`) { - t.Fatalf("expected description to be present, and not empty: %s", stateJSON) - } - - eventJSON, err := getRawEvent(db, res.Events[len(res.Events)-1].Metadata.EventId) - if err != nil { - t.Fatal(err.Error()) - } - - if !strings.Contains(eventJSON, `"description":`) { - t.Fatalf("expected description to be present: %s", eventJSON) - } - }) - - t.Run("Get with missing", func(t *testing.T) { - ctx := t.Context() - event := newFooUpdatedEvent(fooID, tenantID, func(u *test_pb.FooEventType_Updated) { - u.Description = nil - }) - - _, err := sm.Transition(ctx, event) - if err != nil { - t.Fatal(err.Error()) - } - - req := &test_spb.FooGetRequest{ - FooId: fooID, - } - - res := &test_spb.FooGetResponse{} - - err = queryer.Get(ctx, db, req, res) - if err != nil { - t.Fatal(err.Error()) - } - - if res.Foo.Data.Description != nil { - t.Fatalf("expected description to be nil") - } - - stateJSON, err := getRawState(db, fooID) - if err != nil { - t.Fatal(err.Error()) - } - - if strings.Contains(stateJSON, `"description":`) { - t.Fatalf("expected description to not be present: %s", stateJSON) - } - - eventJSON, err := getRawEvent(db, res.Events[len(res.Events)-1].Metadata.EventId) - if err != nil { - t.Fatal(err.Error()) - } - - if strings.Contains(eventJSON, `"description":`) { - t.Fatalf("expected description to not be present: %s", eventJSON) - } - }) - }) - - t.Run("Non optional field", func(t *testing.T) { - ctx := t.Context() - fooID := uuid.NewString() - - t.Run("Get with empty", func(t *testing.T) { - event := newFooCreatedEvent(fooID, tenantID, func(c *test_pb.FooEventType_Created) { - c.Field = "" - c.Description = nil - }) - - _, err := sm.Transition(ctx, event) - if err != nil { - t.Fatal(err.Error()) - } - - req := &test_spb.FooGetRequest{ - FooId: fooID, - } - - res := &test_spb.FooGetResponse{} - - err = queryer.Get(ctx, db, req, res) - if err != nil { - t.Fatal(err.Error()) - } - - if res.Foo.Data.Field != "" { - t.Fatalf("expected description to be empty") - } - - stateJSON, err := getRawState(db, fooID) - if err != nil { - t.Fatal(err.Error()) - } - - if !strings.Contains(stateJSON, `"field": ""`) { - t.Fatalf("expected field to be present, but empty: %s", stateJSON) - } - - eventJSON, err := getRawEvent(db, res.Events[len(res.Events)-1].Metadata.EventId) - if err != nil { - t.Fatal(err.Error()) - } - - if !strings.Contains(eventJSON, `"field": ""`) { - t.Fatalf("expected field to be present, but empty: %s", eventJSON) - } - }) - - t.Run("Get with non empty", func(t *testing.T) { - ctx := t.Context() - event := newFooUpdatedEvent(fooID, tenantID, func(u *test_pb.FooEventType_Updated) { - u.Field = "non empty" - u.Description = nil - }) - - _, err := sm.Transition(ctx, event) - if err != nil { - t.Fatal(err.Error()) - } - - req := &test_spb.FooGetRequest{ - FooId: fooID, - } - - res := &test_spb.FooGetResponse{} - - err = queryer.Get(ctx, db, req, res) - if err != nil { - t.Fatal(err.Error()) - } - - if res.Foo.Data.Field == "" { - t.Fatalf("expected description to be non empty") - } - - stateJSON, err := getRawState(db, fooID) - if err != nil { - t.Fatal(err.Error()) - } - - if !strings.Contains(stateJSON, `"field":`) { - t.Fatalf("expected field to be present: %s", stateJSON) - } - - eventJSON, err := getRawEvent(db, res.Events[len(res.Events)-1].Metadata.EventId) - if err != nil { - t.Fatal(err.Error()) - } - - if !strings.Contains(eventJSON, `"field":`) { - t.Fatalf("expected field to be present: %s", eventJSON) - } - }) - }) -} diff --git a/internal/integration/query_test/pagination_test.go b/internal/integration/query_test/pagination_test.go deleted file mode 100644 index 4a79c00..0000000 --- a/internal/integration/query_test/pagination_test.go +++ /dev/null @@ -1,350 +0,0 @@ -package integration - -import ( - "context" - "encoding/base64" - "fmt" - "testing" - "time" - - "github.com/google/uuid" - "github.com/pentops/flowtest" - "github.com/pentops/j5/gen/j5/list/v1/list_j5pb" - "github.com/pentops/protostate/internal/testproto/gen/test/v1/test_pb" - "github.com/pentops/protostate/internal/testproto/gen/test/v1/test_spb" - "github.com/pentops/protostate/psm" - "google.golang.org/protobuf/encoding/protojson" - "google.golang.org/protobuf/proto" - "k8s.io/utils/ptr" -) - -func TestPagination(t *testing.T) { - ss, uu := NewFooUniverse(t) - sm := uu.SM - defer ss.RunSteps(t) - - queryer, err := test_spb.NewFooPSMQuerySet(test_spb.DefaultFooPSMQuerySpec(sm.StateTableSpec()), psm.StateQueryOptions{}) - if err != nil { - t.Fatal(err.Error()) - } - - ss.Step("Create", func(ctx context.Context, t flowtest.Asserter) { - tenantID := uuid.NewString() - - restore := silenceLogger() - defer restore() - - for ii := range 30 { - tt := time.Now() - fooID := uuid.NewString() - - event := newFooCreatedEvent(fooID, tenantID, func(c *test_pb.FooEventType_Created) { - c.Field = fmt.Sprintf("foo %d at %s", ii, tt.Format(time.RFC3339Nano)) - c.Weight = ptr.To(10 + int64(ii)) - }) - - stateOut, err := sm.Transition(ctx, event) - if err != nil { - t.Fatal(err.Error()) - } - t.Equal(test_pb.FooStatus_ACTIVE, stateOut.Status) - t.Equal(tenantID, *stateOut.Keys.TenantId) - } - }) - - var pageResp *list_j5pb.PageResponse - - ss.Step("List Page 1", func(ctx context.Context, t flowtest.Asserter) { - res := uu.ListFoo(t, &test_spb.FooListRequest{}) - - if len(res.Foo) != 20 { - t.Fatalf("expected 20 states, got %d", len(res.Foo)) - } - - for ii, state := range res.Foo { - t.Logf("%d: %s", ii, state.Data.Field) - } - - pageResp = res.Page - - if pageResp.GetNextToken() == "" { - t.Fatalf("NextToken should not be empty") - } - if pageResp.NextToken == nil { - t.Fatalf("Should not be the final page") - } - }) - - ss.Step("List Page 2", func(ctx context.Context, t flowtest.Asserter) { - req := &test_spb.FooListRequest{ - Page: &list_j5pb.PageRequest{ - Token: pageResp.NextToken, - }, - } - res := &test_spb.FooListResponse{} - - query, err := queryer.MainLister.BuildQuery(ctx, req.ProtoReflect(), res.ProtoReflect()) - if err != nil { - t.Fatal(err.Error()) - } - printQuery(t, query) - - err = queryer.List(ctx, uu.DB, req, res) - if err != nil { - t.Fatal(err.Error()) - } - - for ii, state := range res.Foo { - t.Logf("%d: %s", ii, state.Data.Field) - } - - if len(res.Foo) != 10 { - t.Fatalf("expected 10 states, got %d", len(res.Foo)) - } - }) -} - -func TestEventPagination(t *testing.T) { - // Foo event default sort is deeply nested. This tests that the nested filter - // works on pagination - ss, uu := NewFooUniverse(t) - sm := uu.SM - db := uu.DB - defer ss.RunSteps(t) - - queryer, err := test_spb.NewFooPSMQuerySet(test_spb.DefaultFooPSMQuerySpec(sm.StateTableSpec()), psm.StateQueryOptions{}) - if err != nil { - t.Fatal(err.Error()) - } - - fooID := uuid.NewString() - ss.Step("CreateEvents", func(ctx context.Context, t flowtest.Asserter) { - tenantID := uuid.NewString() - - restore := silenceLogger() - defer restore() - - event := newFooCreatedEvent(fooID, tenantID) - _, err := sm.Transition(ctx, event) - if err != nil { - t.Fatal(err.Error()) - } - - for ii := range 30 { - tt := time.Now() - event := newFooUpdatedEvent(fooID, tenantID, func(u *test_pb.FooEventType_Updated) { - u.Field = fmt.Sprintf("foo %d at %s", ii, tt.Format(time.RFC3339Nano)) - u.Weight = ptr.To(11 + int64(ii)) - }) - _, err := sm.Transition(ctx, event) - if err != nil { - t.Fatal(err.Error()) - } - } - }) - - var pageResp *list_j5pb.PageResponse - - ss.Step("List Page 1", func(ctx context.Context, t flowtest.Asserter) { - - req := &test_spb.FooEventsRequest{ - FooId: fooID, - } - res := &test_spb.FooEventsResponse{} - - err = queryer.ListEvents(ctx, db, req, res) - if err != nil { - t.Fatal(err.Error()) - } - - if len(res.Events) != 20 { - t.Fatalf("expected 20 states, got %d", len(res.Events)) - } - - for ii, evt := range res.Events { - tsv := evt.Metadata.Timestamp.AsTime().Round(time.Microsecond).UnixMicro() - switch et := evt.Event.Type.(type) { - case *test_pb.FooEventType_Created_: - t.Logf("%03d: Create %s - %d", ii, et.Created.Field, tsv) - case *test_pb.FooEventType_Updated_: - t.Logf("%03d: Update %s - %d", ii, et.Updated.Field, tsv) - default: - t.Fatalf("unexpected event type %T", et) - } - } - - pageResp = res.Page - - if pageResp.GetNextToken() == "" { - t.Fatalf("NextToken should not be empty") - } - if pageResp.NextToken == nil { - t.Fatalf("Should not be the final page") - } - - rowBytes, err := base64.StdEncoding.DecodeString(*pageResp.NextToken) - if err != nil { - t.Fatal(err.Error()) - } - - msg := &test_pb.FooEvent{} - if err := proto.Unmarshal(rowBytes, msg); err != nil { - t.Fatal(err.Error()) - } - - t.Log(protojson.Format(msg)) - t.Logf("Token entry, TS: %d, ID: %s", msg.Metadata.Timestamp.AsTime().Round(time.Microsecond).UnixMicro(), msg.Metadata.EventId) - - }) - - ss.Step("List Page 2", func(ctx context.Context, t flowtest.Asserter) { - - req := &test_spb.FooEventsRequest{ - Page: &list_j5pb.PageRequest{ - Token: pageResp.NextToken, - }, - FooId: fooID, - } - res := &test_spb.FooEventsResponse{} - - query, err := queryer.EventLister.BuildQuery(ctx, req.ProtoReflect(), res.ProtoReflect()) - if err != nil { - t.Fatal(err.Error()) - } - printQuery(t, query) - - err = queryer.ListEvents(ctx, db, req, res) - if err != nil { - t.Fatal(err.Error()) - } - - for ii, evt := range res.Events { - switch et := evt.Event.Type.(type) { - case *test_pb.FooEventType_Created_: - t.Logf("%d: Create %s", ii, et.Created.Field) - case *test_pb.FooEventType_Updated_: - t.Logf("%d: Update %s", ii, et.Updated.Field) - default: - t.Fatalf("unexpected event type %T", et) - } - } - - if len(res.Events) != 11 { - t.Fatalf("expected 10 states, got %d", len(res.Events)) - } - }) -} - -func TestPageSize(t *testing.T) { - - ss, uu := NewFooUniverse(t) - sm := uu.SM - db := uu.DB - queryer := uu.Query - defer ss.RunSteps(t) - - ss.Step("Create", func(ctx context.Context, t flowtest.Asserter) { - tenantID := uuid.NewString() - - restore := silenceLogger() - defer restore() - - for ii := range 30 { - tt := time.Now() - fooID := uuid.NewString() - - event1 := newFooEvent(&test_pb.FooKeys{ - TenantId: &tenantID, - FooId: fooID, - MetaTenantId: metaTenant, - }, &test_pb.FooEventType_Created{ - Name: "foo", - Field: fmt.Sprintf("foo %d at %s", ii, tt.Format(time.RFC3339Nano)), - Weight: ptr.To(10 + int64(ii)), - }, - ) - stateOut, err := sm.Transition(ctx, event1) - if err != nil { - t.Fatal(err.Error()) - } - t.Equal(test_pb.FooStatus_ACTIVE, stateOut.Status) - t.Equal(tenantID, *stateOut.Keys.TenantId) - } - }) - - var pageResp *list_j5pb.PageResponse - - ss.Step("List Page (default)", func(ctx context.Context, t flowtest.Asserter) { - req := &test_spb.FooListRequest{} - res := &test_spb.FooListResponse{} - - err := queryer.List(ctx, db, req, res) - if err != nil { - t.Fatal(err.Error()) - } - - if len(res.Foo) != int(20) { - t.Fatalf("expected %d states, got %d", 20, len(res.Foo)) - } - - for ii, state := range res.Foo { - t.Logf("%d: %s", ii, state.Data.Field) - } - - pageResp = res.Page - - if pageResp.GetNextToken() == "" { - t.Fatalf("NextToken should not be empty") - } - if pageResp.NextToken == nil { - t.Fatalf("Should not be the final page") - } - }) - - ss.Step("List Page", func(ctx context.Context, t flowtest.Asserter) { - pageSize := int64(5) - req := &test_spb.FooListRequest{ - Page: &list_j5pb.PageRequest{ - PageSize: &pageSize, - }, - } - res := &test_spb.FooListResponse{} - - err := queryer.List(ctx, db, req, res) - if err != nil { - t.Fatal(err.Error()) - } - - if len(res.Foo) != int(pageSize) { - t.Fatalf("expected %d states, got %d", pageSize, len(res.Foo)) - } - - for ii, state := range res.Foo { - t.Logf("%d: %s", ii, state.Data.Field) - } - - pageResp = res.Page - - if pageResp.GetNextToken() == "" { - t.Fatalf("NextToken should not be empty") - } - if pageResp.NextToken == nil { - t.Fatalf("Should not be the final page") - } - }) - - ss.Step("List Page (exceeding)", func(ctx context.Context, t flowtest.Asserter) { - pageSize := int64(50) - req := &test_spb.FooListRequest{ - Page: &list_j5pb.PageRequest{ - PageSize: &pageSize, - }, - } - res := &test_spb.FooListResponse{} - - err := queryer.List(ctx, db, req, res) - if err == nil { - t.Fatal("expected error") - } - }) -} diff --git a/internal/integration/query_test/search_test.go b/internal/integration/query_test/search_test.go deleted file mode 100644 index da63b3e..0000000 --- a/internal/integration/query_test/search_test.go +++ /dev/null @@ -1,188 +0,0 @@ -package integration - -import ( - "context" - "testing" - - "github.com/google/uuid" - "github.com/pentops/flowtest" - "github.com/pentops/j5/gen/j5/list/v1/list_j5pb" - "github.com/pentops/protostate/internal/testproto/gen/test/v1/test_spb" - "google.golang.org/protobuf/proto" -) - -func TestDynamicSearching(t *testing.T) { - ss, uu := NewFooUniverse(t) - sm := uu.SM - db := uu.DB - queryer := uu.Query - defer ss.RunSteps(t) - - tenants := []string{uuid.NewString()} - setupFooListableData(ss, sm, tenants, 30) - - t.Run("Simple Search Field", func(t *testing.T) { - ss.Step("List Page", func(ctx context.Context, t flowtest.Asserter) { - req := &test_spb.FooListRequest{ - Page: &list_j5pb.PageRequest{ - PageSize: proto.Int64(5), - }, - Query: &list_j5pb.QueryRequest{ - Searches: []*list_j5pb.Search{ - { - Field: "data.field", - Value: "weighted 30", - }, - }, - }, - } - res := &test_spb.FooListResponse{} - - err := queryer.List(ctx, db, req, res) - if err != nil { - t.Fatal(err.Error()) - } - - if len(res.Foo) != 1 { - t.Fatalf("expected %d states, got %d", 1, len(res.Foo)) - } - - for ii, state := range res.Foo { - t.Logf("%d: %s", ii, state.Data.Field) - } - - for ii, state := range res.Foo { - if state.Data.Characteristics.Weight != int64(30-ii) { - t.Fatalf("expected weight %d, got %d", 30-ii, state.Data.Characteristics.Weight) - } - } - - if res.Page != nil { - t.Fatalf("page response should be empty") - } - }) - }) - - /* - t.Run("Complex Search Field", func(t *testing.T) { - nextToken := "" - ss.Step("List Page 1", func(ctx context.Context, t flowtest.Asserter) { - req := &test_spb.FooListRequest{ - Page: &list_j5pb.PageRequest{ - PageSize: proto.Int64(5), - }, - Query: &list_j5pb.QueryRequest{ - Filters: []*list_j5pb.Filter{ - { - Type: &list_j5pb.Filter_Field{ - Field: &list_j5pb.Field{ - Name: "profiles.place", - Type: &list_j5pb.Field_Range{ - Range: &list_j5pb.Range{ - Min: "15", - Max: "21", - }, - }, - }, - }, - }, - }, - }, - } - res := &test_spb.FooListResponse{} - - err = queryer.List(ctx, db, req, res) - if err != nil { - t.Fatal(err.Error()) - } - - for ii, state := range res.Foo { - t.Logf("%d: %s", ii, state.Profiles) - } - - if len(res.Foo) != 5 { - t.Fatalf("expected %d states, got %d", 5, len(res.Foo)) - } - - for _, state := range res.Foo { - matched := false - for _, profile := range state.Profiles { - if profile.Place >= 17 && profile.Place <= 21 { - matched = true - break - } - } - - if !matched { - t.Fatalf("expected at least one profile to match the filter") - } - } - - pageResp := res.Page - - if pageResp.GetNextToken() == "" { - t.Fatalf("NextToken should not be empty") - } - if pageResp.NextToken == nil { - t.Fatalf("Should not be the final page") - } - - nextToken = pageResp.GetNextToken() - }) - - ss.Step("List Page 2", func(ctx context.Context, t flowtest.Asserter) { - req := &test_spb.FooListRequest{ - Page: &list_j5pb.PageRequest{ - PageSize: proto.Int64(5), - Token: &nextToken, - }, - Query: &list_j5pb.QueryRequest{ - Filters: []*list_j5pb.Filter{ - { - Type: &list_j5pb.Filter_Field{ - Field: &list_j5pb.Field{ - Name: "profiles.place", - Type: &list_j5pb.Field_Range{ - Range: &list_j5pb.Range{ - Min: "15", - Max: "21", - }, - }, - }, - }, - }, - }, - }, - } - res := &test_spb.FooListResponse{} - - err = queryer.List(ctx, db, req, res) - if err != nil { - t.Fatal(err.Error()) - } - - for ii, state := range res.Foo { - t.Logf("%d: %s", ii, state.Profiles) - } - - if len(res.Foo) != 2 { - t.Fatalf("expected %d states, got %d", 2, len(res.Foo)) - } - - for _, state := range res.Foo { - matched := false - for _, profile := range state.Profiles { - if profile.Place >= 15 && profile.Place <= 16 { - matched = true - break - } - } - - if !matched { - t.Fatalf("expected at least one profile to match the filter") - } - } - }) - }) - */ -} diff --git a/internal/integration/query_test/setup.go b/internal/integration/query_test/setup.go deleted file mode 100644 index 01486d4..0000000 --- a/internal/integration/query_test/setup.go +++ /dev/null @@ -1,138 +0,0 @@ -package integration - -import ( - "fmt" - - "github.com/google/uuid" - "github.com/pentops/j5/gen/j5/state/v1/psm_j5pb" - "github.com/pentops/protostate/internal/testproto/gen/test/v1/test_pb" - "k8s.io/utils/ptr" -) - -func NewFooStateMachine() (*test_pb.FooPSM, error) { - sm, err := test_pb.FooPSMBuilder().BuildStateMachine() - if err != nil { - return nil, err - } - - sm.From(0). - OnEvent(test_pb.FooPSMEventCreated). - SetStatus(test_pb.FooStatus_ACTIVE). - Mutate(test_pb.FooPSMMutation(func( - data *test_pb.FooData, - event *test_pb.FooEventType_Created, - ) error { - data.Name = event.Name - data.Field = event.Field - data.Description = event.Description - data.Characteristics = &test_pb.FooCharacteristics{ - Weight: event.GetWeight(), - Height: event.GetHeight(), - Length: event.GetLength(), - } - data.Profiles = event.Profiles - return nil - })) - - // Testing Mutate() without OnEvent, the callback implies the event type. - sm.From(test_pb.FooStatus_ACTIVE). - Mutate(test_pb.FooPSMMutation(func( - data *test_pb.FooData, - event *test_pb.FooEventType_Updated, - ) error { - data.Field = event.Field - data.Name = event.Name - data.Description = event.Description - data.Characteristics = &test_pb.FooCharacteristics{ - Weight: event.GetWeight(), - Height: event.GetHeight(), - Length: event.GetLength(), - } - - return nil - })) - - sm.From(test_pb.FooStatus_ACTIVE). - OnEvent(test_pb.FooPSMEventDeleted). - SetStatus(test_pb.FooStatus_DELETED) - - return sm, nil - -} - -var metaTenant = uuid.NewString() - -func newFooCreatedEvent(fooID, tenantID string, mod ...func(c *test_pb.FooEventType_Created)) *test_pb.FooPSMEventSpec { - weight := int64(10) - created := &test_pb.FooEventType_Created{ - Name: "foo", - Field: fmt.Sprintf("weight: %d", weight), - Description: ptr.To("creation event for foo: " + fooID), - Weight: &weight, - } - - for _, m := range mod { - m(created) - } - - return newFooEvent(&test_pb.FooKeys{ - FooId: fooID, - TenantId: &tenantID, - MetaTenantId: metaTenant, - }, created) -} - -func newFooUpdatedEvent(fooID, tenantID string, mod ...func(u *test_pb.FooEventType_Updated)) *test_pb.FooPSMEventSpec { - weight := int64(20) - updated := &test_pb.FooEventType_Updated{ - Name: "foo", - Field: fmt.Sprintf("weight: %d", weight), - Description: ptr.To("update event for foo: " + fooID), - Weight: &weight, - } - - for _, m := range mod { - m(updated) - } - - return newFooEvent(&test_pb.FooKeys{ - FooId: fooID, - TenantId: &tenantID, - MetaTenantId: metaTenant, - }, updated) - -} -func newFooDeletedEvent(fooID, tenantID string, mod ...func(u *test_pb.FooEventType_Deleted)) *test_pb.FooPSMEventSpec { - deleted := &test_pb.FooEventType_Deleted{} - - for _, m := range mod { - m(deleted) - } - - return newFooEvent(&test_pb.FooKeys{ - FooId: fooID, - TenantId: &tenantID, - MetaTenantId: metaTenant, - }, deleted) -} - -func newFooEvent(keys *test_pb.FooKeys, et test_pb.FooPSMEvent) *test_pb.FooPSMEventSpec { - - if keys.MetaTenantId == "" { - panic("metaTenantId is required") - } - e := &test_pb.FooPSMEventSpec{ - Keys: keys, - Cause: &psm_j5pb.Cause{ - Type: &psm_j5pb.Cause_ExternalEvent{ - ExternalEvent: &psm_j5pb.ExternalEventCause{ - SystemName: "a", - EventName: "b", - }, - }, - }, - Event: et, - } - - return e -} diff --git a/internal/integration/query_test/sort_test.go b/internal/integration/query_test/sort_test.go deleted file mode 100644 index bfbac19..0000000 --- a/internal/integration/query_test/sort_test.go +++ /dev/null @@ -1,630 +0,0 @@ -package integration - -import ( - "context" - "testing" - - "github.com/google/uuid" - "github.com/pentops/flowtest" - "github.com/pentops/j5/gen/j5/auth/v1/auth_j5pb" - "github.com/pentops/j5/gen/j5/list/v1/list_j5pb" - "github.com/pentops/protostate/internal/testproto/gen/test/v1/test_spb" - "google.golang.org/protobuf/proto" -) - -func TestSortingWithAuthScope(t *testing.T) { - - ss, uu := NewFooUniverse(t, WithStateQueryOptions(newTokenQueryStateOption())) - sm := uu.SM - db := uu.DB - queryer := uu.Query - var err error - defer ss.RunSteps(t) - - tenantID1 := uuid.NewString() - tenantID2 := uuid.NewString() - - tenants := []string{tenantID1, tenantID2} - setupFooListableData(ss, sm, tenants, 10) - - tkn := &token{ - claim: &auth_j5pb.Claim{ - TenantType: "tenant", - TenantId: tenantID1, - }, - } - - nextToken := "" - ss.Step("List Page 1", func(ctx context.Context, t flowtest.Asserter) { - ctx = tkn.WithToken(ctx) - - req := &test_spb.FooListRequest{ - Page: &list_j5pb.PageRequest{ - PageSize: proto.Int64(5), - }, - Query: &list_j5pb.QueryRequest{ - Sorts: []*list_j5pb.Sort{ - {Field: "data.characteristics.weight"}, - }, - }, - } - res := &test_spb.FooListResponse{} - - err = queryer.List(ctx, db, req, res) - if err != nil { - t.Fatal(err.Error()) - } - - if len(res.Foo) != int(5) { - t.Fatalf("expected %d states, got %d", 5, len(res.Foo)) - } - - for ii, state := range res.Foo { - t.Logf("%d: %s", ii, state.Data.Field) - } - - for ii, state := range res.Foo { - if state.Data.Characteristics.Weight != int64(10+ii) { - t.Fatalf("expected weight %d, got %d", 10+ii, state.Data.Characteristics.Weight) - } - - if *state.Keys.TenantId != tenantID1 { - t.Fatalf("expected tenant ID %s, got %s", tenantID1, state.Keys.TenantId) - } - } - - pageResp := res.Page - - if pageResp.GetNextToken() == "" { - t.Fatalf("NextToken should not be empty") - } - if pageResp.NextToken == nil { - t.Fatalf("Should not be the final page") - } - - nextToken = pageResp.GetNextToken() - }) - - ss.Step("List Page 2", func(ctx context.Context, t flowtest.Asserter) { - ctx = tkn.WithToken(ctx) - - req := &test_spb.FooListRequest{ - Page: &list_j5pb.PageRequest{ - PageSize: proto.Int64(5), - Token: &nextToken, - }, - Query: &list_j5pb.QueryRequest{ - Sorts: []*list_j5pb.Sort{ - {Field: "data.characteristics.weight"}, - }, - }, - } - res := &test_spb.FooListResponse{} - - err = queryer.List(ctx, db, req, res) - if err != nil { - t.Fatal(err.Error()) - } - - if len(res.Foo) != int(5) { - t.Fatalf("expected %d states, got %d", 5, len(res.Foo)) - } - - for ii, state := range res.Foo { - t.Logf("%d: %s", ii, state.Data.Field) - } - - for ii, state := range res.Foo { - if state.Data.Characteristics.Weight != int64(15+ii) { - t.Fatalf("expected weight %d, got %d", 15+ii, state.Data.Characteristics.Weight) - } - - if *state.Keys.TenantId != tenantID1 { - t.Fatalf("expected tenant ID %s, got %s", tenantID1, state.Keys.TenantId) - } - } - - if res.Page != nil { - t.Fatalf("page response should be empty") - } - }) -} - -func TestSortingWithAuthNoScope(t *testing.T) { - ss, uu := NewFooUniverse(t, WithStateQueryOptions(newTokenQueryStateOption())) - sm := uu.SM - db := uu.DB - queryer := uu.Query - var err error - defer ss.RunSteps(t) - - tenants := []string{uuid.NewString(), uuid.NewString()} - setupFooListableData(ss, sm, tenants, 30) - - tkn := &token{ - claim: &auth_j5pb.Claim{ - TenantType: "meta_tenant", - TenantId: metaTenant, - }, - } - - nextToken := "" - ss.Step("List Page 1", func(ctx context.Context, t flowtest.Asserter) { - ctx = tkn.WithToken(ctx) - - req := &test_spb.FooListRequest{ - Page: &list_j5pb.PageRequest{ - PageSize: proto.Int64(5), - }, - Query: &list_j5pb.QueryRequest{ - Sorts: []*list_j5pb.Sort{ - {Field: "data.characteristics.weight"}, - }, - }, - } - res := &test_spb.FooListResponse{} - - err = queryer.List(ctx, db, req, res) - if err != nil { - t.Fatal(err.Error()) - } - - if len(res.Foo) != int(5) { - t.Fatalf("expected %d states, got %d", 5, len(res.Foo)) - } - - for ii, state := range res.Foo { - t.Logf("%d: %s", ii, state.Data.Field) - } - - for ii, state := range res.Foo { - if state.Data.Characteristics.Weight != int64(10+ii) { - t.Fatalf("expected weight %d, got %d", 10+ii, state.Data.Characteristics.Weight) - } - } - - pageResp := res.Page - - if pageResp.GetNextToken() == "" { - t.Fatalf("NextToken should not be empty") - } - if pageResp.NextToken == nil { - t.Fatalf("Should not be the final page") - } - - nextToken = pageResp.GetNextToken() - }) - - ss.Step("List Page 2", func(ctx context.Context, t flowtest.Asserter) { - ctx = tkn.WithToken(ctx) - - req := &test_spb.FooListRequest{ - Page: &list_j5pb.PageRequest{ - PageSize: proto.Int64(5), - Token: &nextToken, - }, - Query: &list_j5pb.QueryRequest{ - Sorts: []*list_j5pb.Sort{ - {Field: "data.characteristics.weight"}, - }, - }, - } - res := &test_spb.FooListResponse{} - - err = queryer.List(ctx, db, req, res) - if err != nil { - t.Fatal(err.Error()) - } - - if len(res.Foo) != int(5) { - t.Fatalf("expected %d states, got %d", 5, len(res.Foo)) - } - - for ii, state := range res.Foo { - t.Logf("%d: %s", ii, state.Data.Field) - } - - for ii, state := range res.Foo { - if state.Data.Characteristics.Weight != int64(15+ii) { - t.Fatalf("expected weight %d, got %d", 15+ii, state.Data.Characteristics.Weight) - } - } - - pageResp := res.Page - - if pageResp.GetNextToken() == "" { - t.Fatalf("NextToken should not be empty") - } - if pageResp.NextToken == nil { - t.Fatalf("Should not be the final page") - } - }) -} - -func TestDynamicSorting(t *testing.T) { - ss, uu := NewFooUniverse(t) - sm := uu.SM - db := uu.DB - queryer := uu.Query - var err error - defer ss.RunSteps(t) - - tenants := []string{uuid.NewString()} - setupFooListableData(ss, sm, tenants, 30) - - t.Run("Top Level Field", func(t *testing.T) { - nextToken := "" - ss.Step("List Page 1", func(ctx context.Context, t flowtest.Asserter) { - req := &test_spb.FooListRequest{ - Page: &list_j5pb.PageRequest{ - PageSize: proto.Int64(5), - }, - Query: &list_j5pb.QueryRequest{ - Sorts: []*list_j5pb.Sort{ - {Field: "metadata.createdAt"}, - }, - }, - } - res := &test_spb.FooListResponse{} - - err = queryer.List(ctx, db, req, res) - if err != nil { - t.Fatal(err.Error()) - } - - if len(res.Foo) != int(5) { - t.Fatalf("expected %d states, got %d", 5, len(res.Foo)) - } - - for ii, state := range res.Foo { - t.Logf("%d: %s", ii, state.Data.Field) - } - - for ii, state := range res.Foo { - if state.Data.Characteristics.Weight != int64(10+ii) { - t.Fatalf("expected weight %d, got %d", 10+ii, state.Data.Characteristics.Weight) - } - } - - pageResp := res.Page - - if pageResp.GetNextToken() == "" { - t.Fatalf("NextToken should not be empty") - } - if pageResp.NextToken == nil { - t.Fatalf("Should not be the final page") - } - - nextToken = pageResp.GetNextToken() - }) - - ss.Step("List Page 2", func(ctx context.Context, t flowtest.Asserter) { - req := &test_spb.FooListRequest{ - Page: &list_j5pb.PageRequest{ - PageSize: proto.Int64(5), - Token: &nextToken, - }, - Query: &list_j5pb.QueryRequest{ - Sorts: []*list_j5pb.Sort{ - {Field: "metadata.createdAt"}, - }, - }, - } - res := &test_spb.FooListResponse{} - - err = queryer.List(ctx, db, req, res) - if err != nil { - t.Fatal(err.Error()) - } - - if len(res.Foo) != int(5) { - t.Fatalf("expected %d states, got %d", 5, len(res.Foo)) - } - - for ii, state := range res.Foo { - t.Logf("%d: %s", ii, state.Data.Field) - } - - for ii, state := range res.Foo { - if state.Data.Characteristics.Weight != int64(15+ii) { - t.Fatalf("expected weight %d, got %d", 15+ii, state.Data.Characteristics.Weight) - } - } - - pageResp := res.Page - - if pageResp.GetNextToken() == "" { - t.Fatalf("NextToken should not be empty") - } - if pageResp.NextToken == nil { - t.Fatalf("Should not be the final page") - } - }) - }) - - t.Run("Nested Field", func(t *testing.T) { - nextToken := "" - ss.Step("List Page 1", func(ctx context.Context, t flowtest.Asserter) { - req := &test_spb.FooListRequest{ - Page: &list_j5pb.PageRequest{ - PageSize: proto.Int64(5), - }, - Query: &list_j5pb.QueryRequest{ - Sorts: []*list_j5pb.Sort{ - {Field: "data.characteristics.weight"}, - }, - }, - } - res := &test_spb.FooListResponse{} - - err = queryer.List(ctx, db, req, res) - if err != nil { - t.Fatal(err.Error()) - } - - if len(res.Foo) != int(5) { - t.Fatalf("expected %d states, got %d", 5, len(res.Foo)) - } - - for ii, state := range res.Foo { - t.Logf("%d: %s", ii, state.Data.Field) - } - - for ii, state := range res.Foo { - if state.Data.Characteristics.Weight != int64(10+ii) { - t.Fatalf("expected weight %d, got %d", 10+ii, state.Data.Characteristics.Weight) - } - } - - pageResp := res.Page - - if pageResp.GetNextToken() == "" { - t.Fatalf("NextToken should not be empty") - } - if pageResp.NextToken == nil { - t.Fatalf("Should not be the final page") - } - - nextToken = pageResp.GetNextToken() - }) - - ss.Step("List Page 2", func(ctx context.Context, t flowtest.Asserter) { - req := &test_spb.FooListRequest{ - Page: &list_j5pb.PageRequest{ - PageSize: proto.Int64(5), - Token: &nextToken, - }, - Query: &list_j5pb.QueryRequest{ - Sorts: []*list_j5pb.Sort{ - {Field: "data.characteristics.weight"}, - }, - }, - } - res := &test_spb.FooListResponse{} - - err = queryer.List(ctx, db, req, res) - if err != nil { - t.Fatal(err.Error()) - } - - if len(res.Foo) != int(5) { - t.Fatalf("expected %d states, got %d", 5, len(res.Foo)) - } - - for ii, state := range res.Foo { - t.Logf("%d: %s", ii, state.Data.Field) - } - - for ii, state := range res.Foo { - if state.Data.Characteristics.Weight != int64(15+ii) { - t.Fatalf("expected weight %d, got %d", 15+ii, state.Data.Characteristics.Weight) - } - } - - pageResp := res.Page - - if pageResp.GetNextToken() == "" { - t.Fatalf("NextToken should not be empty") - } - if pageResp.NextToken == nil { - t.Fatalf("Should not be the final page") - } - }) - }) - - t.Run("Multiple Nested Fields", func(t *testing.T) { - nextToken := "" - ss.Step("List Page", func(ctx context.Context, t flowtest.Asserter) { - req := &test_spb.FooListRequest{ - Page: &list_j5pb.PageRequest{ - PageSize: proto.Int64(5), - }, - Query: &list_j5pb.QueryRequest{ - Sorts: []*list_j5pb.Sort{ - {Field: "data.characteristics.length"}, - {Field: "data.characteristics.weight"}, - }, - }, - } - res := &test_spb.FooListResponse{} - - err = queryer.List(ctx, db, req, res) - if err != nil { - t.Fatal(err.Error()) - } - - if len(res.Foo) != int(5) { - t.Fatalf("expected %d states, got %d", 5, len(res.Foo)) - } - - for ii, state := range res.Foo { - t.Logf("%d: %s", ii, state.Data.Field) - } - - if res.Foo[0].Data.Characteristics.Weight != int64(10) { - t.Fatalf("expected list to start with weight %d, got %d", 10, res.Foo[0].Data.Characteristics.Weight) - } - - for _, state := range res.Foo { - if state.Data.Characteristics.Weight%2 != 0 { - t.Fatalf("expected even number weight, got %d", state.Data.Characteristics.Weight) - } - } - - pageResp := res.Page - - if pageResp.GetNextToken() == "" { - t.Fatalf("NextToken should not be empty") - } - if pageResp.NextToken == nil { - t.Fatalf("Should not be the final page") - } - - nextToken = pageResp.GetNextToken() - }) - - ss.Step("List Page 2", func(ctx context.Context, t flowtest.Asserter) { - req := &test_spb.FooListRequest{ - Page: &list_j5pb.PageRequest{ - PageSize: proto.Int64(5), - Token: &nextToken, - }, - Query: &list_j5pb.QueryRequest{ - Sorts: []*list_j5pb.Sort{ - {Field: "data.characteristics.length"}, - {Field: "data.characteristics.weight"}, - }, - }, - } - res := &test_spb.FooListResponse{} - - err = queryer.List(ctx, db, req, res) - if err != nil { - t.Fatal(err.Error()) - } - - if len(res.Foo) != int(5) { - t.Fatalf("expected %d states, got %d", 5, len(res.Foo)) - } - - for ii, state := range res.Foo { - t.Logf("%d: %s", ii, state.Data.Field) - } - - if res.Foo[0].Data.Characteristics.Weight != int64(20) { - t.Fatalf("expected list to start with weight %d, got %d", 20, res.Foo[0].Data.Characteristics.Weight) - } - - for _, state := range res.Foo { - if state.Data.Characteristics.Weight%2 != 0 { - t.Fatalf("expected even number weight, got %d", state.Data.Characteristics.Weight) - } - } - - pageResp := res.Page - - if pageResp.GetNextToken() == "" { - t.Fatalf("NextToken should not be empty") - } - if pageResp.NextToken == nil { - t.Fatalf("Should not be the final page") - } - }) - }) - - t.Run("Descending", func(t *testing.T) { - nextToken := "" - ss.Step("List Page", func(ctx context.Context, t flowtest.Asserter) { - req := &test_spb.FooListRequest{ - Page: &list_j5pb.PageRequest{ - PageSize: proto.Int64(5), - }, - Query: &list_j5pb.QueryRequest{ - Sorts: []*list_j5pb.Sort{ - { - Field: "data.characteristics.weight", - Descending: true, - }, - }, - }, - } - res := &test_spb.FooListResponse{} - - err = queryer.List(ctx, db, req, res) - if err != nil { - t.Fatal(err.Error()) - } - - if len(res.Foo) != int(5) { - t.Fatalf("expected %d states, got %d", 5, len(res.Foo)) - } - - for ii, state := range res.Foo { - t.Logf("%d: %s", ii, state.Data.Field) - } - - for ii, state := range res.Foo { - if state.Data.Characteristics.Weight != int64(39-ii) { - t.Fatalf("expected weight %d, got %d", 39-ii, state.Data.Characteristics.Weight) - } - } - - pageResp := res.Page - - if pageResp.GetNextToken() == "" { - t.Fatalf("NextToken should not be empty") - } - if pageResp.NextToken == nil { - t.Fatalf("Should not be the final page") - } - - nextToken = pageResp.GetNextToken() - }) - - ss.Step("List Page 2", func(ctx context.Context, t flowtest.Asserter) { - req := &test_spb.FooListRequest{ - Page: &list_j5pb.PageRequest{ - PageSize: proto.Int64(5), - Token: &nextToken, - }, - Query: &list_j5pb.QueryRequest{ - Sorts: []*list_j5pb.Sort{ - { - Field: "data.characteristics.weight", - Descending: true, - }, - }, - }, - } - res := &test_spb.FooListResponse{} - - err = queryer.List(ctx, db, req, res) - if err != nil { - t.Fatal(err.Error()) - } - - if len(res.Foo) != int(5) { - t.Fatalf("expected %d states, got %d", 5, len(res.Foo)) - } - - for ii, state := range res.Foo { - t.Logf("%d: %s", ii, state.Data.Field) - } - - for ii, state := range res.Foo { - if state.Data.Characteristics.Weight != int64(34-ii) { - t.Fatalf("expected weight %d, got %d", 34-ii, state.Data.Characteristics.Weight) - } - } - - pageResp := res.Page - - if pageResp.GetNextToken() == "" { - t.Fatalf("NextToken should not be empty") - } - if pageResp.NextToken == nil { - t.Fatalf("Should not be the final page") - } - }) - }) -} diff --git a/internal/integration/query_test/token.go b/internal/integration/query_test/token.go deleted file mode 100644 index ea9e410..0000000 --- a/internal/integration/query_test/token.go +++ /dev/null @@ -1,53 +0,0 @@ -package integration - -import ( - "context" - "fmt" - - "github.com/pentops/j5/gen/j5/auth/v1/auth_j5pb" - "github.com/pentops/protostate/pquery" - "github.com/pentops/protostate/psm" -) - -type tokenCtxKey struct{} - -type token struct { - claim *auth_j5pb.Claim -} - -func (t *token) WithToken(ctx context.Context) context.Context { - return context.WithValue(ctx, tokenCtxKey{}, t) -} - -func TokenFromCtx(ctx context.Context) (*token, error) { - token, exists := ctx.Value(tokenCtxKey{}).(*token) - if !exists { - return nil, fmt.Errorf("no token found") - } - - return token, nil -} - -func newTokenQueryStateOption() psm.StateQueryOptions { - fieldMap := map[string]string{ - "tenant": "tenant_id", - "meta_tenant": "meta_tenant_id", - } - return psm.StateQueryOptions{ - Auth: pquery.AuthProviderFunc(func(ctx context.Context) (map[string]string, error) { - token, err := TokenFromCtx(ctx) - if err != nil { - return nil, err - } - - tt, ok := fieldMap[token.claim.TenantType] - if !ok { - return nil, fmt.Errorf("no field mapping for tenant type %s", token.claim.TenantType) - } - - return map[string]string{ - tt: token.claim.TenantId, - }, nil - }), - } -} diff --git a/internal/integration/query_test/universe.go b/internal/integration/query_test/universe.go deleted file mode 100644 index 09bcc96..0000000 --- a/internal/integration/query_test/universe.go +++ /dev/null @@ -1,106 +0,0 @@ -package integration - -import ( - "context" - "testing" - - "github.com/pentops/flowtest" - "github.com/pentops/pgtest.go/pgtest" - "github.com/pentops/protostate/internal/pgstore/pgmigrate" - "github.com/pentops/protostate/internal/testproto/gen/test/v1/test_pb" - "github.com/pentops/protostate/internal/testproto/gen/test/v1/test_spb" - "github.com/pentops/protostate/pquery" - "github.com/pentops/protostate/psm" - "github.com/pentops/sqrlx.go/sqrlx" -) - -type Universe struct { - SM *test_pb.FooPSMDB - Query *test_spb.FooPSMQuerySet - DB sqrlx.Transactor -} - -type universeSpec struct { - opts psm.StateQueryOptions -} - -type universeOption func(*universeSpec) - -func WithStateQueryOptions(opts psm.StateQueryOptions) universeOption { - return func(us *universeSpec) { - us.opts = opts - } -} - -func NewFooUniverse(t *testing.T, opts ...universeOption) (*flowtest.Stepper[*testing.T], *Universe) { - t.Helper() - - spec := &universeSpec{ - opts: psm.StateQueryOptions{}, - } - - for _, opt := range opts { - opt(spec) - } - - smR, err := NewFooStateMachine() - if err != nil { - t.Fatal(err.Error()) - } - - conn := pgtest.GetTestDB(t, pgtest.WithSchemaName("query_test")) - db := sqrlx.NewPostgres(conn) - - specs := []psm.QueryTableSpec{ - smR.StateTableSpec(), - } - - if err := pgmigrate.CreateStateMachines(context.Background(), conn, specs...); err != nil { - t.Fatal(err.Error()) - } - - if err := pgmigrate.AddIndexes(context.Background(), conn, specs...); err != nil { - t.Fatal(err.Error()) - } - - sm := smR.WithDB(db) - - ss := flowtest.NewStepper[*testing.T](t.Name()) - defer ss.RunSteps(t) - - queryer, err := test_spb.NewFooPSMQuerySet(test_spb.DefaultFooPSMQuerySpec(sm.StateTableSpec()), spec.opts) - if err != nil { - t.Fatal(err.Error()) - } - queryer.SetQueryLogger(testLogger(t)) - - return ss, &Universe{ - DB: db, - SM: sm, - Query: queryer, - } -} - -func (uu *Universe) ListFoo(t flowtest.Asserter, req *test_spb.FooListRequest) *test_spb.FooListResponse { - t.Helper() - ctx := context.Background() - - resp := &test_spb.FooListResponse{} - err := uu.Query.List(ctx, uu.DB, req, resp) - if err != nil { - t.Fatal(err.Error()) - } - - return resp -} - -func testLogger(t *testing.T) pquery.QueryLogger { - return func(query sqrlx.Sqlizer) { - queryString, args, err := query.ToSql() - if err != nil { - t.Logf("Query Error: %s", err.Error()) - return - } - t.Logf("Query %s; ARGS %#v", queryString, args) - } -} diff --git a/internal/integration/universe_test.go b/internal/integration/universe_test.go index 361cb88..6c9555c 100644 --- a/internal/integration/universe_test.go +++ b/internal/integration/universe_test.go @@ -8,10 +8,10 @@ import ( "github.com/pentops/flowtest" "github.com/pentops/log.go/log" "github.com/pentops/pgtest.go/pgtest" - "github.com/pentops/protostate/internal/pgstore/pgmigrate" "github.com/pentops/protostate/internal/testproto/gen/test/v1/test_pb" "github.com/pentops/protostate/internal/testproto/gen/test/v1/test_spb" "github.com/pentops/protostate/psm" + "github.com/pentops/protostate/psmigrate" "github.com/pentops/sqrlx.go/sqrlx" ) @@ -58,7 +58,6 @@ func setupUniverse(t flowtest.Asserter, uu *Universe) { } fooQuery, err := test_spb.NewFooPSMQuerySet(test_spb.DefaultFooPSMQuerySpec(sm.Foo.StateTableSpec()), psm.StateQueryOptions{}) - if err != nil { t.Fatal(err.Error()) } @@ -73,7 +72,7 @@ func setupUniverse(t flowtest.Asserter, uu *Universe) { uu.FooQuery = NewMiniFooController(db, fooQuery) uu.BarQuery = NewMiniBarController(db, barQuery) - if err := pgmigrate.CreateStateMachines(context.Background(), conn, + if err := psmigrate.CreateStateMachines(context.Background(), conn, sm.Foo.StateTableSpec(), sm.Bar.StateTableSpec(), ); err != nil { diff --git a/internal/pgstore/path.go b/internal/pgstore/path.go deleted file mode 100644 index 08202e5..0000000 --- a/internal/pgstore/path.go +++ /dev/null @@ -1,473 +0,0 @@ -package pgstore - -import ( - "fmt" - "strings" - - "github.com/pentops/j5/gen/j5/ext/v1/ext_j5pb" - "github.com/pentops/j5/lib/j5schema" - "google.golang.org/protobuf/proto" - "google.golang.org/protobuf/reflect/protoreflect" - "google.golang.org/protobuf/types/descriptorpb" -) - -type Table interface { - TableName() string -} - -type NestedField struct { - - // The column containing the element JSONB - RootColumn string - - // The path from the column root to the node - Path Path - - // ValueColumn contains the value of the field directly in the table in - // addition to nested within the JSONB data. - ValueColumn *string -} - -type ProtoFieldSpec struct { - // The column holding the data, either directly or JSONB from here using the - // path - ColumnName string - - // The path from the column root to the node being specified (not the path - // of the node from a 'root node' of the table) - Path ProtoPathSpec - - // The path from the nominal table root message to the node being specified - PathFromRoot ProtoPathSpec -} - -func (nf *NestedField) ProtoChild(name protoreflect.Name) (*NestedField, error) { - pathChild, err := nf.Path.Child(name) - if err != nil { - return nil, err - } - return &NestedField{ - RootColumn: nf.RootColumn, - Path: *pathChild, - }, nil -} - -func (nf *NestedField) Selector(inTable string) string { - if nf.ValueColumn != nil { - return fmt.Sprintf("%s.%s", inTable, *nf.ValueColumn) - } - if len(nf.Path.path) == 0 { - return fmt.Sprintf("%s.%s", inTable, nf.RootColumn) - } - return fmt.Sprintf("%s.%s%s", inTable, nf.RootColumn, nf.Path.JSONBArrowPath()) -} - -type pathNode struct { - name protoreflect.Name - field protoreflect.FieldDescriptor - oneof protoreflect.OneofDescriptor -} - -type Path struct { - root protoreflect.MessageDescriptor - path []pathNode - - leafField protoreflect.FieldDescriptor - leafOneof protoreflect.OneofDescriptor -} - -func (pp Path) Root() protoreflect.MessageDescriptor { - return pp.root -} - -func (pp Path) LeafField() protoreflect.FieldDescriptor { - return pp.leafField -} - -func (pp Path) LeafOneof() protoreflect.OneofDescriptor { - return pp.leafOneof -} - -// LeafOneofWrapper handles the special case where the leaf is a oneof inside of -// a oneof wrapper, then this returns the field definition of the object which -// references the wrapper. -func (pp Path) LeafOneofWrapper() protoreflect.FieldDescriptor { - if pp.leafOneof == nil { - return nil - } - parentMsg := pp.leafOneof.Parent().(protoreflect.MessageDescriptor) - if !j5schema.IsOneofWrapper(parentMsg) { - return nil - } - - // The leaf is a proto oneof in a j5 oneof wrapper. - // path[-1] should be the message - // path[-2] should be the field - fieldNode := pp.path[len(pp.path)-2] - return fieldNode.field -} - -func (pp Path) Leaf() protoreflect.Descriptor { - if pp.leafField != nil { - return pp.leafField - } - return pp.leafOneof -} - -func (pp Path) Child(name protoreflect.Name) (*Path, error) { - if pp.leafField == nil { - return nil, fmt.Errorf("child requires a message field") - } - if pp.leafField.Kind() != protoreflect.MessageKind { - return nil, fmt.Errorf("child requires a message field") - } - field := pp.leafField.Message().Fields().ByName(name) - if field == nil { - return nil, fmt.Errorf("field %s not found in message %s", name, pp.leafField.Message().FullName()) - } - return &Path{ - root: pp.root, - path: append(pp.path, pathNode{ - name: name, - field: field, - }), - }, nil -} - -// IDPath uniquely identifies the path within a specific root type context -func (pp Path) IDPath() string { - return pp.pathNodeNames() -} - -func (pp Path) DebugName() string { - return fmt.Sprintf("%s:%s", pp.root.FullName(), pp.pathNodeNames()) -} - -func (pp Path) pathNodeNames() string { - names := make([]string, 0, len(pp.path)) - for _, node := range pp.path { - names = append(names, string(node.name)) - } - return strings.Join(names, ".") -} - -func (pp *Path) JSONBArrowPath() string { - elements := make([]string, 0, len(pp.path)) - end, path := pp.path[len(pp.path)-1], pp.path[:len(pp.path)-1] - for _, part := range path { - if part.oneof != nil { - continue // Ignore the node, it isn't in the JSONB tree - } - if part.field.IsList() { - panic("list fields not supported by JSONBArrowPath()") - } - elements = append(elements, fmt.Sprintf("->'%s'", part.field.JSONName())) - } - - return fmt.Sprintf("%s->>'%s'", strings.Join(elements, ""), end.field.JSONName()) -} - -func (pp *Path) JSONPathQuery() string { - elements := make([]string, 1, len(pp.path)+1) - elements[0] = "$" // Used sometimes, not always? - for _, part := range pp.path { - if part.oneof != nil { - continue // Ignore the node, it isn't in the JSONB tree - } - if part.field == nil { - panic(fmt.Sprintf("invalid path: %v", pp.DebugName())) - } - elements = append(elements, fmt.Sprintf(".%s", part.field.JSONName())) - if part.field.IsList() { - elements = append(elements, "[*]") - } - } - - return strings.Join(elements, "") -} - -// WalkPathNodes visits every field in the message tree other than the root -// message itself, calling the callback for each. -func WalkPathNodes(rootMessage protoreflect.MessageDescriptor, callback func(Path) error) error { - root := &Path{ - root: rootMessage, - } - return root.walk(rootMessage, callback) -} - -func (pp Path) walk(msg protoreflect.MessageDescriptor, callback func(Path) error) error { - fields := msg.Fields() - // walks only fields, not oneofs. - for i := range fields.Len() { - field := fields.Get(i) - fieldPath := append(pp.path, pathNode{ - name: field.Name(), - field: field, - }) - fieldPathSpec := Path{ - root: pp.root, - path: fieldPath, - leafField: field, - } - - if err := callback(fieldPathSpec); err != nil { - return err - } - - if field.Kind() != protoreflect.MessageKind { - continue - } - if err := fieldPathSpec.walk(field.Message(), callback); err != nil { - return fmt.Errorf("walking %s: %w", field.Name(), err) - } - } - - return nil - -} - -// An element in a path from a root message to a leaf node -// Messages use field name strings -// Repeated uses index numbes as strings (1 = "1") -// Maps use the map keys, which are always strings in J5 land. -// OneOf is not included as it doesn't appear in the proto tree -type ProtoPathSpec []string - -func ParseProtoPathSpec(path string) ProtoPathSpec { - return ProtoPathSpec(strings.Split(path, ".")) -} - -func (pp ProtoPathSpec) String() string { - return strings.Join(pp, ".") -} - -func NewProtoPath(message protoreflect.MessageDescriptor, fieldPath ProtoPathSpec) (*Path, error) { - - if len(fieldPath) == 0 { - return nil, fmt.Errorf("fieldPath must have at least one element") - } - - pathSpec := &Path{ - root: message, - path: make([]pathNode, 0, len(fieldPath)), - } - - walkMessage := message - var pathElem string - walkPath := fieldPath - var inOneof *int - - for { - - pathElem, walkPath = walkPath[0], walkPath[1:] - node := pathNode{ - name: protoreflect.Name(pathElem), - } - field := walkMessage.Fields().ByName(protoreflect.Name(pathElem)) - if field != nil { - node.field = field - // Check that it isn't a oneof. - if inOneof != nil { - if field.ContainingOneof().Index() != *inOneof { - return nil, fmt.Errorf("field %s not in oneof %d", pathElem, *inOneof) - } - inOneof = nil - } - } else { - if inOneof != nil { - return nil, fmt.Errorf("field %s not found in oneof %d", pathElem, *inOneof) - } - // Oneof needn't be specified as it doens't appear in the node tree, - // but if a oneof is named, this adds it as a node in the tree, - // hoping that the next element will be an actual field. - oneof := walkMessage.Oneofs().ByName(protoreflect.Name(pathElem)) - if oneof != nil { - node.oneof = oneof - } else { - return nil, fmt.Errorf("no field named '%s' in message %s", pathElem, walkMessage.FullName()) - } - idx := oneof.Index() - inOneof = &idx - - pathSpec.path = append(pathSpec.path, node) - if len(walkPath) == 0 { - pathSpec.leafOneof = node.oneof - break - } - continue - } - - if field.IsMap() { - return nil, fmt.Errorf("unimplemented: map fields in path spec") - } - - pathSpec.path = append(pathSpec.path, node) - - if len(walkPath) == 0 { - pathSpec.leafField = node.field - break - } - - if field.Kind() != protoreflect.MessageKind { - return nil, fmt.Errorf("field %s is not a message, but path elements remain (%v)", pathElem, walkPath) - } - - walkMessage = field.Message() - - } - - return pathSpec, nil -} - -func findField(message protoreflect.MessageDescriptor, fieldPath string) []pathNode { - fields := message.Fields() - if field := fields.ByJSONName(fieldPath); field != nil { - return []pathNode{{ - name: protoreflect.Name(fieldPath), - field: field, - }} - } - - for i := range fields.Len() { - field := fields.Get(i) - // Check for flattened fields - fieldOpts, ok := proto.GetExtension(field.Options().(*descriptorpb.FieldOptions), ext_j5pb.E_Field).(*ext_j5pb.FieldOptions) - if !ok { - continue - } - - if fieldOpts != nil { - // TODO: remove the use of Message once everything has been moved over to Object - if (fieldOpts.GetMessage() != nil && fieldOpts.GetMessage().Flatten) || - (fieldOpts.GetObject() != nil && fieldOpts.GetObject().Flatten) { - if flattenedField := field.Message().Fields().ByJSONName(fieldPath); flattenedField != nil { - return []pathNode{ - { - name: field.Name(), - field: field, - }, - { - name: protoreflect.Name(fieldPath), - field: flattenedField, - }, - } - } - } - } - } - - return nil -} - -// Like ProtoPathSpec but uses JSON field names -type JSONPathSpec []string - -func ParseJSONPathSpec(path string) JSONPathSpec { - return JSONPathSpec(strings.Split(path, ".")) -} - -func (jp JSONPathSpec) String() string { - return strings.Join(jp, ".") -} - -func NewJSONPath(message protoreflect.MessageDescriptor, fieldPath JSONPathSpec) (*Path, error) { - - if len(fieldPath) == 0 { - return nil, fmt.Errorf("fieldPath must have at least one element") - } - - pathSpec := &Path{ - root: message, - path: make([]pathNode, 0, len(fieldPath)), - } - - walkMessage := message - var pathElem string - walkPath := fieldPath - var nodes []pathNode - - for { - pathElem, walkPath = walkPath[0], walkPath[1:] - nodes = findField(walkMessage, pathElem) - if nodes == nil { - // Very Special Edge Case: Oneof wrapper types allow the client to - // filter based on the type of the oneof. So the oneof can be at the - // end of the path, and the field can be the oneof wrapper type. - - if len(walkPath) == 0 && pathElem == "type" { - oneof := walkMessage.Oneofs().ByName(protoreflect.Name("type")) - if oneof != nil { - nodes = []pathNode{{ - name: protoreflect.Name(pathElem), - oneof: oneof, - }} - pathSpec.path = append(pathSpec.path, nodes...) - pathSpec.leafOneof = oneof - break - } - } - - return nil, fmt.Errorf("JSON field '%s' not found in message %s", pathElem, walkMessage.FullName()) - } - - for _, node := range nodes { - if node.field.IsMap() { - return nil, fmt.Errorf("unimplemented: map fields in path spec") - } - } - - pathSpec.path = append(pathSpec.path, nodes...) - if len(walkPath) == 0 { - pathSpec.leafField = nodes[len(nodes)-1].field - break - } - - if nodes[len(nodes)-1].field.Kind() != protoreflect.MessageKind { - return nil, fmt.Errorf("field %s is not a message, but path elements remain", pathElem) - } - - walkMessage = nodes[len(nodes)-1].field.Message() - } - - return pathSpec, nil -} - -func (pp *Path) GetValue(msg protoreflect.Message) (protoreflect.Value, error) { - if len(pp.path) == 0 { - return protoreflect.Value{}, fmt.Errorf("empty path") - } - var val protoreflect.Value - - var walkNode pathNode - walkMessage := msg - - remainingPath := pp.path - for { - walkNode, remainingPath = remainingPath[0], remainingPath[1:] - if walkNode.oneof != nil { - // ignore the oneof - if len(remainingPath) == 0 { - return protoreflect.Value{}, fmt.Errorf("oneof at leaf") - } - continue - } - - if walkNode.field == nil { - return protoreflect.Value{}, fmt.Errorf("no field or oneof") - } - - // Has vs Get, Has returns false if the field is set to the default value for - // scalar types. We still want the fields if they are set to the default value, - // and can use validation of a field's existence before this point to ensure - // that the field is available. - val = walkMessage.Get(walkNode.field) - if len(remainingPath) == 0 { - return val, nil - } - - if walkNode.field.Kind() != protoreflect.MessageKind { - return protoreflect.Value{}, fmt.Errorf("field %s is not a message", walkNode.field.Name()) - } - walkMessage = val.Message() - } -} diff --git a/internal/pgstore/path_test.go b/internal/pgstore/path_test.go deleted file mode 100644 index 6765f42..0000000 --- a/internal/pgstore/path_test.go +++ /dev/null @@ -1,132 +0,0 @@ -package pgstore - -import ( - "testing" - - "github.com/pentops/flowtest/prototest" - "github.com/stretchr/testify/assert" -) - -func TestFindFieldSpec(t *testing.T) { - descFiles := prototest.DescriptorsFromSource(t, map[string]string{ - "test.proto": ` - syntax = "proto3"; - - - package test; - - message Foo { - string id = 1; - Profile profile = 2; - } - - message Profile { - int64 weight = 1; - - oneof type { - Card card = 2; - } - } - - message Card { - int64 size = 1; - } - `}) - - fooDesc := descFiles.MessageByName(t, "test.Foo") - - tcs := []struct { - path ProtoPathSpec - jsonPath JSONPathSpec - }{ - {path: ProtoPathSpec{"id"}}, - {path: ProtoPathSpec{"profile", "weight"}}, - {path: ProtoPathSpec{"profile", "card", "size"}}, - } - - for _, tc := range tcs { - if tc.jsonPath == nil { - tc.jsonPath = JSONPathSpec(tc.path) // assume it's just lower case wingle word - } - t.Run(tc.path.String()+" Proto", func(t *testing.T) { - spec, err := NewProtoPath(fooDesc, tc.path) - if err != nil { - t.Fatal(err) - } - - if spec.leafField == nil { - t.Fatal("expected field") - } - - name := tc.path[len(tc.path)-1] - assert.Equal(t, string(spec.leafField.Name()), name) - }) - t.Run(tc.jsonPath.String()+" JSON", func(t *testing.T) { - spec, err := NewJSONPath(fooDesc, tc.jsonPath) - if err != nil { - t.Fatal(err) - } - - if spec.leafField == nil { - t.Fatal("expected field") - } - - name := tc.path[len(tc.path)-1] - assert.Equal(t, string(spec.leafField.Name()), name) - }) - } - - tcs = []struct { - path ProtoPathSpec - jsonPath JSONPathSpec - }{ - {path: ProtoPathSpec{"foo"}}, - {path: ProtoPathSpec{"profile", "weight", "size"}}, - } - - for _, tc := range tcs { - if tc.jsonPath == nil { - tc.jsonPath = JSONPathSpec(tc.path) // assume it's just lower case wingle word - } - t.Run(tc.path.String()+" Proto Sad", func(t *testing.T) { - _, err := NewProtoPath(fooDesc, tc.path) - if err == nil { - t.Fatal("expected an error") - } - }) - t.Run(tc.jsonPath.String()+" JSON Sad", func(t *testing.T) { - _, err := NewJSONPath(fooDesc, tc.jsonPath) - if err == nil { - t.Fatal("expected an error") - } - }) - } - - t.Run("Walk", func(t *testing.T) { - - expectedNodes := map[string]struct{}{ - "$.id": {}, - "$.profile": {}, - "$.profile.weight": {}, - "$.profile.card": {}, - "$.profile.card.size": {}, - } - - err := WalkPathNodes(fooDesc, func(node Path) error { - pathQuery := node.JSONPathQuery() - t.Log(pathQuery) - _, ok := expectedNodes[pathQuery] - if !ok { - t.Fatalf("unexpected node %s", pathQuery) - } - delete(expectedNodes, pathQuery) - return nil - }) - if err != nil { - t.Fatal(err) - } - for node := range expectedNodes { - t.Errorf("expected node %s", node) - } - }) -} diff --git a/internal/pgstore/pgmigrate/builder.go b/internal/pgstore/pgmigrate/builder.go deleted file mode 100644 index c2c6d73..0000000 --- a/internal/pgstore/pgmigrate/builder.go +++ /dev/null @@ -1,181 +0,0 @@ -package pgmigrate - -import ( - "fmt" - "strings" -) - -type CreateTableBuilder struct { - name string - columns []*column - foreignKeys []*ForeignKeyBuilder -} - -func CreateTable(name string) *CreateTableBuilder { - return &CreateTableBuilder{ - name: name, - } -} - -func (t *CreateTableBuilder) Column(name string, typ ColumnType, options ...ColumnOption) *CreateTableBuilder { - column := &column{ - name: name, - typeName: typ, - flags: []string{}, - } - for _, opt := range options { - opt(column) - } - t.columns = append(t.columns, column) - return t -} - -func (t *CreateTableBuilder) ForeignKey(name, tableName string) *ForeignKeyBuilder { - fk := &ForeignKeyBuilder{ - name: name, - tableName: tableName, - } - t.foreignKeys = append(t.foreignKeys, fk) - return fk -} - -type ForeignKeyBuilder struct { - name string - tableName string - columns []ColumnPair -} - -type ColumnPair struct { - local, foreign string -} - -func (fk *ForeignKeyBuilder) Column(localName, remoteName string) *ForeignKeyBuilder { - fk.columns = append(fk.columns, ColumnPair{local: localName, foreign: remoteName}) - return fk -} - -type column struct { - name string - - primaryKey bool // Multi Primary Key is possible - notNull bool - - typeName ColumnType - flags []string -} - -type ColumnOption func(*column) - -// PrimaryKey adds this column as a primary key, if there are multiple -// primary keys, they will be added as a composite key -func PrimaryKey(c *column) { - c.primaryKey = true -} - -func NotNull(c *column) { - c.notNull = true -} - -type ColumnType string - -const ( - UUID ColumnType = "uuid" - Text ColumnType = "text" - Timestamptz ColumnType = "timestamptz" - JSONB ColumnType = "jsonb" - Int ColumnType = "int" -) - -func (t *CreateTableBuilder) Build() (*Table, error) { - table := &Table{ - Name: t.name, - } - - for _, col := range t.columns { - column := Column{ - Name: col.name, - Type: string(col.typeName), - } - if col.primaryKey { - table.PrimaryKey = append(table.PrimaryKey, col.name) - } - if col.notNull { - column.Flags = append(column.Flags, "NOT NULL") - } - table.Columns = append(table.Columns, column) - } - - for _, fk := range t.foreignKeys { - foreignKey := ForeignKey{ - Name: fk.name, - TableName: fk.tableName, - Columns: fk.columns, - } - - table.ForeignKeys = append(table.ForeignKeys, foreignKey) - } - - return table, nil -} - -type Table struct { - Name string - Columns []Column - PrimaryKey []string - ForeignKeys []ForeignKey -} - -type Column struct { - Name string - Type string - Flags []string -} - -type ForeignKey struct { - Name string - TableName string - Columns []ColumnPair -} - -func (tt *Table) DownSQL() (string, error) { - return fmt.Sprintf("DROP TABLE %s;", tt.Name), nil -} - -func (table *Table) ToSQL() (string, error) { - clauses := make([]string, 0) - - for _, col := range table.Columns { - line := make([]string, 2+len(col.Flags)) - line[0] = col.Name - line[1] = col.Type - copy(line[2:], col.Flags) - clauses = append(clauses, strings.Join(line, " ")) - } - - if len(table.PrimaryKey) > 0 { - clauses = append(clauses, fmt.Sprintf("CONSTRAINT %s_pk PRIMARY KEY (%s)", table.Name, strings.Join(table.PrimaryKey, ", "))) - } - - for _, fk := range table.ForeignKeys { - localColumns := make([]string, 0, len(fk.Columns)) - remoteColumns := make([]string, 0, len(fk.Columns)) - for _, col := range fk.Columns { - localColumns = append(localColumns, col.local) - remoteColumns = append(remoteColumns, col.foreign) - } - - clauses = append(clauses, fmt.Sprintf("CONSTRAINT %s_fk_%s FOREIGN KEY (%s) REFERENCES %s(%s)", table.Name, fk.Name, strings.Join(localColumns, ", "), fk.TableName, strings.Join(remoteColumns, ", "))) - } - - lines := make([]string, 1, len(clauses)+2) - lines[0] = fmt.Sprintf("CREATE TABLE %s (", table.Name) - for idx, clause := range clauses { - suffix := "," - if idx == len(clauses)-1 { - suffix = "" - } - lines = append(lines, " "+clause+suffix) - } - lines = append(lines, ");") - return strings.Join(lines, "\n"), nil -} diff --git a/internal/pgstore/pgmigrate/printer.go b/internal/pgstore/pgmigrate/printer.go deleted file mode 100644 index c5957db..0000000 --- a/internal/pgstore/pgmigrate/printer.go +++ /dev/null @@ -1,74 +0,0 @@ -package pgmigrate - -import ( - "bytes" - "fmt" - "strings" -) - -type MigrationItem interface { - ToSQL() (string, error) - DownSQL() (string, error) -} - -func PrintMigrations(items ...MigrationItem) ([]byte, error) { - p := newPrinter() - p.p("-- +goose Up") - p.setGap() - for _, table := range items { - val, err := table.ToSQL() - if err != nil { - return nil, err - } - p.p(val) - p.setGap() - } - p.p("-- +goose Down") - p.setGap() - for idx := len(items) - 1; idx >= 0; idx-- { - item := items[idx] - downVal, err := item.DownSQL() - if err != nil { - return nil, err - } - if len(strings.Split(downVal, "\n")) > 1 { - p.setGap() - p.p(downVal) - p.setGap() - } else { - p.p(downVal) - } - } - - return p.bytes(), nil -} - -type printer struct { - buf bytes.Buffer - gap bool -} - -func newPrinter() *printer { - return &printer{ - buf: bytes.Buffer{}, - } -} - -func (p *printer) setGap() { - p.gap = true -} - -func (p *printer) p(elem ...any) { - if p.gap { - fmt.Fprintln(&p.buf) - p.gap = false - } - for _, elem := range elem { - fmt.Fprint(&p.buf, elem) - } - fmt.Fprintln(&p.buf) -} - -func (p *printer) bytes() []byte { - return p.buf.Bytes() -} diff --git a/internal/pgstore/pgmigrate/psm.go b/internal/pgstore/pgmigrate/psm.go deleted file mode 100644 index 3cda056..0000000 --- a/internal/pgstore/pgmigrate/psm.go +++ /dev/null @@ -1,298 +0,0 @@ -package pgmigrate - -import ( - "context" - "fmt" - "strings" - - sq "github.com/elgris/sqrl" - "github.com/pentops/j5/gen/j5/list/v1/list_j5pb" - "github.com/pentops/j5/gen/j5/schema/v1/schema_j5pb" - "github.com/pentops/protostate/internal/pgstore" - "github.com/pentops/protostate/psm" - "github.com/pentops/sqrlx.go/sqrlx" - "google.golang.org/protobuf/proto" - "google.golang.org/protobuf/reflect/protoreflect" - "google.golang.org/protobuf/types/descriptorpb" -) - -func BuildStateMachineMigrations(specs ...psm.QueryTableSpec) ([]byte, error) { - - allMigrations := make([]MigrationItem, 0, len(specs)*4) - - for _, spec := range specs { - stateTable, eventTable, err := BuildPSMTables(spec) - if err != nil { - return nil, err - } - allMigrations = append(allMigrations, stateTable) - - indexes, err := buildIndexes(spec.State.TableName, spec.State.Root.ColumnName, spec.StateType) - if err != nil { - return nil, err - } - for _, index := range indexes { - allMigrations = append(allMigrations, index) - } - - allMigrations = append(allMigrations, eventTable) - - indexes, err = buildIndexes(spec.Event.TableName, spec.Event.Root.ColumnName, spec.EventType) - if err != nil { - return nil, err - } - for _, index := range indexes { - allMigrations = append(allMigrations, index) - } - } - - fileData, err := PrintMigrations(allMigrations...) - if err != nil { - return nil, err - } - return fileData, nil -} - -func CreateStateMachines(ctx context.Context, conn sqrlx.Connection, specs ...psm.QueryTableSpec) error { - db, err := sqrlx.New(conn, sq.Dollar) - if err != nil { - return err - } - - tables := make([]*Table, 0, len(specs)) - for _, spec := range specs { - stateTable, eventTable, err := BuildPSMTables(spec) - if err != nil { - return err - } - tables = append(tables, stateTable, eventTable) - } - - return db.Transact(ctx, nil, func(ctx context.Context, tx sqrlx.Transaction) error { - if _, err := conn.BeginTx(ctx, nil); err != nil { - return err - } - - for _, table := range tables { - - statement, err := table.ToSQL() - if err != nil { - return err - } - _, err = tx.ExecRaw(ctx, statement) - if err != nil { - return err - } - } - return nil - }) -} - -type searchSpec struct { - tsvColumn string - tableName string - columnName string - path pgstore.Path -} - -func (ss searchSpec) ToSQL() (string, error) { - statement := fmt.Sprintf("to_tsvector('english', jsonb_path_query_array(%s, '%s'))", ss.columnName, ss.path.JSONPathQuery()) - - lines := []string{} - - lines = append(lines, fmt.Sprintf("ALTER TABLE %s ADD COLUMN %s tsvector GENERATED ALWAYS", ss.tableName, ss.tsvColumn)) - lines = append(lines, fmt.Sprintf(" AS (%s) STORED;", statement)) - lines = append(lines, "") - lines = append(lines, fmt.Sprintf("CREATE INDEX %s_%s_idx ON %s USING GIN (%s);", ss.tableName, ss.tsvColumn, ss.tableName, ss.tsvColumn)) - return strings.Join(lines, "\n"), nil - -} - -func (ss searchSpec) DownSQL() (string, error) { - return fmt.Sprintf("DROP INDEX %s_%s_idx;\nALTER TABLE %s DROP COLUMN %s;", ss.tableName, ss.tsvColumn, ss.tableName, ss.tsvColumn), nil -} - -func AddIndexes(ctx context.Context, conn sqrlx.Connection, specs ...psm.QueryTableSpec) error { - allIndexes := make([]searchSpec, 0) - for _, spec := range specs { - indexes, err := buildIndexes(spec.State.TableName, spec.State.Root.ColumnName, spec.StateType) - if err != nil { - return err - } - allIndexes = append(allIndexes, indexes...) - - indexes, err = buildIndexes(spec.Event.TableName, spec.Event.Root.ColumnName, spec.EventType) - if err != nil { - return err - } - allIndexes = append(allIndexes, indexes...) - } - - return writeIndexes(ctx, conn, allIndexes) -} - -func buildIndexes(tableName string, columnName string, rootType protoreflect.MessageDescriptor) ([]searchSpec, error) { - - specs := []searchSpec{} - - if err := pgstore.WalkPathNodes(rootType, func(node pgstore.Path) error { - field := node.LeafField() - if field == nil || field.Kind() != protoreflect.StringKind { - return nil - } - - fieldOpts, ok := proto.GetExtension(field.Options().(*descriptorpb.FieldOptions), list_j5pb.E_Field).(*list_j5pb.FieldConstraint) - if !ok { - return nil - } - - switch fieldOpts.GetString_().GetWellKnown().(type) { - case *list_j5pb.StringRules_OpenText: - searchOpts := fieldOpts.GetString_().GetOpenText().GetSearching() - if searchOpts == nil || !searchOpts.Searchable { - return nil - } - - specs = append(specs, searchSpec{ - tsvColumn: searchOpts.GetFieldIdentifier(), - tableName: tableName, - columnName: columnName, - path: node, - }) - } - - return nil - }); err != nil { - return nil, err - } - - return specs, nil - -} - -func writeIndexes(ctx context.Context, conn sqrlx.Connection, specs []searchSpec) error { - - db, err := sqrlx.New(conn, sq.Dollar) - if err != nil { - return err - } - - return db.Transact(ctx, nil, func(ctx context.Context, tx sqrlx.Transaction) error { - for _, spec := range specs { - - var count int - - err := tx.QueryRow(ctx, sq.Select("COUNT(column_name)"). - From("information_schema.columns"). - Where("table_schema = CURRENT_SCHEMA"). - Where(sq.Eq{"table_name": spec.tableName, "column_name": spec.tsvColumn})).Scan(&count) - if err != nil { - return err - } - if count > 0 { - return nil - } - - statement := fmt.Sprintf("to_tsvector('english', jsonb_path_query_array(%s, '%s'))", spec.columnName, spec.path.JSONPathQuery()) - - _, err = tx.ExecRaw(ctx, fmt.Sprintf("ALTER TABLE %s ADD COLUMN %s tsvector GENERATED ALWAYS AS (%s) STORED;", spec.tableName, spec.tsvColumn, statement)) - if err != nil { - return err - } - - _, err = tx.ExecRaw(ctx, fmt.Sprintf("CREATE INDEX %s_%s_idx ON %s USING GIN (%s);", spec.tableName, spec.tsvColumn, spec.tableName, spec.tsvColumn)) - if err != nil { - return err - } - - } - return nil - }) - -} - -const ( - uuidType = ColumnType("uuid") - textType = ColumnType("text") - id62Type = ColumnType("char(22)") - intType = ColumnType("int") - timestamptzType = ColumnType("timestamptz") - jsonbType = ColumnType("jsonb") - dateType = ColumnType("date") -) - -func BuildPSMTables(spec psm.QueryTableSpec) (*Table, *Table, error) { - - stateTable := CreateTable(spec.State.TableName) - - eventTable := CreateTable(spec.Event.TableName). - Column(spec.Event.ID.ColumnName, uuidType, PrimaryKey) - - eventForeignKey := eventTable.ForeignKey("state", spec.State.TableName) - for _, key := range spec.KeyColumns { - format, err := fieldFormat(key.Schema) - if err != nil { - return nil, nil, fmt.Errorf("key %s: %w", key.ColumnName, err) - } - - if key.Primary { - stateTable.Column(key.ColumnName, format, PrimaryKey) - eventTable.Column(key.ColumnName, format, NotNull) - eventForeignKey.Column(key.ColumnName, key.ColumnName) - continue - } - if key.Required { - stateTable.Column(key.ColumnName, format, NotNull) - eventTable.Column(key.ColumnName, format, NotNull) - continue - } - stateTable.Column(key.ColumnName, format) - eventTable.Column(key.ColumnName, format) - } - - stateTable.Column(spec.State.Root.ColumnName, jsonbType, NotNull) - - eventTable.Column(spec.Event.Timestamp.ColumnName, timestamptzType, NotNull). - Column(spec.Event.Sequence.ColumnName, intType, NotNull). - Column(spec.Event.Root.ColumnName, jsonbType, NotNull). - Column(spec.Event.StateSnapshot.ColumnName, jsonbType, NotNull) - - state, err := stateTable.Build() - if err != nil { - return nil, nil, err - } - - event, err := eventTable.Build() - if err != nil { - return nil, nil, err - } - - return state, event, nil -} - -func fieldFormat(schema *schema_j5pb.Field) (ColumnType, error) { - - switch ft := schema.Type.(type) { - case *schema_j5pb.Field_String_: - return textType, nil - case *schema_j5pb.Field_Key: - if ft.Key.Format == nil { - return textType, nil - } - switch ft.Key.Format.Type.(type) { - case *schema_j5pb.KeyFormat_Custom_: - return textType, nil - case *schema_j5pb.KeyFormat_Uuid: - return uuidType, nil - case *schema_j5pb.KeyFormat_Id62: - return id62Type, nil - default: - return textType, nil - } - case *schema_j5pb.Field_Date: - return dateType, nil - default: - return textType, fmt.Errorf("unsupported type for key field %T", schema.Type) - } - -} diff --git a/internal/protogen/query/code.go b/internal/protogen/query/code.go index e4ee24a..22e606a 100644 --- a/internal/protogen/query/code.go +++ b/internal/protogen/query/code.go @@ -7,10 +7,13 @@ import ( ) var ( - protoPackage = protogen.GoImportPath("google.golang.org/protobuf/proto") + //protoPackage = protogen.GoImportPath("google.golang.org/protobuf/proto") stateMachinePackage = protogen.GoImportPath("github.com/pentops/protostate/psm") sqrlxPkg = protogen.GoImportPath("github.com/pentops/sqrlx.go/sqrlx") + j5ReflectPackage = protogen.GoImportPath("github.com/pentops/j5/lib/j5reflect") + j5SchemaPackage = protogen.GoImportPath("github.com/pentops/j5/lib/j5schema") contextPkg = protogen.GoImportPath("context") + fmtPackage = protogen.GoImportPath("fmt") ) func quoteString(s string) string { @@ -57,24 +60,30 @@ func (qs PSMQuerySet) Write(g *protogen.GeneratedFile) { func (qs PSMQuerySet) writeQuerySet(g *protogen.GeneratedFile) { qs.genericTypeAlias(g, qs.GoName+"PSMQuerySet", "StateQuerySet") g.P("func New", qs.GoName, "PSMQuerySet(") - g.P("smSpec ", stateMachinePackage.Ident("QuerySpec"), "[") - qs.writeGenericTypeSet(g) - g.P("],") + g.P("smSpec ", stateMachinePackage.Ident("QuerySpec"), ",") g.P("options ", stateMachinePackage.Ident("StateQueryOptions"), ",") g.P(") (*", qs.GoName, "PSMQuerySet, error) {") - g.P("return ", stateMachinePackage.Ident("BuildStateQuerySet"), "[") - qs.writeGenericTypeSet(g) - g.P("](smSpec, options)") + g.P("return ", stateMachinePackage.Ident("BuildStateQuerySet"), "(smSpec, options)") g.P("}") } +func j5Method(g *protogen.GeneratedFile, m protogen.Method, name string) { + g.P(" ", name, ": &", j5SchemaPackage.Ident("MethodSchema"), "{") + g.P(" Request: ", j5SchemaPackage.Ident("MustObjectSchema"), "((&", m.Input.GoIdent, "{}).ProtoReflect().Descriptor()),") + g.P(" Response: ", j5SchemaPackage.Ident("MustObjectSchema"), "((&", m.Output.GoIdent, "{}).ProtoReflect().Descriptor()),") + g.P(" },") +} + func (qs PSMQuerySet) writeQuerySpec(g *protogen.GeneratedFile) { qs.genericTypeAlias(g, qs.GoName+"PSMQuerySpec", "QuerySpec") g.P() g.P("func Default", qs.GoName, "PSMQuerySpec(tableSpec ", stateMachinePackage.Ident("QueryTableSpec"), ") ", qs.GoName, "PSMQuerySpec {") - g.P(" return ", stateMachinePackage.Ident("QuerySpec"), "[") - qs.writeGenericTypeSet(g) - g.P(" ]{") + g.P(" return ", stateMachinePackage.Ident("QuerySpec"), "{") + j5Method(g, qs.GetMethod, "GetMethod") + j5Method(g, qs.ListMethod, "ListMethod") + if qs.ListEventsMethod != nil { + j5Method(g, *qs.ListEventsMethod, "ListEventsMethod") + } g.P(" QueryTableSpec: tableSpec,") qs.listFilter(g, qs.ListREQ, "ListRequestFilter", qs.ListRequestFilter) qs.listFilter(g, *qs.ListEventsREQ, "ListEventsRequestFilter", qs.ListEventsRequestFilter) @@ -102,7 +111,7 @@ func (qs PSMQuerySet) writeDefaultService(g *protogen.GeneratedFile) { g.P() g.P("func (s *", serviceName, ") ", qs.GetMethod.GoName, "(ctx ", contextPkg.Ident("Context"), ", req *", qs.GetREQ.GoName, ") (*", qs.GetRES.GoName, ", error) {") g.P(" resObject := &", qs.GetRES.GoName, "{}") - g.P(" err := s.querySet.Get(ctx, s.db, req, resObject)") + g.P(" err := s.querySet.Get(ctx, s.db, req.J5Object(), resObject.J5Object())") g.P(" if err != nil {") g.P(" return nil, err") g.P(" }") @@ -111,7 +120,7 @@ func (qs PSMQuerySet) writeDefaultService(g *protogen.GeneratedFile) { g.P() g.P("func (s *", serviceName, ") ", qs.ListMethod.GoName, "(ctx ", contextPkg.Ident("Context"), ", req *", qs.ListREQ.GoName, ") (*", qs.ListRES.GoName, ", error) {") g.P(" resObject := &", qs.ListRES.GoName, "{}") - g.P(" err := s.querySet.List(ctx, s.db, req, resObject)") + g.P(" err := s.querySet.List(ctx, s.db, req.J5Object(), resObject.J5Object())") g.P(" if err != nil {") g.P(" return nil, err") g.P(" }") @@ -123,7 +132,7 @@ func (qs PSMQuerySet) writeDefaultService(g *protogen.GeneratedFile) { g.P() g.P("func (s *", serviceName, ") ", qs.ListEventsMethod.GoName, "(ctx ", contextPkg.Ident("Context"), ", req *", *qs.ListEventsREQ, ") (*", *qs.ListEventsRES, ", error) {") g.P(" resObject := &", *qs.ListEventsRES, "{}") - g.P(" err := s.querySet.ListEvents(ctx, s.db, req, resObject)") + g.P(" err := s.querySet.ListEvents(ctx, s.db, req.J5Object(), resObject.J5Object())") g.P(" if err != nil {") g.P(" return nil, err") g.P(" }") @@ -131,6 +140,7 @@ func (qs PSMQuerySet) writeDefaultService(g *protogen.GeneratedFile) { g.P("}") } +/* func (qs PSMQuerySet) writeGenericTypeSet(g *protogen.GeneratedFile) { g.P("*", qs.GetREQ, ",") g.P("*", qs.GetRES, ",") @@ -144,17 +154,23 @@ func (qs PSMQuerySet) writeGenericTypeSet(g *protogen.GeneratedFile) { g.P(protoPackage.Ident("Message"), ",") } } +*/ func (qs PSMQuerySet) genericTypeAlias(g *protogen.GeneratedFile, typedName string, psmName string) { - g.P("type ", typedName, " = ", stateMachinePackage.Ident(psmName), "[") + g.P("type ", typedName, " = ", stateMachinePackage.Ident(psmName)) + /*"[") qs.writeGenericTypeSet(g) g.P("]") - g.P() + g.P()*/ } func (qs PSMQuerySet) listFilter(g *protogen.GeneratedFile, reqType protogen.GoIdent, name string, fields []ListFilterField) { // ListRequestFilter func(ListREQ) (map[string]interface{}, error) - g.P(name, ": func(req *", reqType, ") (map[string]interface{}, error) {") + g.P(name, ": func(reqReflect ", j5ReflectPackage.Ident("Object"), ") (map[string]interface{}, error) {") + g.P(" req, ok := reqReflect.Interface().(*", reqType, ")") + g.P(" if !ok {") + g.P(" return nil, ", fmtPackage.Ident("Errorf"), "(\"expected *", reqType, " but got %T\", req)") + g.P(" }") g.P(" filter := map[string]interface{}{}") for _, field := range fields { if field.Optional { diff --git a/internal/protogen/query/parse.go b/internal/protogen/query/parse.go index df7d7e5..7d0cfb9 100644 --- a/internal/protogen/query/parse.go +++ b/internal/protogen/query/parse.go @@ -7,7 +7,8 @@ import ( "github.com/iancoleman/strcase" "github.com/pentops/j5/gen/j5/ext/v1/ext_j5pb" - "github.com/pentops/protostate/pquery" + pquery "github.com/pentops/j5/lib/j5query" + "github.com/pentops/j5/lib/j5schema" "github.com/pentops/protostate/psm" "google.golang.org/protobuf/compiler/protogen" "google.golang.org/protobuf/proto" @@ -34,20 +35,25 @@ func WalkFile(file *protogen.File) ([]*PSMQuerySet, error) { methodSet := NewQueryServiceGenerateSet(stateQuery.Entity, service) for _, method := range service.Methods { + var stateQuery *ext_j5pb.StateQueryMethodOptions methodOpt := proto.GetExtension(method.Desc.Options(), ext_j5pb.E_Method).(*ext_j5pb.MethodOptions) - if methodOpt == nil { + if methodOpt != nil { + stateQuery = methodOpt.GetStateQuery() + if stateQuery == nil { + return nil, fmt.Errorf("method %s does not have a state query type", method.Desc.Name()) + } + } else { name := string(method.Desc.Name()) - methodOpt := &ext_j5pb.MethodOptions{} if strings.HasPrefix(name, "Get") { - methodOpt.StateQuery = &ext_j5pb.StateQueryMethodOptions{ + stateQuery = &ext_j5pb.StateQueryMethodOptions{ Get: true, } } else if strings.HasPrefix(name, "List") && strings.HasSuffix(name, "Events") { - methodOpt.StateQuery = &ext_j5pb.StateQueryMethodOptions{ + stateQuery = &ext_j5pb.StateQueryMethodOptions{ ListEvents: true, } } else if strings.HasPrefix(name, "List") { - methodOpt.StateQuery = &ext_j5pb.StateQueryMethodOptions{ + stateQuery = &ext_j5pb.StateQueryMethodOptions{ List: true, } } else { @@ -55,7 +61,7 @@ func WalkFile(file *protogen.File) ([]*PSMQuerySet, error) { } } - if err := methodSet.AddMethod(method, methodOpt.StateQuery); err != nil { + if err := methodSet.AddMethod(method, stateQuery); err != nil { return nil, fmt.Errorf("adding method %s to %s: %w", method.Desc.Name(), service.Desc.FullName(), err) } } @@ -133,6 +139,29 @@ func (qs QueryServiceGenerateSet) validate() error { return nil } +func fieldByDesc(fields []*protogen.Field, jsonName string) *protogen.Field { + for _, f := range fields { + if f.Desc.JSONName() == jsonName { + return f + } + } + return nil +} + +func methodPair(method *protogen.Method) (*j5schema.MethodSchema, error) { + reqObj, err := j5schema.Global.ObjectSchema(method.Input.Desc) + if err != nil { + return nil, fmt.Errorf("j5schema.ObjectSchema for %s: %w", method.Desc.FullName(), err) + } + resObj, err := j5schema.Global.ObjectSchema(method.Output.Desc) + if err != nil { + return nil, fmt.Errorf("j5schema.ObjectSchema for %s: %w", method.Desc.FullName(), err) + } + return &j5schema.MethodSchema{ + Request: reqObj, + Response: resObj, + }, nil +} func BuildQuerySet(qs QueryServiceGenerateSet) (*PSMQuerySet, error) { if err := qs.validate(); err != nil { @@ -164,8 +193,13 @@ func BuildQuerySet(qs QueryServiceGenerateSet) (*PSMQuerySet, error) { return nil, errors.Join(errs...) } + listMethod, err := methodPair(qs.listMethod) + if err != nil { + return nil, fmt.Errorf("building list method pair for %s: %w", qs.listMethod.Desc.FullName(), err) + } + // Empty table spec, the fields don't matter here. - listReflectionSet, err := pquery.BuildListReflection(qs.listMethod.Input.Desc, qs.listMethod.Output.Desc, pquery.TableSpec{}) + listReflectionSet, err := pquery.BuildListReflection(listMethod, pquery.TableSpec{}) if err != nil { return nil, fmt.Errorf("pquery.BuildListReflection for %s: %w", qs.listMethod.Desc.FullName(), err) } @@ -183,7 +217,8 @@ func BuildQuerySet(qs QueryServiceGenerateSet) (*PSMQuerySet, error) { GetMethod: *qs.getMethod, } - for _, field := range listReflectionSet.RequestFilterFields { + for _, fieldSpec := range listReflectionSet.RequestFilterFields { + field := fieldByDesc(qs.listMethod.Input.Fields, fieldSpec.JSONName).Desc genField := mapGenField(qs.listMethod.Input, field) ww.ListRequestFilter = append(ww.ListRequestFilter, ListFilterField{ DBName: string(field.Name()), @@ -192,7 +227,12 @@ func BuildQuerySet(qs QueryServiceGenerateSet) (*PSMQuerySet, error) { }) } - listEventsReflectionSet, err := pquery.BuildListReflection(qs.listEventsMethod.Input.Desc, qs.listEventsMethod.Output.Desc, pquery.TableSpec{}) + listEventsMethod, err := methodPair(qs.listEventsMethod) + if err != nil { + return nil, fmt.Errorf("building list events method pair for %s: %w", qs.listEventsMethod.Desc.FullName(), err) + } + + listEventsReflectionSet, err := pquery.BuildListReflection(listEventsMethod, pquery.TableSpec{}) if err != nil { return nil, fmt.Errorf("pquery.BuildListReflection for %s is not compatible with PSM: %w", qs.listEventsMethod.Desc.FullName(), err) } @@ -201,11 +241,11 @@ func BuildQuerySet(qs QueryServiceGenerateSet) (*PSMQuerySet, error) { ww.ListEventsRES = &qs.listEventsMethod.Output.GoIdent ww.ListEventsMethod = qs.listEventsMethod for _, field := range listEventsReflectionSet.RequestFilterFields { - genField := mapGenField(qs.listEventsMethod.Input, field) + genField := mapJSONField(qs.listEventsMethod.Input, field.JSONName) ww.ListEventsRequestFilter = append(ww.ListEventsRequestFilter, ListFilterField{ - DBName: string(field.Name()), + DBName: string(genField.Desc.Name()), Getter: genField.GoName, - Optional: field.HasOptionalKeyword(), + Optional: genField.Desc.HasOptionalKeyword(), }) } @@ -267,7 +307,16 @@ func deriveStateDescriptorFromQueryDescriptor(src QueryServiceGenerateSet) (*psm } } - spec, err := psm.BuildQueryTableSpec(stateMessage, eventMessage) + stateObject, err := j5schema.Global.ObjectSchema(stateMessage) + if err != nil { + return nil, fmt.Errorf("j5schema.ObjectSchema for %s: %w", stateMessage.FullName(), err) + } + eventObject, err := j5schema.Global.ObjectSchema(eventMessage) + if err != nil { + return nil, fmt.Errorf("j5schema.ObjectSchema for %s: %w", eventMessage.FullName(), err) + } + + spec, err := psm.BuildQueryTableSpec(stateObject, eventObject) if err != nil { return nil, err } @@ -286,3 +335,12 @@ func mapGenField(parent *protogen.Message, field protoreflect.FieldDescriptor) * } panic(fmt.Sprintf("field %s not found in parent %s", field.FullName(), parent.Desc.FullName())) } + +func mapJSONField(parent *protogen.Message, field string) *protogen.Field { + for _, f := range parent.Fields { + if f.Desc.JSONName() == field { + return f + } + } + panic(fmt.Sprintf("field %s not found in parent %s", field, parent.Desc.FullName())) +} diff --git a/internal/protogen/state/code.go b/internal/protogen/state/code.go index 6fafc15..21ec31d 100644 --- a/internal/protogen/state/code.go +++ b/internal/protogen/state/code.go @@ -8,7 +8,6 @@ import ( "github.com/pentops/protostate/psm" "google.golang.org/protobuf/compiler/protogen" "google.golang.org/protobuf/proto" - "google.golang.org/protobuf/reflect/protoreflect" ) var ( @@ -155,7 +154,7 @@ func (ss PSMEntity) implementIKeyset(g *protogen.GeneratedFile) { if !columnSpec.Primary { continue } - field := fieldByDesc(ss.keyMessage.Fields, columnSpec.ProtoName) + field := fieldByDesc(ss.keyMessage.Fields, columnSpec.JSONFieldName) g.P(" \"", columnSpec.ColumnName, "\": msg.", field.GoName, ",") } g.P(" }") @@ -163,7 +162,7 @@ func (ss PSMEntity) implementIKeyset(g *protogen.GeneratedFile) { if columnSpec.Primary { continue } - field := fieldByDesc(ss.keyMessage.Fields, columnSpec.ProtoName) + field := fieldByDesc(ss.keyMessage.Fields, columnSpec.JSONFieldName) if columnSpec.ExplicitlyOptional { g.P("if msg.", field.GoName, " != nil {") g.P(" keyset[\"", columnSpec.ColumnName, "\"] = *msg.", field.GoName) @@ -357,9 +356,9 @@ func (ss PSMEntity) tableSpecAndConfig(g *protogen.GeneratedFile) { g.P() } -func fieldByDesc(fields []*protogen.Field, desc protoreflect.Name) *protogen.Field { +func fieldByDesc(fields []*protogen.Field, jsonName string) *protogen.Field { for _, f := range fields { - if f.Desc.Name() == desc { + if f.Desc.JSONName() == jsonName { return f } } diff --git a/internal/protogen/state/parse.go b/internal/protogen/state/parse.go index 3c20553..d2212a5 100644 --- a/internal/protogen/state/parse.go +++ b/internal/protogen/state/parse.go @@ -6,6 +6,7 @@ import ( "github.com/iancoleman/strcase" "github.com/pentops/j5/gen/j5/ext/v1/ext_j5pb" + "github.com/pentops/j5/lib/j5schema" "github.com/pentops/protostate/psm" "google.golang.org/protobuf/compiler/protogen" "google.golang.org/protobuf/proto" @@ -274,7 +275,16 @@ func BuildStateSet(src StateEntityGenerateSet) (*PSMEntity, error) { keyMessage: src.keyMessage, } - spec, err := psm.BuildQueryTableSpec(src.state.message.Desc, src.event.message.Desc) + state, err := j5schema.Global.ObjectSchema(src.state.message.Desc) + if err != nil { + return nil, fmt.Errorf("state object %s: %w", src.options.EntityName, err) + } + event, err := j5schema.Global.ObjectSchema(src.event.message.Desc) + if err != nil { + return nil, fmt.Errorf("event object %s: %w", src.options.EntityName, err) + } + + spec, err := psm.BuildQueryTableSpec(state, event) if err != nil { return nil, err } diff --git a/internal/testproto/gen/test/v1/test_pb/bar.j5s_j5.pb.go b/internal/testproto/gen/test/v1/test_pb/bar.j5s_j5.pb.go new file mode 100644 index 0000000..8493d3d --- /dev/null +++ b/internal/testproto/gen/test/v1/test_pb/bar.j5s_j5.pb.go @@ -0,0 +1,93 @@ +// Code generated by protoc-gen-go-j5. DO NOT EDIT. + +package test_pb + +import ( + j5reflect "github.com/pentops/j5/lib/j5reflect" + proto "google.golang.org/protobuf/proto" +) + +func (msg *BarKeys) J5Reflect() j5reflect.Root { + return j5reflect.MustReflect(msg.ProtoReflect()) +} + +func (msg *BarKeys) J5Object() j5reflect.Object { + return j5reflect.MustReflect(msg.ProtoReflect()).(j5reflect.Object) +} + +func (msg *BarKeys) Clone() any { + return proto.Clone(msg).(*BarKeys) +} +func (msg *BarData) J5Reflect() j5reflect.Root { + return j5reflect.MustReflect(msg.ProtoReflect()) +} + +func (msg *BarData) J5Object() j5reflect.Object { + return j5reflect.MustReflect(msg.ProtoReflect()).(j5reflect.Object) +} + +func (msg *BarData) Clone() any { + return proto.Clone(msg).(*BarData) +} +func (msg *BarState) J5Reflect() j5reflect.Root { + return j5reflect.MustReflect(msg.ProtoReflect()) +} + +func (msg *BarState) J5Object() j5reflect.Object { + return j5reflect.MustReflect(msg.ProtoReflect()).(j5reflect.Object) +} + +func (msg *BarState) Clone() any { + return proto.Clone(msg).(*BarState) +} +func (msg *BarEventType) J5Reflect() j5reflect.Root { + return j5reflect.MustReflect(msg.ProtoReflect()) +} + +func (msg *BarEventType) Clone() any { + return proto.Clone(msg).(*BarEventType) +} +func (msg *BarEventType_Created) J5Reflect() j5reflect.Root { + return j5reflect.MustReflect(msg.ProtoReflect()) +} + +func (msg *BarEventType_Created) J5Object() j5reflect.Object { + return j5reflect.MustReflect(msg.ProtoReflect()).(j5reflect.Object) +} + +func (msg *BarEventType_Created) Clone() any { + return proto.Clone(msg).(*BarEventType_Created) +} +func (msg *BarEventType_Updated) J5Reflect() j5reflect.Root { + return j5reflect.MustReflect(msg.ProtoReflect()) +} + +func (msg *BarEventType_Updated) J5Object() j5reflect.Object { + return j5reflect.MustReflect(msg.ProtoReflect()).(j5reflect.Object) +} + +func (msg *BarEventType_Updated) Clone() any { + return proto.Clone(msg).(*BarEventType_Updated) +} +func (msg *BarEventType_Deleted) J5Reflect() j5reflect.Root { + return j5reflect.MustReflect(msg.ProtoReflect()) +} + +func (msg *BarEventType_Deleted) J5Object() j5reflect.Object { + return j5reflect.MustReflect(msg.ProtoReflect()).(j5reflect.Object) +} + +func (msg *BarEventType_Deleted) Clone() any { + return proto.Clone(msg).(*BarEventType_Deleted) +} +func (msg *BarEvent) J5Reflect() j5reflect.Root { + return j5reflect.MustReflect(msg.ProtoReflect()) +} + +func (msg *BarEvent) J5Object() j5reflect.Object { + return j5reflect.MustReflect(msg.ProtoReflect()).(j5reflect.Object) +} + +func (msg *BarEvent) Clone() any { + return proto.Clone(msg).(*BarEvent) +} diff --git a/internal/testproto/gen/test/v1/test_pb/foo.j5s.pb.go b/internal/testproto/gen/test/v1/test_pb/foo.j5s.pb.go index 47cbb18..6f2d3bd 100644 --- a/internal/testproto/gen/test/v1/test_pb/foo.j5s.pb.go +++ b/internal/testproto/gen/test/v1/test_pb/foo.j5s.pb.go @@ -144,8 +144,9 @@ type FooData struct { Name string `protobuf:"bytes,1,opt,name=name,proto3" json:"name,omitempty"` Field string `protobuf:"bytes,2,opt,name=field,proto3" json:"field,omitempty"` Description *string `protobuf:"bytes,3,opt,name=description,proto3,oneof" json:"description,omitempty"` - Characteristics *FooCharacteristics `protobuf:"bytes,4,opt,name=characteristics,proto3" json:"characteristics,omitempty"` - Profiles []*FooProfile `protobuf:"bytes,5,rep,name=profiles,proto3" json:"profiles,omitempty"` + Shape *FooData_Shape `protobuf:"bytes,4,opt,name=shape,proto3" json:"shape,omitempty"` + Characteristics *FooCharacteristics `protobuf:"bytes,5,opt,name=characteristics,proto3" json:"characteristics,omitempty"` + Profiles []*FooProfile `protobuf:"bytes,6,rep,name=profiles,proto3" json:"profiles,omitempty"` } func (x *FooData) Reset() { @@ -201,6 +202,13 @@ func (x *FooData) GetDescription() string { return "" } +func (x *FooData) GetShape() *FooData_Shape { + if x != nil { + return x.Shape + } + return nil +} + func (x *FooData) GetCharacteristics() *FooCharacteristics { if x != nil { return x.Characteristics @@ -562,6 +570,181 @@ func (x *FooProfile) GetName() string { return "" } +type FooData_Shape struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + // Types that are assignable to Type: + // + // *FooData_Shape_Circle_ + // *FooData_Shape_Square_ + Type isFooData_Shape_Type `protobuf_oneof:"type"` +} + +func (x *FooData_Shape) Reset() { + *x = FooData_Shape{} + if protoimpl.UnsafeEnabled { + mi := &file_test_v1_foo_j5s_proto_msgTypes[7] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *FooData_Shape) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*FooData_Shape) ProtoMessage() {} + +func (x *FooData_Shape) ProtoReflect() protoreflect.Message { + mi := &file_test_v1_foo_j5s_proto_msgTypes[7] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use FooData_Shape.ProtoReflect.Descriptor instead. +func (*FooData_Shape) Descriptor() ([]byte, []int) { + return file_test_v1_foo_j5s_proto_rawDescGZIP(), []int{1, 0} +} + +func (m *FooData_Shape) GetType() isFooData_Shape_Type { + if m != nil { + return m.Type + } + return nil +} + +func (x *FooData_Shape) GetCircle() *FooData_Shape_Circle { + if x, ok := x.GetType().(*FooData_Shape_Circle_); ok { + return x.Circle + } + return nil +} + +func (x *FooData_Shape) GetSquare() *FooData_Shape_Square { + if x, ok := x.GetType().(*FooData_Shape_Square_); ok { + return x.Square + } + return nil +} + +type isFooData_Shape_Type interface { + isFooData_Shape_Type() +} + +type FooData_Shape_Circle_ struct { + Circle *FooData_Shape_Circle `protobuf:"bytes,1,opt,name=circle,proto3,oneof"` +} + +type FooData_Shape_Square_ struct { + Square *FooData_Shape_Square `protobuf:"bytes,2,opt,name=square,proto3,oneof"` +} + +func (*FooData_Shape_Circle_) isFooData_Shape_Type() {} + +func (*FooData_Shape_Square_) isFooData_Shape_Type() {} + +type FooData_Shape_Circle struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + Radius int64 `protobuf:"varint,1,opt,name=radius,proto3" json:"radius,omitempty"` +} + +func (x *FooData_Shape_Circle) Reset() { + *x = FooData_Shape_Circle{} + if protoimpl.UnsafeEnabled { + mi := &file_test_v1_foo_j5s_proto_msgTypes[8] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *FooData_Shape_Circle) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*FooData_Shape_Circle) ProtoMessage() {} + +func (x *FooData_Shape_Circle) ProtoReflect() protoreflect.Message { + mi := &file_test_v1_foo_j5s_proto_msgTypes[8] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use FooData_Shape_Circle.ProtoReflect.Descriptor instead. +func (*FooData_Shape_Circle) Descriptor() ([]byte, []int) { + return file_test_v1_foo_j5s_proto_rawDescGZIP(), []int{1, 0, 0} +} + +func (x *FooData_Shape_Circle) GetRadius() int64 { + if x != nil { + return x.Radius + } + return 0 +} + +type FooData_Shape_Square struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + Side int64 `protobuf:"varint,1,opt,name=side,proto3" json:"side,omitempty"` +} + +func (x *FooData_Shape_Square) Reset() { + *x = FooData_Shape_Square{} + if protoimpl.UnsafeEnabled { + mi := &file_test_v1_foo_j5s_proto_msgTypes[9] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *FooData_Shape_Square) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*FooData_Shape_Square) ProtoMessage() {} + +func (x *FooData_Shape_Square) ProtoReflect() protoreflect.Message { + mi := &file_test_v1_foo_j5s_proto_msgTypes[9] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use FooData_Shape_Square.ProtoReflect.Descriptor instead. +func (*FooData_Shape_Square) Descriptor() ([]byte, []int) { + return file_test_v1_foo_j5s_proto_rawDescGZIP(), []int{1, 0, 1} +} + +func (x *FooData_Shape_Square) GetSide() int64 { + if x != nil { + return x.Side + } + return 0 +} + type FooEventType_Created struct { state protoimpl.MessageState sizeCache protoimpl.SizeCache @@ -579,7 +762,7 @@ type FooEventType_Created struct { func (x *FooEventType_Created) Reset() { *x = FooEventType_Created{} if protoimpl.UnsafeEnabled { - mi := &file_test_v1_foo_j5s_proto_msgTypes[7] + mi := &file_test_v1_foo_j5s_proto_msgTypes[10] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -592,7 +775,7 @@ func (x *FooEventType_Created) String() string { func (*FooEventType_Created) ProtoMessage() {} func (x *FooEventType_Created) ProtoReflect() protoreflect.Message { - mi := &file_test_v1_foo_j5s_proto_msgTypes[7] + mi := &file_test_v1_foo_j5s_proto_msgTypes[10] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -675,7 +858,7 @@ type FooEventType_Updated struct { func (x *FooEventType_Updated) Reset() { *x = FooEventType_Updated{} if protoimpl.UnsafeEnabled { - mi := &file_test_v1_foo_j5s_proto_msgTypes[8] + mi := &file_test_v1_foo_j5s_proto_msgTypes[11] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -688,7 +871,7 @@ func (x *FooEventType_Updated) String() string { func (*FooEventType_Updated) ProtoMessage() {} func (x *FooEventType_Updated) ProtoReflect() protoreflect.Message { - mi := &file_test_v1_foo_j5s_proto_msgTypes[8] + mi := &file_test_v1_foo_j5s_proto_msgTypes[11] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -771,7 +954,7 @@ type FooEventType_Deleted struct { func (x *FooEventType_Deleted) Reset() { *x = FooEventType_Deleted{} if protoimpl.UnsafeEnabled { - mi := &file_test_v1_foo_j5s_proto_msgTypes[9] + mi := &file_test_v1_foo_j5s_proto_msgTypes[12] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -784,7 +967,7 @@ func (x *FooEventType_Deleted) String() string { func (*FooEventType_Deleted) ProtoMessage() {} func (x *FooEventType_Deleted) ProtoReflect() protoreflect.Message { - mi := &file_test_v1_foo_j5s_proto_msgTypes[9] + mi := &file_test_v1_foo_j5s_proto_msgTypes[12] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -836,7 +1019,7 @@ var file_test_v1_foo_j5s_proto_rawDesc = []byte{ 0x08, 0x1a, 0x06, 0x12, 0x04, 0x52, 0x02, 0x08, 0x01, 0x52, 0x0c, 0x6d, 0x65, 0x74, 0x61, 0x54, 0x65, 0x6e, 0x61, 0x6e, 0x74, 0x49, 0x64, 0x3a, 0x13, 0xc2, 0xff, 0x8e, 0x02, 0x02, 0x52, 0x00, 0xea, 0x85, 0x8f, 0x02, 0x07, 0x0a, 0x03, 0x66, 0x6f, 0x6f, 0x10, 0x01, 0x42, 0x0c, 0x0a, 0x0a, - 0x5f, 0x74, 0x65, 0x6e, 0x61, 0x6e, 0x74, 0x5f, 0x69, 0x64, 0x22, 0xf8, 0x02, 0x0a, 0x07, 0x46, + 0x5f, 0x74, 0x65, 0x6e, 0x61, 0x6e, 0x74, 0x5f, 0x69, 0x64, 0x22, 0xe1, 0x05, 0x0a, 0x07, 0x46, 0x6f, 0x6f, 0x44, 0x61, 0x74, 0x61, 0x12, 0x34, 0x0a, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x42, 0x20, 0xc2, 0xff, 0x8e, 0x02, 0x03, 0xf2, 0x01, 0x00, 0x8a, 0xf7, 0x98, 0xc6, 0x02, 0x12, 0x72, 0x10, 0x0a, 0x0e, 0x52, 0x0c, 0x08, 0x01, 0x12, 0x08, 0x74, 0x73, @@ -849,147 +1032,169 @@ var file_test_v1_foo_j5s_proto_rawDesc = []byte{ 0x03, 0xf2, 0x01, 0x00, 0x8a, 0xf7, 0x98, 0xc6, 0x02, 0x19, 0x72, 0x17, 0x0a, 0x15, 0x52, 0x13, 0x08, 0x01, 0x12, 0x0f, 0x74, 0x73, 0x76, 0x5f, 0x64, 0x65, 0x73, 0x63, 0x72, 0x69, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x48, 0x00, 0x52, 0x0b, 0x64, 0x65, 0x73, 0x63, 0x72, 0x69, 0x70, 0x74, 0x69, - 0x6f, 0x6e, 0x88, 0x01, 0x01, 0x12, 0x4e, 0x0a, 0x0f, 0x63, 0x68, 0x61, 0x72, 0x61, 0x63, 0x74, - 0x65, 0x72, 0x69, 0x73, 0x74, 0x69, 0x63, 0x73, 0x18, 0x04, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1b, - 0x2e, 0x74, 0x65, 0x73, 0x74, 0x2e, 0x76, 0x31, 0x2e, 0x46, 0x6f, 0x6f, 0x43, 0x68, 0x61, 0x72, - 0x61, 0x63, 0x74, 0x65, 0x72, 0x69, 0x73, 0x74, 0x69, 0x63, 0x73, 0x42, 0x07, 0xc2, 0xff, 0x8e, - 0x02, 0x02, 0x52, 0x00, 0x52, 0x0f, 0x63, 0x68, 0x61, 0x72, 0x61, 0x63, 0x74, 0x65, 0x72, 0x69, - 0x73, 0x74, 0x69, 0x63, 0x73, 0x12, 0x39, 0x0a, 0x08, 0x70, 0x72, 0x6f, 0x66, 0x69, 0x6c, 0x65, - 0x73, 0x18, 0x05, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x13, 0x2e, 0x74, 0x65, 0x73, 0x74, 0x2e, 0x76, + 0x6f, 0x6e, 0x88, 0x01, 0x01, 0x12, 0x42, 0x0a, 0x05, 0x73, 0x68, 0x61, 0x70, 0x65, 0x18, 0x04, + 0x20, 0x01, 0x28, 0x0b, 0x32, 0x16, 0x2e, 0x74, 0x65, 0x73, 0x74, 0x2e, 0x76, 0x31, 0x2e, 0x46, + 0x6f, 0x6f, 0x44, 0x61, 0x74, 0x61, 0x2e, 0x53, 0x68, 0x61, 0x70, 0x65, 0x42, 0x14, 0xc2, 0xff, + 0x8e, 0x02, 0x02, 0x62, 0x00, 0x8a, 0xf7, 0x98, 0xc6, 0x02, 0x07, 0xaa, 0x01, 0x04, 0x52, 0x02, + 0x08, 0x01, 0x52, 0x05, 0x73, 0x68, 0x61, 0x70, 0x65, 0x12, 0x4e, 0x0a, 0x0f, 0x63, 0x68, 0x61, + 0x72, 0x61, 0x63, 0x74, 0x65, 0x72, 0x69, 0x73, 0x74, 0x69, 0x63, 0x73, 0x18, 0x05, 0x20, 0x01, + 0x28, 0x0b, 0x32, 0x1b, 0x2e, 0x74, 0x65, 0x73, 0x74, 0x2e, 0x76, 0x31, 0x2e, 0x46, 0x6f, 0x6f, + 0x43, 0x68, 0x61, 0x72, 0x61, 0x63, 0x74, 0x65, 0x72, 0x69, 0x73, 0x74, 0x69, 0x63, 0x73, 0x42, + 0x07, 0xc2, 0xff, 0x8e, 0x02, 0x02, 0x52, 0x00, 0x52, 0x0f, 0x63, 0x68, 0x61, 0x72, 0x61, 0x63, + 0x74, 0x65, 0x72, 0x69, 0x73, 0x74, 0x69, 0x63, 0x73, 0x12, 0x39, 0x0a, 0x08, 0x70, 0x72, 0x6f, + 0x66, 0x69, 0x6c, 0x65, 0x73, 0x18, 0x06, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x13, 0x2e, 0x74, 0x65, + 0x73, 0x74, 0x2e, 0x76, 0x31, 0x2e, 0x46, 0x6f, 0x6f, 0x50, 0x72, 0x6f, 0x66, 0x69, 0x6c, 0x65, + 0x42, 0x08, 0xc2, 0xff, 0x8e, 0x02, 0x03, 0xaa, 0x01, 0x00, 0x52, 0x08, 0x70, 0x72, 0x6f, 0x66, + 0x69, 0x6c, 0x65, 0x73, 0x1a, 0xa2, 0x02, 0x0a, 0x05, 0x53, 0x68, 0x61, 0x70, 0x65, 0x12, 0x40, + 0x0a, 0x06, 0x63, 0x69, 0x72, 0x63, 0x6c, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1d, + 0x2e, 0x74, 0x65, 0x73, 0x74, 0x2e, 0x76, 0x31, 0x2e, 0x46, 0x6f, 0x6f, 0x44, 0x61, 0x74, 0x61, + 0x2e, 0x53, 0x68, 0x61, 0x70, 0x65, 0x2e, 0x43, 0x69, 0x72, 0x63, 0x6c, 0x65, 0x42, 0x07, 0xc2, + 0xff, 0x8e, 0x02, 0x02, 0x52, 0x00, 0x48, 0x00, 0x52, 0x06, 0x63, 0x69, 0x72, 0x63, 0x6c, 0x65, + 0x12, 0x40, 0x0a, 0x06, 0x73, 0x71, 0x75, 0x61, 0x72, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, + 0x32, 0x1d, 0x2e, 0x74, 0x65, 0x73, 0x74, 0x2e, 0x76, 0x31, 0x2e, 0x46, 0x6f, 0x6f, 0x44, 0x61, + 0x74, 0x61, 0x2e, 0x53, 0x68, 0x61, 0x70, 0x65, 0x2e, 0x53, 0x71, 0x75, 0x61, 0x72, 0x65, 0x42, + 0x07, 0xc2, 0xff, 0x8e, 0x02, 0x02, 0x52, 0x00, 0x48, 0x00, 0x52, 0x06, 0x73, 0x71, 0x75, 0x61, + 0x72, 0x65, 0x1a, 0x43, 0x0a, 0x06, 0x43, 0x69, 0x72, 0x63, 0x6c, 0x65, 0x12, 0x30, 0x0a, 0x06, + 0x72, 0x61, 0x64, 0x69, 0x75, 0x73, 0x18, 0x01, 0x20, 0x01, 0x28, 0x03, 0x42, 0x18, 0xc2, 0xff, + 0x8e, 0x02, 0x03, 0xfa, 0x01, 0x00, 0x8a, 0xf7, 0x98, 0xc6, 0x02, 0x0a, 0x32, 0x08, 0x52, 0x02, + 0x08, 0x01, 0x5a, 0x02, 0x08, 0x01, 0x52, 0x06, 0x72, 0x61, 0x64, 0x69, 0x75, 0x73, 0x3a, 0x07, + 0xc2, 0xff, 0x8e, 0x02, 0x02, 0x52, 0x00, 0x1a, 0x3f, 0x0a, 0x06, 0x53, 0x71, 0x75, 0x61, 0x72, + 0x65, 0x12, 0x2c, 0x0a, 0x04, 0x73, 0x69, 0x64, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x03, 0x42, + 0x18, 0xc2, 0xff, 0x8e, 0x02, 0x03, 0xfa, 0x01, 0x00, 0x8a, 0xf7, 0x98, 0xc6, 0x02, 0x0a, 0x32, + 0x08, 0x52, 0x02, 0x08, 0x01, 0x5a, 0x02, 0x08, 0x01, 0x52, 0x04, 0x73, 0x69, 0x64, 0x65, 0x3a, + 0x07, 0xc2, 0xff, 0x8e, 0x02, 0x02, 0x52, 0x00, 0x3a, 0x07, 0xc2, 0xff, 0x8e, 0x02, 0x02, 0x5a, + 0x00, 0x42, 0x06, 0x0a, 0x04, 0x74, 0x79, 0x70, 0x65, 0x3a, 0x13, 0xc2, 0xff, 0x8e, 0x02, 0x02, + 0x52, 0x00, 0xea, 0x85, 0x8f, 0x02, 0x07, 0x0a, 0x03, 0x66, 0x6f, 0x6f, 0x10, 0x04, 0x42, 0x0e, + 0x0a, 0x0c, 0x5f, 0x64, 0x65, 0x73, 0x63, 0x72, 0x69, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x22, 0xb2, + 0x02, 0x0a, 0x08, 0x46, 0x6f, 0x6f, 0x53, 0x74, 0x61, 0x74, 0x65, 0x12, 0x45, 0x0a, 0x08, 0x6d, + 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1a, 0x2e, + 0x6a, 0x35, 0x2e, 0x73, 0x74, 0x61, 0x74, 0x65, 0x2e, 0x76, 0x31, 0x2e, 0x53, 0x74, 0x61, 0x74, + 0x65, 0x4d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x42, 0x0d, 0xba, 0x48, 0x03, 0xc8, 0x01, + 0x01, 0xc2, 0xff, 0x8e, 0x02, 0x02, 0x52, 0x00, 0x52, 0x08, 0x6d, 0x65, 0x74, 0x61, 0x64, 0x61, + 0x74, 0x61, 0x12, 0x35, 0x0a, 0x04, 0x6b, 0x65, 0x79, 0x73, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, + 0x32, 0x10, 0x2e, 0x74, 0x65, 0x73, 0x74, 0x2e, 0x76, 0x31, 0x2e, 0x46, 0x6f, 0x6f, 0x4b, 0x65, + 0x79, 0x73, 0x42, 0x0f, 0xba, 0x48, 0x03, 0xc8, 0x01, 0x01, 0xc2, 0xff, 0x8e, 0x02, 0x04, 0x52, + 0x02, 0x08, 0x01, 0x52, 0x04, 0x6b, 0x65, 0x79, 0x73, 0x12, 0x33, 0x0a, 0x04, 0x64, 0x61, 0x74, + 0x61, 0x18, 0x03, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x10, 0x2e, 0x74, 0x65, 0x73, 0x74, 0x2e, 0x76, + 0x31, 0x2e, 0x46, 0x6f, 0x6f, 0x44, 0x61, 0x74, 0x61, 0x42, 0x0d, 0xba, 0x48, 0x03, 0xc8, 0x01, + 0x01, 0xc2, 0xff, 0x8e, 0x02, 0x02, 0x52, 0x00, 0x52, 0x04, 0x64, 0x61, 0x74, 0x61, 0x12, 0x5e, + 0x0a, 0x06, 0x73, 0x74, 0x61, 0x74, 0x75, 0x73, 0x18, 0x04, 0x20, 0x01, 0x28, 0x0e, 0x32, 0x12, + 0x2e, 0x74, 0x65, 0x73, 0x74, 0x2e, 0x76, 0x31, 0x2e, 0x46, 0x6f, 0x6f, 0x53, 0x74, 0x61, 0x74, + 0x75, 0x73, 0x42, 0x32, 0xba, 0x48, 0x08, 0xc8, 0x01, 0x01, 0x82, 0x01, 0x02, 0x10, 0x01, 0xc2, + 0xff, 0x8e, 0x02, 0x02, 0x5a, 0x00, 0x8a, 0xf7, 0x98, 0xc6, 0x02, 0x1a, 0xa2, 0x01, 0x17, 0x52, + 0x15, 0x08, 0x01, 0x12, 0x11, 0x46, 0x4f, 0x4f, 0x5f, 0x53, 0x54, 0x41, 0x54, 0x55, 0x53, 0x5f, + 0x41, 0x43, 0x54, 0x49, 0x56, 0x45, 0x52, 0x06, 0x73, 0x74, 0x61, 0x74, 0x75, 0x73, 0x3a, 0x13, + 0xc2, 0xff, 0x8e, 0x02, 0x02, 0x52, 0x00, 0xea, 0x85, 0x8f, 0x02, 0x07, 0x0a, 0x03, 0x66, 0x6f, + 0x6f, 0x10, 0x02, 0x22, 0x8d, 0x08, 0x0a, 0x0c, 0x46, 0x6f, 0x6f, 0x45, 0x76, 0x65, 0x6e, 0x74, + 0x54, 0x79, 0x70, 0x65, 0x12, 0x42, 0x0a, 0x07, 0x63, 0x72, 0x65, 0x61, 0x74, 0x65, 0x64, 0x18, + 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1d, 0x2e, 0x74, 0x65, 0x73, 0x74, 0x2e, 0x76, 0x31, 0x2e, + 0x46, 0x6f, 0x6f, 0x45, 0x76, 0x65, 0x6e, 0x74, 0x54, 0x79, 0x70, 0x65, 0x2e, 0x43, 0x72, 0x65, + 0x61, 0x74, 0x65, 0x64, 0x42, 0x07, 0xc2, 0xff, 0x8e, 0x02, 0x02, 0x52, 0x00, 0x48, 0x00, 0x52, + 0x07, 0x63, 0x72, 0x65, 0x61, 0x74, 0x65, 0x64, 0x12, 0x42, 0x0a, 0x07, 0x75, 0x70, 0x64, 0x61, + 0x74, 0x65, 0x64, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1d, 0x2e, 0x74, 0x65, 0x73, 0x74, + 0x2e, 0x76, 0x31, 0x2e, 0x46, 0x6f, 0x6f, 0x45, 0x76, 0x65, 0x6e, 0x74, 0x54, 0x79, 0x70, 0x65, + 0x2e, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x64, 0x42, 0x07, 0xc2, 0xff, 0x8e, 0x02, 0x02, 0x52, + 0x00, 0x48, 0x00, 0x52, 0x07, 0x75, 0x70, 0x64, 0x61, 0x74, 0x65, 0x64, 0x12, 0x42, 0x0a, 0x07, + 0x64, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x64, 0x18, 0x03, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1d, 0x2e, + 0x74, 0x65, 0x73, 0x74, 0x2e, 0x76, 0x31, 0x2e, 0x46, 0x6f, 0x6f, 0x45, 0x76, 0x65, 0x6e, 0x74, + 0x54, 0x79, 0x70, 0x65, 0x2e, 0x44, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x64, 0x42, 0x07, 0xc2, 0xff, + 0x8e, 0x02, 0x02, 0x52, 0x00, 0x48, 0x00, 0x52, 0x07, 0x64, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x64, + 0x1a, 0xe2, 0x02, 0x0a, 0x07, 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, 0x64, 0x12, 0x1c, 0x0a, 0x04, + 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x42, 0x08, 0xc2, 0xff, 0x8e, 0x02, + 0x03, 0xf2, 0x01, 0x00, 0x52, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x12, 0x1e, 0x0a, 0x05, 0x66, 0x69, + 0x65, 0x6c, 0x64, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x42, 0x08, 0xc2, 0xff, 0x8e, 0x02, 0x03, + 0xf2, 0x01, 0x00, 0x52, 0x05, 0x66, 0x69, 0x65, 0x6c, 0x64, 0x12, 0x2f, 0x0a, 0x0b, 0x64, 0x65, + 0x73, 0x63, 0x72, 0x69, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x42, + 0x08, 0xc2, 0xff, 0x8e, 0x02, 0x03, 0xf2, 0x01, 0x00, 0x48, 0x00, 0x52, 0x0b, 0x64, 0x65, 0x73, + 0x63, 0x72, 0x69, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x88, 0x01, 0x01, 0x12, 0x25, 0x0a, 0x06, 0x77, + 0x65, 0x69, 0x67, 0x68, 0x74, 0x18, 0x04, 0x20, 0x01, 0x28, 0x03, 0x42, 0x08, 0xc2, 0xff, 0x8e, + 0x02, 0x03, 0xfa, 0x01, 0x00, 0x48, 0x01, 0x52, 0x06, 0x77, 0x65, 0x69, 0x67, 0x68, 0x74, 0x88, + 0x01, 0x01, 0x12, 0x25, 0x0a, 0x06, 0x68, 0x65, 0x69, 0x67, 0x68, 0x74, 0x18, 0x05, 0x20, 0x01, + 0x28, 0x03, 0x42, 0x08, 0xc2, 0xff, 0x8e, 0x02, 0x03, 0xfa, 0x01, 0x00, 0x48, 0x02, 0x52, 0x06, + 0x68, 0x65, 0x69, 0x67, 0x68, 0x74, 0x88, 0x01, 0x01, 0x12, 0x25, 0x0a, 0x06, 0x6c, 0x65, 0x6e, + 0x67, 0x74, 0x68, 0x18, 0x06, 0x20, 0x01, 0x28, 0x03, 0x42, 0x08, 0xc2, 0xff, 0x8e, 0x02, 0x03, + 0xfa, 0x01, 0x00, 0x48, 0x03, 0x52, 0x06, 0x6c, 0x65, 0x6e, 0x67, 0x74, 0x68, 0x88, 0x01, 0x01, + 0x12, 0x39, 0x0a, 0x08, 0x70, 0x72, 0x6f, 0x66, 0x69, 0x6c, 0x65, 0x73, 0x18, 0x07, 0x20, 0x03, + 0x28, 0x0b, 0x32, 0x13, 0x2e, 0x74, 0x65, 0x73, 0x74, 0x2e, 0x76, 0x31, 0x2e, 0x46, 0x6f, 0x6f, + 0x50, 0x72, 0x6f, 0x66, 0x69, 0x6c, 0x65, 0x42, 0x08, 0xc2, 0xff, 0x8e, 0x02, 0x03, 0xaa, 0x01, + 0x00, 0x52, 0x08, 0x70, 0x72, 0x6f, 0x66, 0x69, 0x6c, 0x65, 0x73, 0x3a, 0x07, 0xc2, 0xff, 0x8e, + 0x02, 0x02, 0x52, 0x00, 0x42, 0x0e, 0x0a, 0x0c, 0x5f, 0x64, 0x65, 0x73, 0x63, 0x72, 0x69, 0x70, + 0x74, 0x69, 0x6f, 0x6e, 0x42, 0x09, 0x0a, 0x07, 0x5f, 0x77, 0x65, 0x69, 0x67, 0x68, 0x74, 0x42, + 0x09, 0x0a, 0x07, 0x5f, 0x68, 0x65, 0x69, 0x67, 0x68, 0x74, 0x42, 0x09, 0x0a, 0x07, 0x5f, 0x6c, + 0x65, 0x6e, 0x67, 0x74, 0x68, 0x1a, 0x84, 0x03, 0x0a, 0x07, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, + 0x64, 0x12, 0x1c, 0x0a, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x42, + 0x08, 0xc2, 0xff, 0x8e, 0x02, 0x03, 0xf2, 0x01, 0x00, 0x52, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x12, + 0x1e, 0x0a, 0x05, 0x66, 0x69, 0x65, 0x6c, 0x64, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x42, 0x08, + 0xc2, 0xff, 0x8e, 0x02, 0x03, 0xf2, 0x01, 0x00, 0x52, 0x05, 0x66, 0x69, 0x65, 0x6c, 0x64, 0x12, + 0x2f, 0x0a, 0x0b, 0x64, 0x65, 0x73, 0x63, 0x72, 0x69, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x18, 0x03, + 0x20, 0x01, 0x28, 0x09, 0x42, 0x08, 0xc2, 0xff, 0x8e, 0x02, 0x03, 0xf2, 0x01, 0x00, 0x48, 0x00, + 0x52, 0x0b, 0x64, 0x65, 0x73, 0x63, 0x72, 0x69, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x88, 0x01, 0x01, + 0x12, 0x25, 0x0a, 0x06, 0x77, 0x65, 0x69, 0x67, 0x68, 0x74, 0x18, 0x04, 0x20, 0x01, 0x28, 0x03, + 0x42, 0x08, 0xc2, 0xff, 0x8e, 0x02, 0x03, 0xfa, 0x01, 0x00, 0x48, 0x01, 0x52, 0x06, 0x77, 0x65, + 0x69, 0x67, 0x68, 0x74, 0x88, 0x01, 0x01, 0x12, 0x25, 0x0a, 0x06, 0x68, 0x65, 0x69, 0x67, 0x68, + 0x74, 0x18, 0x05, 0x20, 0x01, 0x28, 0x03, 0x42, 0x08, 0xc2, 0xff, 0x8e, 0x02, 0x03, 0xfa, 0x01, + 0x00, 0x48, 0x02, 0x52, 0x06, 0x68, 0x65, 0x69, 0x67, 0x68, 0x74, 0x88, 0x01, 0x01, 0x12, 0x25, + 0x0a, 0x06, 0x6c, 0x65, 0x6e, 0x67, 0x74, 0x68, 0x18, 0x06, 0x20, 0x01, 0x28, 0x03, 0x42, 0x08, + 0xc2, 0xff, 0x8e, 0x02, 0x03, 0xfa, 0x01, 0x00, 0x48, 0x03, 0x52, 0x06, 0x6c, 0x65, 0x6e, 0x67, + 0x74, 0x68, 0x88, 0x01, 0x01, 0x12, 0x39, 0x0a, 0x08, 0x70, 0x72, 0x6f, 0x66, 0x69, 0x6c, 0x65, + 0x73, 0x18, 0x07, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x13, 0x2e, 0x74, 0x65, 0x73, 0x74, 0x2e, 0x76, 0x31, 0x2e, 0x46, 0x6f, 0x6f, 0x50, 0x72, 0x6f, 0x66, 0x69, 0x6c, 0x65, 0x42, 0x08, 0xc2, 0xff, 0x8e, 0x02, 0x03, 0xaa, 0x01, 0x00, 0x52, 0x08, 0x70, 0x72, 0x6f, 0x66, 0x69, 0x6c, 0x65, 0x73, - 0x3a, 0x13, 0xc2, 0xff, 0x8e, 0x02, 0x02, 0x52, 0x00, 0xea, 0x85, 0x8f, 0x02, 0x07, 0x0a, 0x03, - 0x66, 0x6f, 0x6f, 0x10, 0x04, 0x42, 0x0e, 0x0a, 0x0c, 0x5f, 0x64, 0x65, 0x73, 0x63, 0x72, 0x69, - 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x22, 0xb2, 0x02, 0x0a, 0x08, 0x46, 0x6f, 0x6f, 0x53, 0x74, 0x61, - 0x74, 0x65, 0x12, 0x45, 0x0a, 0x08, 0x6d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x18, 0x01, - 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1a, 0x2e, 0x6a, 0x35, 0x2e, 0x73, 0x74, 0x61, 0x74, 0x65, 0x2e, - 0x76, 0x31, 0x2e, 0x53, 0x74, 0x61, 0x74, 0x65, 0x4d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, - 0x42, 0x0d, 0xba, 0x48, 0x03, 0xc8, 0x01, 0x01, 0xc2, 0xff, 0x8e, 0x02, 0x02, 0x52, 0x00, 0x52, - 0x08, 0x6d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x12, 0x35, 0x0a, 0x04, 0x6b, 0x65, 0x79, - 0x73, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x10, 0x2e, 0x74, 0x65, 0x73, 0x74, 0x2e, 0x76, - 0x31, 0x2e, 0x46, 0x6f, 0x6f, 0x4b, 0x65, 0x79, 0x73, 0x42, 0x0f, 0xba, 0x48, 0x03, 0xc8, 0x01, - 0x01, 0xc2, 0xff, 0x8e, 0x02, 0x04, 0x52, 0x02, 0x08, 0x01, 0x52, 0x04, 0x6b, 0x65, 0x79, 0x73, - 0x12, 0x33, 0x0a, 0x04, 0x64, 0x61, 0x74, 0x61, 0x18, 0x03, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x10, - 0x2e, 0x74, 0x65, 0x73, 0x74, 0x2e, 0x76, 0x31, 0x2e, 0x46, 0x6f, 0x6f, 0x44, 0x61, 0x74, 0x61, - 0x42, 0x0d, 0xba, 0x48, 0x03, 0xc8, 0x01, 0x01, 0xc2, 0xff, 0x8e, 0x02, 0x02, 0x52, 0x00, 0x52, - 0x04, 0x64, 0x61, 0x74, 0x61, 0x12, 0x5e, 0x0a, 0x06, 0x73, 0x74, 0x61, 0x74, 0x75, 0x73, 0x18, - 0x04, 0x20, 0x01, 0x28, 0x0e, 0x32, 0x12, 0x2e, 0x74, 0x65, 0x73, 0x74, 0x2e, 0x76, 0x31, 0x2e, - 0x46, 0x6f, 0x6f, 0x53, 0x74, 0x61, 0x74, 0x75, 0x73, 0x42, 0x32, 0xba, 0x48, 0x08, 0xc8, 0x01, - 0x01, 0x82, 0x01, 0x02, 0x10, 0x01, 0xc2, 0xff, 0x8e, 0x02, 0x02, 0x5a, 0x00, 0x8a, 0xf7, 0x98, - 0xc6, 0x02, 0x1a, 0xa2, 0x01, 0x17, 0x52, 0x15, 0x08, 0x01, 0x12, 0x11, 0x46, 0x4f, 0x4f, 0x5f, - 0x53, 0x54, 0x41, 0x54, 0x55, 0x53, 0x5f, 0x41, 0x43, 0x54, 0x49, 0x56, 0x45, 0x52, 0x06, 0x73, - 0x74, 0x61, 0x74, 0x75, 0x73, 0x3a, 0x13, 0xc2, 0xff, 0x8e, 0x02, 0x02, 0x52, 0x00, 0xea, 0x85, - 0x8f, 0x02, 0x07, 0x0a, 0x03, 0x66, 0x6f, 0x6f, 0x10, 0x02, 0x22, 0x8d, 0x08, 0x0a, 0x0c, 0x46, - 0x6f, 0x6f, 0x45, 0x76, 0x65, 0x6e, 0x74, 0x54, 0x79, 0x70, 0x65, 0x12, 0x42, 0x0a, 0x07, 0x63, - 0x72, 0x65, 0x61, 0x74, 0x65, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1d, 0x2e, 0x74, - 0x65, 0x73, 0x74, 0x2e, 0x76, 0x31, 0x2e, 0x46, 0x6f, 0x6f, 0x45, 0x76, 0x65, 0x6e, 0x74, 0x54, - 0x79, 0x70, 0x65, 0x2e, 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, 0x64, 0x42, 0x07, 0xc2, 0xff, 0x8e, - 0x02, 0x02, 0x52, 0x00, 0x48, 0x00, 0x52, 0x07, 0x63, 0x72, 0x65, 0x61, 0x74, 0x65, 0x64, 0x12, - 0x42, 0x0a, 0x07, 0x75, 0x70, 0x64, 0x61, 0x74, 0x65, 0x64, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, - 0x32, 0x1d, 0x2e, 0x74, 0x65, 0x73, 0x74, 0x2e, 0x76, 0x31, 0x2e, 0x46, 0x6f, 0x6f, 0x45, 0x76, - 0x65, 0x6e, 0x74, 0x54, 0x79, 0x70, 0x65, 0x2e, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x64, 0x42, - 0x07, 0xc2, 0xff, 0x8e, 0x02, 0x02, 0x52, 0x00, 0x48, 0x00, 0x52, 0x07, 0x75, 0x70, 0x64, 0x61, - 0x74, 0x65, 0x64, 0x12, 0x42, 0x0a, 0x07, 0x64, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x64, 0x18, 0x03, - 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1d, 0x2e, 0x74, 0x65, 0x73, 0x74, 0x2e, 0x76, 0x31, 0x2e, 0x46, - 0x6f, 0x6f, 0x45, 0x76, 0x65, 0x6e, 0x74, 0x54, 0x79, 0x70, 0x65, 0x2e, 0x44, 0x65, 0x6c, 0x65, - 0x74, 0x65, 0x64, 0x42, 0x07, 0xc2, 0xff, 0x8e, 0x02, 0x02, 0x52, 0x00, 0x48, 0x00, 0x52, 0x07, - 0x64, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x64, 0x1a, 0xe2, 0x02, 0x0a, 0x07, 0x43, 0x72, 0x65, 0x61, - 0x74, 0x65, 0x64, 0x12, 0x1c, 0x0a, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, - 0x09, 0x42, 0x08, 0xc2, 0xff, 0x8e, 0x02, 0x03, 0xf2, 0x01, 0x00, 0x52, 0x04, 0x6e, 0x61, 0x6d, - 0x65, 0x12, 0x1e, 0x0a, 0x05, 0x66, 0x69, 0x65, 0x6c, 0x64, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, - 0x42, 0x08, 0xc2, 0xff, 0x8e, 0x02, 0x03, 0xf2, 0x01, 0x00, 0x52, 0x05, 0x66, 0x69, 0x65, 0x6c, - 0x64, 0x12, 0x2f, 0x0a, 0x0b, 0x64, 0x65, 0x73, 0x63, 0x72, 0x69, 0x70, 0x74, 0x69, 0x6f, 0x6e, - 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x42, 0x08, 0xc2, 0xff, 0x8e, 0x02, 0x03, 0xf2, 0x01, 0x00, - 0x48, 0x00, 0x52, 0x0b, 0x64, 0x65, 0x73, 0x63, 0x72, 0x69, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x88, - 0x01, 0x01, 0x12, 0x25, 0x0a, 0x06, 0x77, 0x65, 0x69, 0x67, 0x68, 0x74, 0x18, 0x04, 0x20, 0x01, - 0x28, 0x03, 0x42, 0x08, 0xc2, 0xff, 0x8e, 0x02, 0x03, 0xfa, 0x01, 0x00, 0x48, 0x01, 0x52, 0x06, - 0x77, 0x65, 0x69, 0x67, 0x68, 0x74, 0x88, 0x01, 0x01, 0x12, 0x25, 0x0a, 0x06, 0x68, 0x65, 0x69, - 0x67, 0x68, 0x74, 0x18, 0x05, 0x20, 0x01, 0x28, 0x03, 0x42, 0x08, 0xc2, 0xff, 0x8e, 0x02, 0x03, - 0xfa, 0x01, 0x00, 0x48, 0x02, 0x52, 0x06, 0x68, 0x65, 0x69, 0x67, 0x68, 0x74, 0x88, 0x01, 0x01, - 0x12, 0x25, 0x0a, 0x06, 0x6c, 0x65, 0x6e, 0x67, 0x74, 0x68, 0x18, 0x06, 0x20, 0x01, 0x28, 0x03, - 0x42, 0x08, 0xc2, 0xff, 0x8e, 0x02, 0x03, 0xfa, 0x01, 0x00, 0x48, 0x03, 0x52, 0x06, 0x6c, 0x65, - 0x6e, 0x67, 0x74, 0x68, 0x88, 0x01, 0x01, 0x12, 0x39, 0x0a, 0x08, 0x70, 0x72, 0x6f, 0x66, 0x69, - 0x6c, 0x65, 0x73, 0x18, 0x07, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x13, 0x2e, 0x74, 0x65, 0x73, 0x74, - 0x2e, 0x76, 0x31, 0x2e, 0x46, 0x6f, 0x6f, 0x50, 0x72, 0x6f, 0x66, 0x69, 0x6c, 0x65, 0x42, 0x08, - 0xc2, 0xff, 0x8e, 0x02, 0x03, 0xaa, 0x01, 0x00, 0x52, 0x08, 0x70, 0x72, 0x6f, 0x66, 0x69, 0x6c, - 0x65, 0x73, 0x3a, 0x07, 0xc2, 0xff, 0x8e, 0x02, 0x02, 0x52, 0x00, 0x42, 0x0e, 0x0a, 0x0c, 0x5f, + 0x12, 0x20, 0x0a, 0x06, 0x64, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x18, 0x08, 0x20, 0x01, 0x28, 0x08, + 0x42, 0x08, 0xc2, 0xff, 0x8e, 0x02, 0x03, 0x8a, 0x02, 0x00, 0x52, 0x06, 0x64, 0x65, 0x6c, 0x65, + 0x74, 0x65, 0x3a, 0x07, 0xc2, 0xff, 0x8e, 0x02, 0x02, 0x52, 0x00, 0x42, 0x0e, 0x0a, 0x0c, 0x5f, 0x64, 0x65, 0x73, 0x63, 0x72, 0x69, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x42, 0x09, 0x0a, 0x07, 0x5f, 0x77, 0x65, 0x69, 0x67, 0x68, 0x74, 0x42, 0x09, 0x0a, 0x07, 0x5f, 0x68, 0x65, 0x69, 0x67, 0x68, - 0x74, 0x42, 0x09, 0x0a, 0x07, 0x5f, 0x6c, 0x65, 0x6e, 0x67, 0x74, 0x68, 0x1a, 0x84, 0x03, 0x0a, - 0x07, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x64, 0x12, 0x1c, 0x0a, 0x04, 0x6e, 0x61, 0x6d, 0x65, - 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x42, 0x08, 0xc2, 0xff, 0x8e, 0x02, 0x03, 0xf2, 0x01, 0x00, - 0x52, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x12, 0x1e, 0x0a, 0x05, 0x66, 0x69, 0x65, 0x6c, 0x64, 0x18, - 0x02, 0x20, 0x01, 0x28, 0x09, 0x42, 0x08, 0xc2, 0xff, 0x8e, 0x02, 0x03, 0xf2, 0x01, 0x00, 0x52, - 0x05, 0x66, 0x69, 0x65, 0x6c, 0x64, 0x12, 0x2f, 0x0a, 0x0b, 0x64, 0x65, 0x73, 0x63, 0x72, 0x69, - 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x42, 0x08, 0xc2, 0xff, 0x8e, - 0x02, 0x03, 0xf2, 0x01, 0x00, 0x48, 0x00, 0x52, 0x0b, 0x64, 0x65, 0x73, 0x63, 0x72, 0x69, 0x70, - 0x74, 0x69, 0x6f, 0x6e, 0x88, 0x01, 0x01, 0x12, 0x25, 0x0a, 0x06, 0x77, 0x65, 0x69, 0x67, 0x68, - 0x74, 0x18, 0x04, 0x20, 0x01, 0x28, 0x03, 0x42, 0x08, 0xc2, 0xff, 0x8e, 0x02, 0x03, 0xfa, 0x01, - 0x00, 0x48, 0x01, 0x52, 0x06, 0x77, 0x65, 0x69, 0x67, 0x68, 0x74, 0x88, 0x01, 0x01, 0x12, 0x25, - 0x0a, 0x06, 0x68, 0x65, 0x69, 0x67, 0x68, 0x74, 0x18, 0x05, 0x20, 0x01, 0x28, 0x03, 0x42, 0x08, - 0xc2, 0xff, 0x8e, 0x02, 0x03, 0xfa, 0x01, 0x00, 0x48, 0x02, 0x52, 0x06, 0x68, 0x65, 0x69, 0x67, - 0x68, 0x74, 0x88, 0x01, 0x01, 0x12, 0x25, 0x0a, 0x06, 0x6c, 0x65, 0x6e, 0x67, 0x74, 0x68, 0x18, - 0x06, 0x20, 0x01, 0x28, 0x03, 0x42, 0x08, 0xc2, 0xff, 0x8e, 0x02, 0x03, 0xfa, 0x01, 0x00, 0x48, - 0x03, 0x52, 0x06, 0x6c, 0x65, 0x6e, 0x67, 0x74, 0x68, 0x88, 0x01, 0x01, 0x12, 0x39, 0x0a, 0x08, - 0x70, 0x72, 0x6f, 0x66, 0x69, 0x6c, 0x65, 0x73, 0x18, 0x07, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x13, - 0x2e, 0x74, 0x65, 0x73, 0x74, 0x2e, 0x76, 0x31, 0x2e, 0x46, 0x6f, 0x6f, 0x50, 0x72, 0x6f, 0x66, - 0x69, 0x6c, 0x65, 0x42, 0x08, 0xc2, 0xff, 0x8e, 0x02, 0x03, 0xaa, 0x01, 0x00, 0x52, 0x08, 0x70, - 0x72, 0x6f, 0x66, 0x69, 0x6c, 0x65, 0x73, 0x12, 0x20, 0x0a, 0x06, 0x64, 0x65, 0x6c, 0x65, 0x74, - 0x65, 0x18, 0x08, 0x20, 0x01, 0x28, 0x08, 0x42, 0x08, 0xc2, 0xff, 0x8e, 0x02, 0x03, 0x8a, 0x02, + 0x74, 0x42, 0x09, 0x0a, 0x07, 0x5f, 0x6c, 0x65, 0x6e, 0x67, 0x74, 0x68, 0x1a, 0x34, 0x0a, 0x07, + 0x44, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x64, 0x12, 0x20, 0x0a, 0x06, 0x64, 0x65, 0x6c, 0x65, 0x74, + 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x08, 0x42, 0x08, 0xc2, 0xff, 0x8e, 0x02, 0x03, 0x8a, 0x02, 0x00, 0x52, 0x06, 0x64, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x3a, 0x07, 0xc2, 0xff, 0x8e, 0x02, 0x02, - 0x52, 0x00, 0x42, 0x0e, 0x0a, 0x0c, 0x5f, 0x64, 0x65, 0x73, 0x63, 0x72, 0x69, 0x70, 0x74, 0x69, - 0x6f, 0x6e, 0x42, 0x09, 0x0a, 0x07, 0x5f, 0x77, 0x65, 0x69, 0x67, 0x68, 0x74, 0x42, 0x09, 0x0a, - 0x07, 0x5f, 0x68, 0x65, 0x69, 0x67, 0x68, 0x74, 0x42, 0x09, 0x0a, 0x07, 0x5f, 0x6c, 0x65, 0x6e, - 0x67, 0x74, 0x68, 0x1a, 0x34, 0x0a, 0x07, 0x44, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x64, 0x12, 0x20, - 0x0a, 0x06, 0x64, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x08, 0x42, 0x08, - 0xc2, 0xff, 0x8e, 0x02, 0x03, 0x8a, 0x02, 0x00, 0x52, 0x06, 0x64, 0x65, 0x6c, 0x65, 0x74, 0x65, - 0x3a, 0x07, 0xc2, 0xff, 0x8e, 0x02, 0x02, 0x52, 0x00, 0x3a, 0x07, 0xc2, 0xff, 0x8e, 0x02, 0x02, - 0x5a, 0x00, 0x42, 0x06, 0x0a, 0x04, 0x74, 0x79, 0x70, 0x65, 0x22, 0xe6, 0x01, 0x0a, 0x08, 0x46, - 0x6f, 0x6f, 0x45, 0x76, 0x65, 0x6e, 0x74, 0x12, 0x45, 0x0a, 0x08, 0x6d, 0x65, 0x74, 0x61, 0x64, - 0x61, 0x74, 0x61, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1a, 0x2e, 0x6a, 0x35, 0x2e, 0x73, - 0x74, 0x61, 0x74, 0x65, 0x2e, 0x76, 0x31, 0x2e, 0x45, 0x76, 0x65, 0x6e, 0x74, 0x4d, 0x65, 0x74, - 0x61, 0x64, 0x61, 0x74, 0x61, 0x42, 0x0d, 0xba, 0x48, 0x03, 0xc8, 0x01, 0x01, 0xc2, 0xff, 0x8e, - 0x02, 0x02, 0x52, 0x00, 0x52, 0x08, 0x6d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x12, 0x35, - 0x0a, 0x04, 0x6b, 0x65, 0x79, 0x73, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x10, 0x2e, 0x74, - 0x65, 0x73, 0x74, 0x2e, 0x76, 0x31, 0x2e, 0x46, 0x6f, 0x6f, 0x4b, 0x65, 0x79, 0x73, 0x42, 0x0f, - 0xba, 0x48, 0x03, 0xc8, 0x01, 0x01, 0xc2, 0xff, 0x8e, 0x02, 0x04, 0x52, 0x02, 0x08, 0x01, 0x52, - 0x04, 0x6b, 0x65, 0x79, 0x73, 0x12, 0x47, 0x0a, 0x05, 0x65, 0x76, 0x65, 0x6e, 0x74, 0x18, 0x03, - 0x20, 0x01, 0x28, 0x0b, 0x32, 0x15, 0x2e, 0x74, 0x65, 0x73, 0x74, 0x2e, 0x76, 0x31, 0x2e, 0x46, - 0x6f, 0x6f, 0x45, 0x76, 0x65, 0x6e, 0x74, 0x54, 0x79, 0x70, 0x65, 0x42, 0x1a, 0xba, 0x48, 0x03, - 0xc8, 0x01, 0x01, 0xc2, 0xff, 0x8e, 0x02, 0x02, 0x62, 0x00, 0x8a, 0xf7, 0x98, 0xc6, 0x02, 0x07, - 0xaa, 0x01, 0x04, 0x52, 0x02, 0x08, 0x01, 0x52, 0x05, 0x65, 0x76, 0x65, 0x6e, 0x74, 0x3a, 0x13, - 0xc2, 0xff, 0x8e, 0x02, 0x02, 0x52, 0x00, 0xea, 0x85, 0x8f, 0x02, 0x07, 0x0a, 0x03, 0x66, 0x6f, - 0x6f, 0x10, 0x03, 0x22, 0xb3, 0x01, 0x0a, 0x12, 0x46, 0x6f, 0x6f, 0x43, 0x68, 0x61, 0x72, 0x61, - 0x63, 0x74, 0x65, 0x72, 0x69, 0x73, 0x74, 0x69, 0x63, 0x73, 0x12, 0x30, 0x0a, 0x06, 0x77, 0x65, - 0x69, 0x67, 0x68, 0x74, 0x18, 0x01, 0x20, 0x01, 0x28, 0x03, 0x42, 0x18, 0xc2, 0xff, 0x8e, 0x02, - 0x03, 0xfa, 0x01, 0x00, 0x8a, 0xf7, 0x98, 0xc6, 0x02, 0x0a, 0x32, 0x08, 0x52, 0x02, 0x08, 0x01, - 0x5a, 0x02, 0x08, 0x01, 0x52, 0x06, 0x77, 0x65, 0x69, 0x67, 0x68, 0x74, 0x12, 0x30, 0x0a, 0x06, - 0x68, 0x65, 0x69, 0x67, 0x68, 0x74, 0x18, 0x02, 0x20, 0x01, 0x28, 0x03, 0x42, 0x18, 0xc2, 0xff, - 0x8e, 0x02, 0x03, 0xfa, 0x01, 0x00, 0x8a, 0xf7, 0x98, 0xc6, 0x02, 0x0a, 0x32, 0x08, 0x52, 0x02, - 0x08, 0x01, 0x5a, 0x02, 0x08, 0x01, 0x52, 0x06, 0x68, 0x65, 0x69, 0x67, 0x68, 0x74, 0x12, 0x30, - 0x0a, 0x06, 0x6c, 0x65, 0x6e, 0x67, 0x74, 0x68, 0x18, 0x03, 0x20, 0x01, 0x28, 0x03, 0x42, 0x18, - 0xc2, 0xff, 0x8e, 0x02, 0x03, 0xfa, 0x01, 0x00, 0x8a, 0xf7, 0x98, 0xc6, 0x02, 0x0a, 0x32, 0x08, - 0x52, 0x02, 0x08, 0x01, 0x5a, 0x02, 0x08, 0x01, 0x52, 0x06, 0x6c, 0x65, 0x6e, 0x67, 0x74, 0x68, - 0x3a, 0x07, 0xc2, 0xff, 0x8e, 0x02, 0x02, 0x52, 0x00, 0x22, 0x7f, 0x0a, 0x0a, 0x46, 0x6f, 0x6f, - 0x50, 0x72, 0x6f, 0x66, 0x69, 0x6c, 0x65, 0x12, 0x2a, 0x0a, 0x05, 0x70, 0x6c, 0x61, 0x63, 0x65, - 0x18, 0x01, 0x20, 0x01, 0x28, 0x03, 0x42, 0x14, 0xc2, 0xff, 0x8e, 0x02, 0x03, 0xfa, 0x01, 0x00, - 0x8a, 0xf7, 0x98, 0xc6, 0x02, 0x06, 0x32, 0x04, 0x52, 0x02, 0x08, 0x01, 0x52, 0x05, 0x70, 0x6c, - 0x61, 0x63, 0x65, 0x12, 0x3c, 0x0a, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, - 0x09, 0x42, 0x28, 0xc2, 0xff, 0x8e, 0x02, 0x03, 0xf2, 0x01, 0x00, 0x8a, 0xf7, 0x98, 0xc6, 0x02, - 0x1a, 0x72, 0x18, 0x0a, 0x16, 0x52, 0x14, 0x08, 0x01, 0x12, 0x10, 0x74, 0x73, 0x76, 0x5f, 0x70, - 0x72, 0x6f, 0x66, 0x69, 0x6c, 0x65, 0x5f, 0x6e, 0x61, 0x6d, 0x65, 0x52, 0x04, 0x6e, 0x61, 0x6d, - 0x65, 0x3a, 0x07, 0xc2, 0xff, 0x8e, 0x02, 0x02, 0x52, 0x00, 0x2a, 0x56, 0x0a, 0x09, 0x46, 0x6f, - 0x6f, 0x53, 0x74, 0x61, 0x74, 0x75, 0x73, 0x12, 0x1a, 0x0a, 0x16, 0x46, 0x4f, 0x4f, 0x5f, 0x53, - 0x54, 0x41, 0x54, 0x55, 0x53, 0x5f, 0x55, 0x4e, 0x53, 0x50, 0x45, 0x43, 0x49, 0x46, 0x49, 0x45, - 0x44, 0x10, 0x00, 0x12, 0x15, 0x0a, 0x11, 0x46, 0x4f, 0x4f, 0x5f, 0x53, 0x54, 0x41, 0x54, 0x55, - 0x53, 0x5f, 0x41, 0x43, 0x54, 0x49, 0x56, 0x45, 0x10, 0x01, 0x12, 0x16, 0x0a, 0x12, 0x46, 0x4f, - 0x4f, 0x5f, 0x53, 0x54, 0x41, 0x54, 0x55, 0x53, 0x5f, 0x44, 0x45, 0x4c, 0x45, 0x54, 0x45, 0x44, - 0x10, 0x02, 0x42, 0x46, 0x5a, 0x44, 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, - 0x2f, 0x70, 0x65, 0x6e, 0x74, 0x6f, 0x70, 0x73, 0x2f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x73, 0x74, - 0x61, 0x74, 0x65, 0x2f, 0x69, 0x6e, 0x74, 0x65, 0x72, 0x6e, 0x61, 0x6c, 0x2f, 0x74, 0x65, 0x73, - 0x74, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2f, 0x67, 0x65, 0x6e, 0x2f, 0x74, 0x65, 0x73, 0x74, 0x2f, - 0x76, 0x31, 0x2f, 0x74, 0x65, 0x73, 0x74, 0x5f, 0x70, 0x62, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, - 0x6f, 0x33, + 0x52, 0x00, 0x3a, 0x07, 0xc2, 0xff, 0x8e, 0x02, 0x02, 0x5a, 0x00, 0x42, 0x06, 0x0a, 0x04, 0x74, + 0x79, 0x70, 0x65, 0x22, 0xe6, 0x01, 0x0a, 0x08, 0x46, 0x6f, 0x6f, 0x45, 0x76, 0x65, 0x6e, 0x74, + 0x12, 0x45, 0x0a, 0x08, 0x6d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x18, 0x01, 0x20, 0x01, + 0x28, 0x0b, 0x32, 0x1a, 0x2e, 0x6a, 0x35, 0x2e, 0x73, 0x74, 0x61, 0x74, 0x65, 0x2e, 0x76, 0x31, + 0x2e, 0x45, 0x76, 0x65, 0x6e, 0x74, 0x4d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x42, 0x0d, + 0xba, 0x48, 0x03, 0xc8, 0x01, 0x01, 0xc2, 0xff, 0x8e, 0x02, 0x02, 0x52, 0x00, 0x52, 0x08, 0x6d, + 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x12, 0x35, 0x0a, 0x04, 0x6b, 0x65, 0x79, 0x73, 0x18, + 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x10, 0x2e, 0x74, 0x65, 0x73, 0x74, 0x2e, 0x76, 0x31, 0x2e, + 0x46, 0x6f, 0x6f, 0x4b, 0x65, 0x79, 0x73, 0x42, 0x0f, 0xba, 0x48, 0x03, 0xc8, 0x01, 0x01, 0xc2, + 0xff, 0x8e, 0x02, 0x04, 0x52, 0x02, 0x08, 0x01, 0x52, 0x04, 0x6b, 0x65, 0x79, 0x73, 0x12, 0x47, + 0x0a, 0x05, 0x65, 0x76, 0x65, 0x6e, 0x74, 0x18, 0x03, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x15, 0x2e, + 0x74, 0x65, 0x73, 0x74, 0x2e, 0x76, 0x31, 0x2e, 0x46, 0x6f, 0x6f, 0x45, 0x76, 0x65, 0x6e, 0x74, + 0x54, 0x79, 0x70, 0x65, 0x42, 0x1a, 0xba, 0x48, 0x03, 0xc8, 0x01, 0x01, 0xc2, 0xff, 0x8e, 0x02, + 0x02, 0x62, 0x00, 0x8a, 0xf7, 0x98, 0xc6, 0x02, 0x07, 0xaa, 0x01, 0x04, 0x52, 0x02, 0x08, 0x01, + 0x52, 0x05, 0x65, 0x76, 0x65, 0x6e, 0x74, 0x3a, 0x13, 0xc2, 0xff, 0x8e, 0x02, 0x02, 0x52, 0x00, + 0xea, 0x85, 0x8f, 0x02, 0x07, 0x0a, 0x03, 0x66, 0x6f, 0x6f, 0x10, 0x03, 0x22, 0xb3, 0x01, 0x0a, + 0x12, 0x46, 0x6f, 0x6f, 0x43, 0x68, 0x61, 0x72, 0x61, 0x63, 0x74, 0x65, 0x72, 0x69, 0x73, 0x74, + 0x69, 0x63, 0x73, 0x12, 0x30, 0x0a, 0x06, 0x77, 0x65, 0x69, 0x67, 0x68, 0x74, 0x18, 0x01, 0x20, + 0x01, 0x28, 0x03, 0x42, 0x18, 0xc2, 0xff, 0x8e, 0x02, 0x03, 0xfa, 0x01, 0x00, 0x8a, 0xf7, 0x98, + 0xc6, 0x02, 0x0a, 0x32, 0x08, 0x52, 0x02, 0x08, 0x01, 0x5a, 0x02, 0x08, 0x01, 0x52, 0x06, 0x77, + 0x65, 0x69, 0x67, 0x68, 0x74, 0x12, 0x30, 0x0a, 0x06, 0x68, 0x65, 0x69, 0x67, 0x68, 0x74, 0x18, + 0x02, 0x20, 0x01, 0x28, 0x03, 0x42, 0x18, 0xc2, 0xff, 0x8e, 0x02, 0x03, 0xfa, 0x01, 0x00, 0x8a, + 0xf7, 0x98, 0xc6, 0x02, 0x0a, 0x32, 0x08, 0x52, 0x02, 0x08, 0x01, 0x5a, 0x02, 0x08, 0x01, 0x52, + 0x06, 0x68, 0x65, 0x69, 0x67, 0x68, 0x74, 0x12, 0x30, 0x0a, 0x06, 0x6c, 0x65, 0x6e, 0x67, 0x74, + 0x68, 0x18, 0x03, 0x20, 0x01, 0x28, 0x03, 0x42, 0x18, 0xc2, 0xff, 0x8e, 0x02, 0x03, 0xfa, 0x01, + 0x00, 0x8a, 0xf7, 0x98, 0xc6, 0x02, 0x0a, 0x32, 0x08, 0x52, 0x02, 0x08, 0x01, 0x5a, 0x02, 0x08, + 0x01, 0x52, 0x06, 0x6c, 0x65, 0x6e, 0x67, 0x74, 0x68, 0x3a, 0x07, 0xc2, 0xff, 0x8e, 0x02, 0x02, + 0x52, 0x00, 0x22, 0x7f, 0x0a, 0x0a, 0x46, 0x6f, 0x6f, 0x50, 0x72, 0x6f, 0x66, 0x69, 0x6c, 0x65, + 0x12, 0x2a, 0x0a, 0x05, 0x70, 0x6c, 0x61, 0x63, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x03, 0x42, + 0x14, 0xc2, 0xff, 0x8e, 0x02, 0x03, 0xfa, 0x01, 0x00, 0x8a, 0xf7, 0x98, 0xc6, 0x02, 0x06, 0x32, + 0x04, 0x52, 0x02, 0x08, 0x01, 0x52, 0x05, 0x70, 0x6c, 0x61, 0x63, 0x65, 0x12, 0x3c, 0x0a, 0x04, + 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x42, 0x28, 0xc2, 0xff, 0x8e, 0x02, + 0x03, 0xf2, 0x01, 0x00, 0x8a, 0xf7, 0x98, 0xc6, 0x02, 0x1a, 0x72, 0x18, 0x0a, 0x16, 0x52, 0x14, + 0x08, 0x01, 0x12, 0x10, 0x74, 0x73, 0x76, 0x5f, 0x70, 0x72, 0x6f, 0x66, 0x69, 0x6c, 0x65, 0x5f, + 0x6e, 0x61, 0x6d, 0x65, 0x52, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x3a, 0x07, 0xc2, 0xff, 0x8e, 0x02, + 0x02, 0x52, 0x00, 0x2a, 0x56, 0x0a, 0x09, 0x46, 0x6f, 0x6f, 0x53, 0x74, 0x61, 0x74, 0x75, 0x73, + 0x12, 0x1a, 0x0a, 0x16, 0x46, 0x4f, 0x4f, 0x5f, 0x53, 0x54, 0x41, 0x54, 0x55, 0x53, 0x5f, 0x55, + 0x4e, 0x53, 0x50, 0x45, 0x43, 0x49, 0x46, 0x49, 0x45, 0x44, 0x10, 0x00, 0x12, 0x15, 0x0a, 0x11, + 0x46, 0x4f, 0x4f, 0x5f, 0x53, 0x54, 0x41, 0x54, 0x55, 0x53, 0x5f, 0x41, 0x43, 0x54, 0x49, 0x56, + 0x45, 0x10, 0x01, 0x12, 0x16, 0x0a, 0x12, 0x46, 0x4f, 0x4f, 0x5f, 0x53, 0x54, 0x41, 0x54, 0x55, + 0x53, 0x5f, 0x44, 0x45, 0x4c, 0x45, 0x54, 0x45, 0x44, 0x10, 0x02, 0x42, 0x46, 0x5a, 0x44, 0x67, + 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x70, 0x65, 0x6e, 0x74, 0x6f, 0x70, + 0x73, 0x2f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x73, 0x74, 0x61, 0x74, 0x65, 0x2f, 0x69, 0x6e, 0x74, + 0x65, 0x72, 0x6e, 0x61, 0x6c, 0x2f, 0x74, 0x65, 0x73, 0x74, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2f, + 0x67, 0x65, 0x6e, 0x2f, 0x74, 0x65, 0x73, 0x74, 0x2f, 0x76, 0x31, 0x2f, 0x74, 0x65, 0x73, 0x74, + 0x5f, 0x70, 0x62, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, } var ( @@ -1005,7 +1210,7 @@ func file_test_v1_foo_j5s_proto_rawDescGZIP() []byte { } var file_test_v1_foo_j5s_proto_enumTypes = make([]protoimpl.EnumInfo, 1) -var file_test_v1_foo_j5s_proto_msgTypes = make([]protoimpl.MessageInfo, 10) +var file_test_v1_foo_j5s_proto_msgTypes = make([]protoimpl.MessageInfo, 13) var file_test_v1_foo_j5s_proto_goTypes = []interface{}{ (FooStatus)(0), // 0: test.v1.FooStatus (*FooKeys)(nil), // 1: test.v1.FooKeys @@ -1015,32 +1220,38 @@ var file_test_v1_foo_j5s_proto_goTypes = []interface{}{ (*FooEvent)(nil), // 5: test.v1.FooEvent (*FooCharacteristics)(nil), // 6: test.v1.FooCharacteristics (*FooProfile)(nil), // 7: test.v1.FooProfile - (*FooEventType_Created)(nil), // 8: test.v1.FooEventType.Created - (*FooEventType_Updated)(nil), // 9: test.v1.FooEventType.Updated - (*FooEventType_Deleted)(nil), // 10: test.v1.FooEventType.Deleted - (*psm_j5pb.StateMetadata)(nil), // 11: j5.state.v1.StateMetadata - (*psm_j5pb.EventMetadata)(nil), // 12: j5.state.v1.EventMetadata + (*FooData_Shape)(nil), // 8: test.v1.FooData.Shape + (*FooData_Shape_Circle)(nil), // 9: test.v1.FooData.Shape.Circle + (*FooData_Shape_Square)(nil), // 10: test.v1.FooData.Shape.Square + (*FooEventType_Created)(nil), // 11: test.v1.FooEventType.Created + (*FooEventType_Updated)(nil), // 12: test.v1.FooEventType.Updated + (*FooEventType_Deleted)(nil), // 13: test.v1.FooEventType.Deleted + (*psm_j5pb.StateMetadata)(nil), // 14: j5.state.v1.StateMetadata + (*psm_j5pb.EventMetadata)(nil), // 15: j5.state.v1.EventMetadata } var file_test_v1_foo_j5s_proto_depIdxs = []int32{ - 6, // 0: test.v1.FooData.characteristics:type_name -> test.v1.FooCharacteristics - 7, // 1: test.v1.FooData.profiles:type_name -> test.v1.FooProfile - 11, // 2: test.v1.FooState.metadata:type_name -> j5.state.v1.StateMetadata - 1, // 3: test.v1.FooState.keys:type_name -> test.v1.FooKeys - 2, // 4: test.v1.FooState.data:type_name -> test.v1.FooData - 0, // 5: test.v1.FooState.status:type_name -> test.v1.FooStatus - 8, // 6: test.v1.FooEventType.created:type_name -> test.v1.FooEventType.Created - 9, // 7: test.v1.FooEventType.updated:type_name -> test.v1.FooEventType.Updated - 10, // 8: test.v1.FooEventType.deleted:type_name -> test.v1.FooEventType.Deleted - 12, // 9: test.v1.FooEvent.metadata:type_name -> j5.state.v1.EventMetadata - 1, // 10: test.v1.FooEvent.keys:type_name -> test.v1.FooKeys - 4, // 11: test.v1.FooEvent.event:type_name -> test.v1.FooEventType - 7, // 12: test.v1.FooEventType.Created.profiles:type_name -> test.v1.FooProfile - 7, // 13: test.v1.FooEventType.Updated.profiles:type_name -> test.v1.FooProfile - 14, // [14:14] is the sub-list for method output_type - 14, // [14:14] is the sub-list for method input_type - 14, // [14:14] is the sub-list for extension type_name - 14, // [14:14] is the sub-list for extension extendee - 0, // [0:14] is the sub-list for field type_name + 8, // 0: test.v1.FooData.shape:type_name -> test.v1.FooData.Shape + 6, // 1: test.v1.FooData.characteristics:type_name -> test.v1.FooCharacteristics + 7, // 2: test.v1.FooData.profiles:type_name -> test.v1.FooProfile + 14, // 3: test.v1.FooState.metadata:type_name -> j5.state.v1.StateMetadata + 1, // 4: test.v1.FooState.keys:type_name -> test.v1.FooKeys + 2, // 5: test.v1.FooState.data:type_name -> test.v1.FooData + 0, // 6: test.v1.FooState.status:type_name -> test.v1.FooStatus + 11, // 7: test.v1.FooEventType.created:type_name -> test.v1.FooEventType.Created + 12, // 8: test.v1.FooEventType.updated:type_name -> test.v1.FooEventType.Updated + 13, // 9: test.v1.FooEventType.deleted:type_name -> test.v1.FooEventType.Deleted + 15, // 10: test.v1.FooEvent.metadata:type_name -> j5.state.v1.EventMetadata + 1, // 11: test.v1.FooEvent.keys:type_name -> test.v1.FooKeys + 4, // 12: test.v1.FooEvent.event:type_name -> test.v1.FooEventType + 9, // 13: test.v1.FooData.Shape.circle:type_name -> test.v1.FooData.Shape.Circle + 10, // 14: test.v1.FooData.Shape.square:type_name -> test.v1.FooData.Shape.Square + 7, // 15: test.v1.FooEventType.Created.profiles:type_name -> test.v1.FooProfile + 7, // 16: test.v1.FooEventType.Updated.profiles:type_name -> test.v1.FooProfile + 17, // [17:17] is the sub-list for method output_type + 17, // [17:17] is the sub-list for method input_type + 17, // [17:17] is the sub-list for extension type_name + 17, // [17:17] is the sub-list for extension extendee + 0, // [0:17] is the sub-list for field type_name } func init() { file_test_v1_foo_j5s_proto_init() } @@ -1134,7 +1345,7 @@ func file_test_v1_foo_j5s_proto_init() { } } file_test_v1_foo_j5s_proto_msgTypes[7].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*FooEventType_Created); i { + switch v := v.(*FooData_Shape); i { case 0: return &v.state case 1: @@ -1146,7 +1357,7 @@ func file_test_v1_foo_j5s_proto_init() { } } file_test_v1_foo_j5s_proto_msgTypes[8].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*FooEventType_Updated); i { + switch v := v.(*FooData_Shape_Circle); i { case 0: return &v.state case 1: @@ -1158,6 +1369,42 @@ func file_test_v1_foo_j5s_proto_init() { } } file_test_v1_foo_j5s_proto_msgTypes[9].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*FooData_Shape_Square); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_test_v1_foo_j5s_proto_msgTypes[10].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*FooEventType_Created); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_test_v1_foo_j5s_proto_msgTypes[11].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*FooEventType_Updated); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_test_v1_foo_j5s_proto_msgTypes[12].Exporter = func(v interface{}, i int) interface{} { switch v := v.(*FooEventType_Deleted); i { case 0: return &v.state @@ -1177,15 +1424,19 @@ func file_test_v1_foo_j5s_proto_init() { (*FooEventType_Updated_)(nil), (*FooEventType_Deleted_)(nil), } - file_test_v1_foo_j5s_proto_msgTypes[7].OneofWrappers = []interface{}{} - file_test_v1_foo_j5s_proto_msgTypes[8].OneofWrappers = []interface{}{} + file_test_v1_foo_j5s_proto_msgTypes[7].OneofWrappers = []interface{}{ + (*FooData_Shape_Circle_)(nil), + (*FooData_Shape_Square_)(nil), + } + file_test_v1_foo_j5s_proto_msgTypes[10].OneofWrappers = []interface{}{} + file_test_v1_foo_j5s_proto_msgTypes[11].OneofWrappers = []interface{}{} type x struct{} out := protoimpl.TypeBuilder{ File: protoimpl.DescBuilder{ GoPackagePath: reflect.TypeOf(x{}).PkgPath(), RawDescriptor: file_test_v1_foo_j5s_proto_rawDesc, NumEnums: 1, - NumMessages: 10, + NumMessages: 13, NumExtensions: 0, NumServices: 0, }, diff --git a/internal/testproto/gen/test/v1/test_pb/foo.j5s_j5.pb.go b/internal/testproto/gen/test/v1/test_pb/foo.j5s_j5.pb.go new file mode 100644 index 0000000..8e82b3a --- /dev/null +++ b/internal/testproto/gen/test/v1/test_pb/foo.j5s_j5.pb.go @@ -0,0 +1,144 @@ +// Code generated by protoc-gen-go-j5. DO NOT EDIT. + +package test_pb + +import ( + j5reflect "github.com/pentops/j5/lib/j5reflect" + proto "google.golang.org/protobuf/proto" +) + +func (msg *FooKeys) J5Reflect() j5reflect.Root { + return j5reflect.MustReflect(msg.ProtoReflect()) +} + +func (msg *FooKeys) J5Object() j5reflect.Object { + return j5reflect.MustReflect(msg.ProtoReflect()).(j5reflect.Object) +} + +func (msg *FooKeys) Clone() any { + return proto.Clone(msg).(*FooKeys) +} +func (msg *FooData) J5Reflect() j5reflect.Root { + return j5reflect.MustReflect(msg.ProtoReflect()) +} + +func (msg *FooData) J5Object() j5reflect.Object { + return j5reflect.MustReflect(msg.ProtoReflect()).(j5reflect.Object) +} + +func (msg *FooData) Clone() any { + return proto.Clone(msg).(*FooData) +} +func (msg *FooData_Shape) J5Reflect() j5reflect.Root { + return j5reflect.MustReflect(msg.ProtoReflect()) +} + +func (msg *FooData_Shape) Clone() any { + return proto.Clone(msg).(*FooData_Shape) +} +func (msg *FooData_Shape_Circle) J5Reflect() j5reflect.Root { + return j5reflect.MustReflect(msg.ProtoReflect()) +} + +func (msg *FooData_Shape_Circle) J5Object() j5reflect.Object { + return j5reflect.MustReflect(msg.ProtoReflect()).(j5reflect.Object) +} + +func (msg *FooData_Shape_Circle) Clone() any { + return proto.Clone(msg).(*FooData_Shape_Circle) +} +func (msg *FooData_Shape_Square) J5Reflect() j5reflect.Root { + return j5reflect.MustReflect(msg.ProtoReflect()) +} + +func (msg *FooData_Shape_Square) J5Object() j5reflect.Object { + return j5reflect.MustReflect(msg.ProtoReflect()).(j5reflect.Object) +} + +func (msg *FooData_Shape_Square) Clone() any { + return proto.Clone(msg).(*FooData_Shape_Square) +} +func (msg *FooState) J5Reflect() j5reflect.Root { + return j5reflect.MustReflect(msg.ProtoReflect()) +} + +func (msg *FooState) J5Object() j5reflect.Object { + return j5reflect.MustReflect(msg.ProtoReflect()).(j5reflect.Object) +} + +func (msg *FooState) Clone() any { + return proto.Clone(msg).(*FooState) +} +func (msg *FooEventType) J5Reflect() j5reflect.Root { + return j5reflect.MustReflect(msg.ProtoReflect()) +} + +func (msg *FooEventType) Clone() any { + return proto.Clone(msg).(*FooEventType) +} +func (msg *FooEventType_Created) J5Reflect() j5reflect.Root { + return j5reflect.MustReflect(msg.ProtoReflect()) +} + +func (msg *FooEventType_Created) J5Object() j5reflect.Object { + return j5reflect.MustReflect(msg.ProtoReflect()).(j5reflect.Object) +} + +func (msg *FooEventType_Created) Clone() any { + return proto.Clone(msg).(*FooEventType_Created) +} +func (msg *FooEventType_Updated) J5Reflect() j5reflect.Root { + return j5reflect.MustReflect(msg.ProtoReflect()) +} + +func (msg *FooEventType_Updated) J5Object() j5reflect.Object { + return j5reflect.MustReflect(msg.ProtoReflect()).(j5reflect.Object) +} + +func (msg *FooEventType_Updated) Clone() any { + return proto.Clone(msg).(*FooEventType_Updated) +} +func (msg *FooEventType_Deleted) J5Reflect() j5reflect.Root { + return j5reflect.MustReflect(msg.ProtoReflect()) +} + +func (msg *FooEventType_Deleted) J5Object() j5reflect.Object { + return j5reflect.MustReflect(msg.ProtoReflect()).(j5reflect.Object) +} + +func (msg *FooEventType_Deleted) Clone() any { + return proto.Clone(msg).(*FooEventType_Deleted) +} +func (msg *FooEvent) J5Reflect() j5reflect.Root { + return j5reflect.MustReflect(msg.ProtoReflect()) +} + +func (msg *FooEvent) J5Object() j5reflect.Object { + return j5reflect.MustReflect(msg.ProtoReflect()).(j5reflect.Object) +} + +func (msg *FooEvent) Clone() any { + return proto.Clone(msg).(*FooEvent) +} +func (msg *FooCharacteristics) J5Reflect() j5reflect.Root { + return j5reflect.MustReflect(msg.ProtoReflect()) +} + +func (msg *FooCharacteristics) J5Object() j5reflect.Object { + return j5reflect.MustReflect(msg.ProtoReflect()).(j5reflect.Object) +} + +func (msg *FooCharacteristics) Clone() any { + return proto.Clone(msg).(*FooCharacteristics) +} +func (msg *FooProfile) J5Reflect() j5reflect.Root { + return j5reflect.MustReflect(msg.ProtoReflect()) +} + +func (msg *FooProfile) J5Object() j5reflect.Object { + return j5reflect.MustReflect(msg.ProtoReflect()).(j5reflect.Object) +} + +func (msg *FooProfile) Clone() any { + return proto.Clone(msg).(*FooProfile) +} diff --git a/internal/testproto/gen/test/v1/test_pb/foo.j5s_sugar.pb.go b/internal/testproto/gen/test/v1/test_pb/foo.j5s_sugar.pb.go index 59173e5..6d8f054 100644 --- a/internal/testproto/gen/test/v1/test_pb/foo.j5s_sugar.pb.go +++ b/internal/testproto/gen/test/v1/test_pb/foo.j5s_sugar.pb.go @@ -8,6 +8,8 @@ import ( proto "google.golang.org/protobuf/proto" ) +type IsFooData_Shape_Type = isFooData_Shape_Type + // FooEventType is a oneof wrapper type FooEventTypeKey string diff --git a/internal/testproto/gen/test/v1/test_spb/bar.p.j5s_j5.pb.go b/internal/testproto/gen/test/v1/test_spb/bar.p.j5s_j5.pb.go new file mode 100644 index 0000000..7748444 --- /dev/null +++ b/internal/testproto/gen/test/v1/test_spb/bar.p.j5s_j5.pb.go @@ -0,0 +1,75 @@ +// Code generated by protoc-gen-go-j5. DO NOT EDIT. + +package test_spb + +import ( + j5reflect "github.com/pentops/j5/lib/j5reflect" + proto "google.golang.org/protobuf/proto" +) + +func (msg *BarGetRequest) J5Reflect() j5reflect.Root { + return j5reflect.MustReflect(msg.ProtoReflect()) +} + +func (msg *BarGetRequest) J5Object() j5reflect.Object { + return j5reflect.MustReflect(msg.ProtoReflect()).(j5reflect.Object) +} + +func (msg *BarGetRequest) Clone() any { + return proto.Clone(msg).(*BarGetRequest) +} +func (msg *BarGetResponse) J5Reflect() j5reflect.Root { + return j5reflect.MustReflect(msg.ProtoReflect()) +} + +func (msg *BarGetResponse) J5Object() j5reflect.Object { + return j5reflect.MustReflect(msg.ProtoReflect()).(j5reflect.Object) +} + +func (msg *BarGetResponse) Clone() any { + return proto.Clone(msg).(*BarGetResponse) +} +func (msg *BarListRequest) J5Reflect() j5reflect.Root { + return j5reflect.MustReflect(msg.ProtoReflect()) +} + +func (msg *BarListRequest) J5Object() j5reflect.Object { + return j5reflect.MustReflect(msg.ProtoReflect()).(j5reflect.Object) +} + +func (msg *BarListRequest) Clone() any { + return proto.Clone(msg).(*BarListRequest) +} +func (msg *BarListResponse) J5Reflect() j5reflect.Root { + return j5reflect.MustReflect(msg.ProtoReflect()) +} + +func (msg *BarListResponse) J5Object() j5reflect.Object { + return j5reflect.MustReflect(msg.ProtoReflect()).(j5reflect.Object) +} + +func (msg *BarListResponse) Clone() any { + return proto.Clone(msg).(*BarListResponse) +} +func (msg *BarEventsRequest) J5Reflect() j5reflect.Root { + return j5reflect.MustReflect(msg.ProtoReflect()) +} + +func (msg *BarEventsRequest) J5Object() j5reflect.Object { + return j5reflect.MustReflect(msg.ProtoReflect()).(j5reflect.Object) +} + +func (msg *BarEventsRequest) Clone() any { + return proto.Clone(msg).(*BarEventsRequest) +} +func (msg *BarEventsResponse) J5Reflect() j5reflect.Root { + return j5reflect.MustReflect(msg.ProtoReflect()) +} + +func (msg *BarEventsResponse) J5Object() j5reflect.Object { + return j5reflect.MustReflect(msg.ProtoReflect()).(j5reflect.Object) +} + +func (msg *BarEventsResponse) Clone() any { + return proto.Clone(msg).(*BarEventsResponse) +} diff --git a/internal/testproto/gen/test/v1/test_spb/bar.p.j5s_psm_query.pb.go b/internal/testproto/gen/test/v1/test_spb/bar.p.j5s_psm_query.pb.go index d0b3d8e..70f9c05 100644 --- a/internal/testproto/gen/test/v1/test_spb/bar.p.j5s_psm_query.pb.go +++ b/internal/testproto/gen/test/v1/test_spb/bar.p.j5s_psm_query.pb.go @@ -4,6 +4,9 @@ package test_spb import ( context "context" + fmt "fmt" + j5reflect "github.com/pentops/j5/lib/j5reflect" + j5schema "github.com/pentops/j5/lib/j5schema" psm "github.com/pentops/protostate/psm" sqrlx "github.com/pentops/sqrlx.go/sqrlx" ) @@ -11,60 +14,45 @@ import ( // State Query Service for %sBar // QuerySet is the query set for the Bar service. -type BarPSMQuerySet = psm.StateQuerySet[ - *BarGetRequest, - *BarGetResponse, - *BarListRequest, - *BarListResponse, - *BarEventsRequest, - *BarEventsResponse, -] +type BarPSMQuerySet = psm.StateQuerySet func NewBarPSMQuerySet( - smSpec psm.QuerySpec[ - *BarGetRequest, - *BarGetResponse, - *BarListRequest, - *BarListResponse, - *BarEventsRequest, - *BarEventsResponse, - ], + smSpec psm.QuerySpec, options psm.StateQueryOptions, ) (*BarPSMQuerySet, error) { - return psm.BuildStateQuerySet[ - *BarGetRequest, - *BarGetResponse, - *BarListRequest, - *BarListResponse, - *BarEventsRequest, - *BarEventsResponse, - ](smSpec, options) + return psm.BuildStateQuerySet(smSpec, options) } -type BarPSMQuerySpec = psm.QuerySpec[ - *BarGetRequest, - *BarGetResponse, - *BarListRequest, - *BarListResponse, - *BarEventsRequest, - *BarEventsResponse, -] +type BarPSMQuerySpec = psm.QuerySpec func DefaultBarPSMQuerySpec(tableSpec psm.QueryTableSpec) BarPSMQuerySpec { - return psm.QuerySpec[ - *BarGetRequest, - *BarGetResponse, - *BarListRequest, - *BarListResponse, - *BarEventsRequest, - *BarEventsResponse, - ]{ + return psm.QuerySpec{ + GetMethod: &j5schema.MethodSchema{ + Request: j5schema.MustObjectSchema((&BarGetRequest{}).ProtoReflect().Descriptor()), + Response: j5schema.MustObjectSchema((&BarGetResponse{}).ProtoReflect().Descriptor()), + }, + ListMethod: &j5schema.MethodSchema{ + Request: j5schema.MustObjectSchema((&BarListRequest{}).ProtoReflect().Descriptor()), + Response: j5schema.MustObjectSchema((&BarListResponse{}).ProtoReflect().Descriptor()), + }, + ListEventsMethod: &j5schema.MethodSchema{ + Request: j5schema.MustObjectSchema((&BarEventsRequest{}).ProtoReflect().Descriptor()), + Response: j5schema.MustObjectSchema((&BarEventsResponse{}).ProtoReflect().Descriptor()), + }, QueryTableSpec: tableSpec, - ListRequestFilter: func(req *BarListRequest) (map[string]interface{}, error) { + ListRequestFilter: func(reqReflect j5reflect.Object) (map[string]interface{}, error) { + req, ok := reqReflect.Interface().(*BarListRequest) + if !ok { + return nil, fmt.Errorf("expected *BarListRequest but got %T", req) + } filter := map[string]interface{}{} return filter, nil }, - ListEventsRequestFilter: func(req *BarEventsRequest) (map[string]interface{}, error) { + ListEventsRequestFilter: func(reqReflect j5reflect.Object) (map[string]interface{}, error) { + req, ok := reqReflect.Interface().(*BarEventsRequest) + if !ok { + return nil, fmt.Errorf("expected *BarEventsRequest but got %T", req) + } filter := map[string]interface{}{} filter["bar_id"] = req.BarId filter["bar_other_id"] = req.BarOtherId @@ -91,7 +79,7 @@ func NewBarQueryServiceImpl(db sqrlx.Transactor, querySet *BarPSMQuerySet) *BarQ func (s *BarQueryServiceImpl) BarGet(ctx context.Context, req *BarGetRequest) (*BarGetResponse, error) { resObject := &BarGetResponse{} - err := s.querySet.Get(ctx, s.db, req, resObject) + err := s.querySet.Get(ctx, s.db, req.J5Object(), resObject.J5Object()) if err != nil { return nil, err } @@ -100,7 +88,7 @@ func (s *BarQueryServiceImpl) BarGet(ctx context.Context, req *BarGetRequest) (* func (s *BarQueryServiceImpl) BarList(ctx context.Context, req *BarListRequest) (*BarListResponse, error) { resObject := &BarListResponse{} - err := s.querySet.List(ctx, s.db, req, resObject) + err := s.querySet.List(ctx, s.db, req.J5Object(), resObject.J5Object()) if err != nil { return nil, err } @@ -109,7 +97,7 @@ func (s *BarQueryServiceImpl) BarList(ctx context.Context, req *BarListRequest) func (s *BarQueryServiceImpl) BarEvents(ctx context.Context, req *BarEventsRequest) (*BarEventsResponse, error) { resObject := &BarEventsResponse{} - err := s.querySet.ListEvents(ctx, s.db, req, resObject) + err := s.querySet.ListEvents(ctx, s.db, req.J5Object(), resObject.J5Object()) if err != nil { return nil, err } diff --git a/internal/testproto/gen/test/v1/test_spb/foo.p.j5s_j5.pb.go b/internal/testproto/gen/test/v1/test_spb/foo.p.j5s_j5.pb.go new file mode 100644 index 0000000..ff665ec --- /dev/null +++ b/internal/testproto/gen/test/v1/test_spb/foo.p.j5s_j5.pb.go @@ -0,0 +1,97 @@ +// Code generated by protoc-gen-go-j5. DO NOT EDIT. + +package test_spb + +import ( + j5reflect "github.com/pentops/j5/lib/j5reflect" + proto "google.golang.org/protobuf/proto" +) + +func (msg *FooGetRequest) J5Reflect() j5reflect.Root { + return j5reflect.MustReflect(msg.ProtoReflect()) +} + +func (msg *FooGetRequest) J5Object() j5reflect.Object { + return j5reflect.MustReflect(msg.ProtoReflect()).(j5reflect.Object) +} + +func (msg *FooGetRequest) Clone() any { + return proto.Clone(msg).(*FooGetRequest) +} +func (msg *FooGetResponse) J5Reflect() j5reflect.Root { + return j5reflect.MustReflect(msg.ProtoReflect()) +} + +func (msg *FooGetResponse) J5Object() j5reflect.Object { + return j5reflect.MustReflect(msg.ProtoReflect()).(j5reflect.Object) +} + +func (msg *FooGetResponse) Clone() any { + return proto.Clone(msg).(*FooGetResponse) +} +func (msg *FooListRequest) J5Reflect() j5reflect.Root { + return j5reflect.MustReflect(msg.ProtoReflect()) +} + +func (msg *FooListRequest) J5Object() j5reflect.Object { + return j5reflect.MustReflect(msg.ProtoReflect()).(j5reflect.Object) +} + +func (msg *FooListRequest) Clone() any { + return proto.Clone(msg).(*FooListRequest) +} +func (msg *FooListResponse) J5Reflect() j5reflect.Root { + return j5reflect.MustReflect(msg.ProtoReflect()) +} + +func (msg *FooListResponse) J5Object() j5reflect.Object { + return j5reflect.MustReflect(msg.ProtoReflect()).(j5reflect.Object) +} + +func (msg *FooListResponse) Clone() any { + return proto.Clone(msg).(*FooListResponse) +} +func (msg *FooEventsRequest) J5Reflect() j5reflect.Root { + return j5reflect.MustReflect(msg.ProtoReflect()) +} + +func (msg *FooEventsRequest) J5Object() j5reflect.Object { + return j5reflect.MustReflect(msg.ProtoReflect()).(j5reflect.Object) +} + +func (msg *FooEventsRequest) Clone() any { + return proto.Clone(msg).(*FooEventsRequest) +} +func (msg *FooEventsResponse) J5Reflect() j5reflect.Root { + return j5reflect.MustReflect(msg.ProtoReflect()) +} + +func (msg *FooEventsResponse) J5Object() j5reflect.Object { + return j5reflect.MustReflect(msg.ProtoReflect()).(j5reflect.Object) +} + +func (msg *FooEventsResponse) Clone() any { + return proto.Clone(msg).(*FooEventsResponse) +} +func (msg *FooSummaryRequest) J5Reflect() j5reflect.Root { + return j5reflect.MustReflect(msg.ProtoReflect()) +} + +func (msg *FooSummaryRequest) J5Object() j5reflect.Object { + return j5reflect.MustReflect(msg.ProtoReflect()).(j5reflect.Object) +} + +func (msg *FooSummaryRequest) Clone() any { + return proto.Clone(msg).(*FooSummaryRequest) +} +func (msg *FooSummaryResponse) J5Reflect() j5reflect.Root { + return j5reflect.MustReflect(msg.ProtoReflect()) +} + +func (msg *FooSummaryResponse) J5Object() j5reflect.Object { + return j5reflect.MustReflect(msg.ProtoReflect()).(j5reflect.Object) +} + +func (msg *FooSummaryResponse) Clone() any { + return proto.Clone(msg).(*FooSummaryResponse) +} diff --git a/internal/testproto/gen/test/v1/test_spb/foo.p.j5s_psm_query.pb.go b/internal/testproto/gen/test/v1/test_spb/foo.p.j5s_psm_query.pb.go index 2daa236..5d47ac8 100644 --- a/internal/testproto/gen/test/v1/test_spb/foo.p.j5s_psm_query.pb.go +++ b/internal/testproto/gen/test/v1/test_spb/foo.p.j5s_psm_query.pb.go @@ -4,6 +4,9 @@ package test_spb import ( context "context" + fmt "fmt" + j5reflect "github.com/pentops/j5/lib/j5reflect" + j5schema "github.com/pentops/j5/lib/j5schema" psm "github.com/pentops/protostate/psm" sqrlx "github.com/pentops/sqrlx.go/sqrlx" ) @@ -11,60 +14,45 @@ import ( // State Query Service for %sFoo // QuerySet is the query set for the Foo service. -type FooPSMQuerySet = psm.StateQuerySet[ - *FooGetRequest, - *FooGetResponse, - *FooListRequest, - *FooListResponse, - *FooEventsRequest, - *FooEventsResponse, -] +type FooPSMQuerySet = psm.StateQuerySet func NewFooPSMQuerySet( - smSpec psm.QuerySpec[ - *FooGetRequest, - *FooGetResponse, - *FooListRequest, - *FooListResponse, - *FooEventsRequest, - *FooEventsResponse, - ], + smSpec psm.QuerySpec, options psm.StateQueryOptions, ) (*FooPSMQuerySet, error) { - return psm.BuildStateQuerySet[ - *FooGetRequest, - *FooGetResponse, - *FooListRequest, - *FooListResponse, - *FooEventsRequest, - *FooEventsResponse, - ](smSpec, options) + return psm.BuildStateQuerySet(smSpec, options) } -type FooPSMQuerySpec = psm.QuerySpec[ - *FooGetRequest, - *FooGetResponse, - *FooListRequest, - *FooListResponse, - *FooEventsRequest, - *FooEventsResponse, -] +type FooPSMQuerySpec = psm.QuerySpec func DefaultFooPSMQuerySpec(tableSpec psm.QueryTableSpec) FooPSMQuerySpec { - return psm.QuerySpec[ - *FooGetRequest, - *FooGetResponse, - *FooListRequest, - *FooListResponse, - *FooEventsRequest, - *FooEventsResponse, - ]{ + return psm.QuerySpec{ + GetMethod: &j5schema.MethodSchema{ + Request: j5schema.MustObjectSchema((&FooGetRequest{}).ProtoReflect().Descriptor()), + Response: j5schema.MustObjectSchema((&FooGetResponse{}).ProtoReflect().Descriptor()), + }, + ListMethod: &j5schema.MethodSchema{ + Request: j5schema.MustObjectSchema((&FooListRequest{}).ProtoReflect().Descriptor()), + Response: j5schema.MustObjectSchema((&FooListResponse{}).ProtoReflect().Descriptor()), + }, + ListEventsMethod: &j5schema.MethodSchema{ + Request: j5schema.MustObjectSchema((&FooEventsRequest{}).ProtoReflect().Descriptor()), + Response: j5schema.MustObjectSchema((&FooEventsResponse{}).ProtoReflect().Descriptor()), + }, QueryTableSpec: tableSpec, - ListRequestFilter: func(req *FooListRequest) (map[string]interface{}, error) { + ListRequestFilter: func(reqReflect j5reflect.Object) (map[string]interface{}, error) { + req, ok := reqReflect.Interface().(*FooListRequest) + if !ok { + return nil, fmt.Errorf("expected *FooListRequest but got %T", req) + } filter := map[string]interface{}{} return filter, nil }, - ListEventsRequestFilter: func(req *FooEventsRequest) (map[string]interface{}, error) { + ListEventsRequestFilter: func(reqReflect j5reflect.Object) (map[string]interface{}, error) { + req, ok := reqReflect.Interface().(*FooEventsRequest) + if !ok { + return nil, fmt.Errorf("expected *FooEventsRequest but got %T", req) + } filter := map[string]interface{}{} filter["foo_id"] = req.FooId return filter, nil @@ -89,7 +77,7 @@ func NewFooQueryServiceImpl(db sqrlx.Transactor, querySet *FooPSMQuerySet) *FooQ func (s *FooQueryServiceImpl) FooGet(ctx context.Context, req *FooGetRequest) (*FooGetResponse, error) { resObject := &FooGetResponse{} - err := s.querySet.Get(ctx, s.db, req, resObject) + err := s.querySet.Get(ctx, s.db, req.J5Object(), resObject.J5Object()) if err != nil { return nil, err } @@ -98,7 +86,7 @@ func (s *FooQueryServiceImpl) FooGet(ctx context.Context, req *FooGetRequest) (* func (s *FooQueryServiceImpl) FooList(ctx context.Context, req *FooListRequest) (*FooListResponse, error) { resObject := &FooListResponse{} - err := s.querySet.List(ctx, s.db, req, resObject) + err := s.querySet.List(ctx, s.db, req.J5Object(), resObject.J5Object()) if err != nil { return nil, err } @@ -107,7 +95,7 @@ func (s *FooQueryServiceImpl) FooList(ctx context.Context, req *FooListRequest) func (s *FooQueryServiceImpl) FooEvents(ctx context.Context, req *FooEventsRequest) (*FooEventsResponse, error) { resObject := &FooEventsResponse{} - err := s.querySet.ListEvents(ctx, s.db, req, resObject) + err := s.querySet.ListEvents(ctx, s.db, req.J5Object(), resObject.J5Object()) if err != nil { return nil, err } diff --git a/internal/testproto/gen/test/v1/test_tpb/bar.p.j5s_j5.pb.go b/internal/testproto/gen/test/v1/test_tpb/bar.p.j5s_j5.pb.go new file mode 100644 index 0000000..8022834 --- /dev/null +++ b/internal/testproto/gen/test/v1/test_tpb/bar.p.j5s_j5.pb.go @@ -0,0 +1,20 @@ +// Code generated by protoc-gen-go-j5. DO NOT EDIT. + +package test_tpb + +import ( + j5reflect "github.com/pentops/j5/lib/j5reflect" + proto "google.golang.org/protobuf/proto" +) + +func (msg *BarEventMessage) J5Reflect() j5reflect.Root { + return j5reflect.MustReflect(msg.ProtoReflect()) +} + +func (msg *BarEventMessage) J5Object() j5reflect.Object { + return j5reflect.MustReflect(msg.ProtoReflect()).(j5reflect.Object) +} + +func (msg *BarEventMessage) Clone() any { + return proto.Clone(msg).(*BarEventMessage) +} diff --git a/internal/testproto/gen/test/v1/test_tpb/foo.p.j5s_j5.pb.go b/internal/testproto/gen/test/v1/test_tpb/foo.p.j5s_j5.pb.go new file mode 100644 index 0000000..017453b --- /dev/null +++ b/internal/testproto/gen/test/v1/test_tpb/foo.p.j5s_j5.pb.go @@ -0,0 +1,20 @@ +// Code generated by protoc-gen-go-j5. DO NOT EDIT. + +package test_tpb + +import ( + j5reflect "github.com/pentops/j5/lib/j5reflect" + proto "google.golang.org/protobuf/proto" +) + +func (msg *FooEventMessage) J5Reflect() j5reflect.Root { + return j5reflect.MustReflect(msg.ProtoReflect()) +} + +func (msg *FooEventMessage) J5Object() j5reflect.Object { + return j5reflect.MustReflect(msg.ProtoReflect()).(j5reflect.Object) +} + +func (msg *FooEventMessage) Clone() any { + return proto.Clone(msg).(*FooEventMessage) +} diff --git a/internal/testproto/test/v1/foo.j5s b/internal/testproto/test/v1/foo.j5s index c643679..2ceb6b5 100644 --- a/internal/testproto/test/v1/foo.j5s +++ b/internal/testproto/test/v1/foo.j5s @@ -36,6 +36,22 @@ entity Foo { listRules.searching.fieldIdentifier = "tsv_description" } + data shape oneof:FooShape { + listRules.filtering.filterable = true + option circle object { + field radius integer:INT64 { + listRules.filtering.filterable = true + listRules.sorting.sortable = true + } + } + option square object { + field side integer:INT64 { + listRules.filtering.filterable = true + listRules.sorting.sortable = true + } + } + } + data characteristics object:FooCharacteristics data profiles array:object:FooProfile diff --git a/j5.yaml b/j5.yaml index 37abbb4..3445177 100644 --- a/j5.yaml +++ b/j5.yaml @@ -21,6 +21,7 @@ generate: - base: go-sugar - base: go-psm - base: go-o5-messaging + - base: go-j5 managedPaths: - internal/testproto/gen @@ -50,4 +51,14 @@ plugins: - base: go name: go-o5-messaging docker: - image: ghcr.io/pentops/protoc-gen-go-o5-messaging:fba07334e9aa1affc26b34eae82254a36f955267 + image: ghcr.io/pentops/protoc-gen-go-o5-messaging:7e07c29129f03edc9ef01ba4739328625ef24746 + + - base: go + name: go-j5 + docker: + image: ghcr.io/pentops/protoc-gen-go-j5:latest + +pluginOverrides: + - name: go-j5 + local: + cmd: protoc-gen-go-j5 diff --git a/pquery/filter.go b/pquery/filter.go deleted file mode 100644 index 7dc34b6..0000000 --- a/pquery/filter.go +++ /dev/null @@ -1,825 +0,0 @@ -package pquery - -import ( - "fmt" - "strconv" - "strings" - "time" - - sq "github.com/elgris/sqrl" - "github.com/elgris/sqrl/pg" - "github.com/google/uuid" - "github.com/pentops/j5/gen/j5/list/v1/list_j5pb" - "github.com/pentops/protostate/internal/pgstore" - "google.golang.org/protobuf/proto" - "google.golang.org/protobuf/reflect/protoreflect" - "google.golang.org/protobuf/types/descriptorpb" - "google.golang.org/protobuf/types/known/timestamppb" -) - -type filterSpec struct { - *pgstore.NestedField - filterVals []any -} - -func filtersForField(field protoreflect.FieldDescriptor) ([]any, error) { - - fieldOpts := proto.GetExtension(field.Options().(*descriptorpb.FieldOptions), list_j5pb.E_Field).(*list_j5pb.FieldConstraint) - vals := []any{} - - if fieldOpts == nil || fieldOpts.Type == nil { - return nil, nil - } - - switch fieldOps := fieldOpts.Type.(type) { - case *list_j5pb.FieldConstraint_Float: - if fieldOps.Float.Filtering != nil && fieldOps.Float.Filtering.Filterable { - for _, val := range fieldOps.Float.Filtering.DefaultFilters { - v, err := strconv.ParseFloat(val, 32) - if err != nil { - return nil, fmt.Errorf("error parsing default filter (%s): %w", field.JSONName(), err) - } - - vals = append(vals, v) - } - } - - case *list_j5pb.FieldConstraint_Double: - if fieldOps.Double.Filtering != nil && fieldOps.Double.Filtering.Filterable { - for _, val := range fieldOps.Double.Filtering.DefaultFilters { - v, err := strconv.ParseFloat(val, 64) - if err != nil { - return nil, fmt.Errorf("error parsing default filter (%s): %w", field.JSONName(), err) - } - - vals = append(vals, v) - } - } - - case *list_j5pb.FieldConstraint_Fixed32: - if fieldOps.Fixed32.Filtering != nil && fieldOps.Fixed32.Filtering.Filterable { - for _, val := range fieldOps.Fixed32.Filtering.DefaultFilters { - v, err := strconv.ParseUint(val, 10, 32) - if err != nil { - return nil, fmt.Errorf("error parsing default filter (%s): %w", field.JSONName(), err) - } - - vals = append(vals, v) - } - } - - case *list_j5pb.FieldConstraint_Fixed64: - if fieldOps.Fixed64.Filtering != nil && fieldOps.Fixed64.Filtering.Filterable { - for _, val := range fieldOps.Fixed64.Filtering.DefaultFilters { - v, err := strconv.ParseUint(val, 10, 64) - if err != nil { - return nil, fmt.Errorf("error parsing default filter (%s): %w", field.JSONName(), err) - } - - vals = append(vals, v) - } - } - - case *list_j5pb.FieldConstraint_Int32: - if fieldOps.Int32.Filtering != nil && fieldOps.Int32.Filtering.Filterable { - for _, val := range fieldOps.Int32.Filtering.DefaultFilters { - v, err := strconv.ParseInt(val, 10, 32) - if err != nil { - return nil, fmt.Errorf("error parsing default filter (%s): %w", field.JSONName(), err) - } - - vals = append(vals, v) - } - } - - case *list_j5pb.FieldConstraint_Int64: - if fieldOps.Int64.Filtering != nil && fieldOps.Int64.Filtering.Filterable { - for _, val := range fieldOps.Int64.Filtering.DefaultFilters { - v, err := strconv.ParseInt(val, 10, 64) - if err != nil { - return nil, fmt.Errorf("error parsing default filter (%s): %w", field.JSONName(), err) - } - - vals = append(vals, v) - } - } - - case *list_j5pb.FieldConstraint_Sfixed32: - if fieldOps.Sfixed32.Filtering != nil && fieldOps.Sfixed32.Filtering.Filterable { - for _, val := range fieldOps.Sfixed32.Filtering.DefaultFilters { - v, err := strconv.ParseInt(val, 10, 32) - if err != nil { - return nil, fmt.Errorf("error parsing default filter (%s): %w", field.JSONName(), err) - } - - vals = append(vals, v) - } - } - - case *list_j5pb.FieldConstraint_Sfixed64: - if fieldOps.Sfixed64.Filtering != nil && fieldOps.Sfixed64.Filtering.Filterable { - for _, val := range fieldOps.Sfixed64.Filtering.DefaultFilters { - v, err := strconv.ParseInt(val, 10, 64) - if err != nil { - return nil, fmt.Errorf("error parsing default filter (%s): %w", field.JSONName(), err) - } - - vals = append(vals, v) - } - } - - case *list_j5pb.FieldConstraint_Sint32: - if fieldOps.Sint32.Filtering != nil && fieldOps.Sint32.Filtering.Filterable { - for _, val := range fieldOps.Sint32.Filtering.DefaultFilters { - v, err := strconv.ParseInt(val, 10, 32) - if err != nil { - return nil, fmt.Errorf("error parsing default filter (%s): %w", field.JSONName(), err) - } - - vals = append(vals, v) - } - } - - case *list_j5pb.FieldConstraint_Sint64: - if fieldOps.Sint64.Filtering != nil && fieldOps.Sint64.Filtering.Filterable { - for _, val := range fieldOps.Sint64.Filtering.DefaultFilters { - v, err := strconv.ParseInt(val, 10, 64) - if err != nil { - return nil, fmt.Errorf("error parsing default filter (%s): %w", field.JSONName(), err) - } - - vals = append(vals, v) - } - } - - case *list_j5pb.FieldConstraint_Uint32: - if fieldOps.Uint32.Filtering != nil && fieldOps.Uint32.Filtering.Filterable { - for _, val := range fieldOps.Uint32.Filtering.DefaultFilters { - v, err := strconv.ParseUint(val, 10, 32) - if err != nil { - return nil, fmt.Errorf("error parsing default filter (%s): %w", field.JSONName(), err) - } - - vals = append(vals, v) - } - } - - case *list_j5pb.FieldConstraint_Uint64: - if fieldOps.Uint64.Filtering != nil && fieldOps.Uint64.Filtering.Filterable { - for _, val := range fieldOps.Uint64.Filtering.DefaultFilters { - v, err := strconv.ParseUint(val, 10, 64) - if err != nil { - return nil, fmt.Errorf("error parsing default filter (%s): %w", field.JSONName(), err) - } - - vals = append(vals, v) - } - } - - case *list_j5pb.FieldConstraint_Bool: - if fieldOps.Bool.Filtering != nil && fieldOps.Bool.Filtering.Filterable { - for _, val := range fieldOps.Bool.Filtering.DefaultFilters { - v, err := strconv.ParseBool(val) - if err != nil { - return nil, fmt.Errorf("error parsing default filter (%s): %w", field.JSONName(), err) - } - - vals = append(vals, v) - } - } - - case *list_j5pb.FieldConstraint_String_: - switch fieldOps := fieldOps.String_.WellKnown.(type) { - case *list_j5pb.StringRules_Date: - if fieldOps.Date.Filtering != nil && fieldOps.Date.Filtering.Filterable { - for _, val := range fieldOps.Date.Filtering.DefaultFilters { - if !dateRegex.MatchString(val) { - return nil, fmt.Errorf("invalid date format for default filter (%s): %s", field.JSONName(), val) - } - - // TODO: change to using ranges for date to handle whole - // year, whole month, whole day - vals = append(vals, val) - } - } - - case *list_j5pb.StringRules_ForeignKey: - switch fieldOps := fieldOps.ForeignKey.Type.(type) { - case *list_j5pb.ForeignKeyRules_UniqueString: - if fieldOps.UniqueString.Filtering != nil && fieldOps.UniqueString.Filtering.Filterable { - for _, val := range fieldOps.UniqueString.Filtering.DefaultFilters { - vals = append(vals, val) - } - } - - case *list_j5pb.ForeignKeyRules_Id62: - if fieldOps.Id62.Filtering != nil && fieldOps.Id62.Filtering.Filterable { - for _, val := range fieldOps.Id62.Filtering.DefaultFilters { - vals = append(vals, val) - } - } - - case *list_j5pb.ForeignKeyRules_Uuid: - if fieldOps.Uuid.Filtering != nil && fieldOps.Uuid.Filtering.Filterable { - for _, val := range fieldOps.Uuid.Filtering.DefaultFilters { - _, err := uuid.Parse(val) - if err != nil { - return nil, fmt.Errorf("error parsing default filter (%s): %w", field.JSONName(), err) - } - - vals = append(vals, val) - } - } - - } - } - - case *list_j5pb.FieldConstraint_Enum: - if fieldOps.Enum.Filtering != nil && fieldOps.Enum.Filtering.Filterable { - for _, val := range fieldOps.Enum.Filtering.DefaultFilters { - vals = append(vals, val) - } - } - - case *list_j5pb.FieldConstraint_Timestamp: - if fieldOps.Timestamp.Filtering != nil && fieldOps.Timestamp.Filtering.Filterable { - for _, val := range fieldOps.Timestamp.Filtering.DefaultFilters { - t, err := time.Parse(time.RFC3339, val) - if err != nil { - return nil, fmt.Errorf("error parsing default filter (%s): %w", field.JSONName(), err) - } - - vals = append(vals, timestamppb.New(t)) - } - } - - case *list_j5pb.FieldConstraint_Date: - if fieldOps.Date.Filtering != nil && fieldOps.Date.Filtering.Filterable { - for _, val := range fieldOps.Date.Filtering.DefaultFilters { - t, err := time.Parse(time.DateOnly, val) - if err != nil { - return nil, fmt.Errorf("error parsing default filter (%s): %w", field.JSONName(), err) - } - - vals = append(vals, t) - } - } - - case *list_j5pb.FieldConstraint_Oneof: - if fieldOps.Oneof.Filtering != nil && fieldOps.Oneof.Filtering.Filterable { - for _, val := range fieldOps.Oneof.Filtering.DefaultFilters { - vals = append(vals, val) - } - } - - default: - return nil, fmt.Errorf("unknown field type for filter rules %T", fieldOps) - } - return vals, nil -} - -func buildDefaultFilters(columnName string, message protoreflect.MessageDescriptor) ([]filterSpec, error) { - var filters []filterSpec - - err := pgstore.WalkPathNodes(message, func(path pgstore.Path) error { - field := path.LeafField() - if field == nil { - oneof := path.LeafOneofWrapper() - if oneof == nil { - return nil - } - field = oneof - } - - vals, err := filtersForField(field) - if err != nil { - return fmt.Errorf("filters for field: %w", err) - } - - if len(vals) > 0 { - filters = append(filters, filterSpec{ - NestedField: &pgstore.NestedField{ - Path: path, - RootColumn: columnName, - }, - filterVals: vals, - }) - } - return nil - }) - if err != nil { - return nil, fmt.Errorf("walk path nodes: %w", err) - } - - return filters, nil -} - -func (ll *Lister[REQ, RES]) buildDynamicFilter(tableAlias string, filters []*list_j5pb.Filter) ([]sq.Sqlizer, error) { - out := []sq.Sqlizer{} - - for i := range filters { - switch filters[i].GetType().(type) { - case *list_j5pb.Filter_Field: - pathSpec := pgstore.ParseJSONPathSpec(filters[i].GetField().GetName()) - spec, err := pgstore.NewJSONPath(ll.arrayField.Message(), pathSpec) - if err != nil { - return nil, fmt.Errorf("dynamic filter: find field: %w", err) - } - - biggerSpec := &pgstore.NestedField{ - Path: *spec, - RootColumn: ll.dataColumn, - } - - var o sq.Sqlizer - switch leaf := spec.Leaf().(type) { - case protoreflect.OneofDescriptor: - o, err = ll.buildDynamicFilterOneof(tableAlias, biggerSpec, filters[i]) - if err != nil { - return nil, fmt.Errorf("dynamic filter: build oneof: %w", err) - } - case protoreflect.FieldDescriptor: - o, err = ll.buildDynamicFilterField(tableAlias, biggerSpec, filters[i]) - if err != nil { - return nil, fmt.Errorf("dynamic filter: build field: %w", err) - } - default: - return nil, fmt.Errorf("unknown leaf type %T", leaf) - } - - out = append(out, o) - case *list_j5pb.Filter_And: - f, err := ll.buildDynamicFilter(tableAlias, filters[i].GetAnd().GetFilters()) - if err != nil { - return nil, fmt.Errorf("dynamic filter: and: %w", err) - } - and := sq.And{} - and = append(and, f...) - - out = append(out, and) - case *list_j5pb.Filter_Or: - f, err := ll.buildDynamicFilter(tableAlias, filters[i].GetOr().GetFilters()) - if err != nil { - return nil, fmt.Errorf("dynamic filter: or: %w", err) - } - or := sq.Or{} - or = append(or, f...) - - out = append(out, or) - } - } - - return out, nil -} - -func (ll *Lister[REQ, RES]) buildDynamicFilterField(tableAlias string, spec *pgstore.NestedField, filter *list_j5pb.Filter) (sq.Sqlizer, error) { - var out sq.And - - if filter.GetField() == nil { - return nil, fmt.Errorf("dynamic filter: field is nil") - } - - leafField := spec.Path.LeafField() - - switch ft := filter.GetField().GetType().Type.(type) { - case *list_j5pb.FieldType_Value: - val := ft.Value - if leafField.Kind() == protoreflect.EnumKind { - name := strings.ToTitle(val) - prefix := strings.TrimSuffix(string(leafField.Enum().Values().Get(0).Name()), "_UNSPECIFIED") - - if !strings.HasPrefix(val, prefix) { - name = prefix + "_" + name - } - - val = name - } - - out = sq.And{sq.Expr( - fmt.Sprintf("jsonb_path_query_array(%s.%s, '%s') @> ?", - tableAlias, - spec.RootColumn, - spec.Path.JSONPathQuery(), - ), pg.JSONB(val))} - - case *list_j5pb.FieldType_Range: - min := ft.Range.GetMin() - max := ft.Range.GetMax() - - switch { - case min != "" && max != "": - exprStr := fmt.Sprintf("jsonb_path_query_array(%s.%s, '%s ?? (@ >= $min && @ <= $max)', jsonb_build_object('min', ?::text, 'max', ?::text)) <> '[]'::jsonb", tableAlias, spec.RootColumn, spec.Path.JSONPathQuery()) - out = sq.And{sq.Expr(exprStr, min, max)} - case min != "": - exprStr := fmt.Sprintf("jsonb_path_query_array(%s.%s, '%s ?? (@ >= $min)', jsonb_build_object('min', ?::text)) <> '[]'::jsonb", tableAlias, spec.RootColumn, spec.Path.JSONPathQuery()) - out = sq.And{sq.Expr(exprStr, min)} - case max != "": - exprStr := fmt.Sprintf("jsonb_path_query_array(%s.%s, '%s ?? (@ <= $max)', jsonb_build_object('max', ?::text)) <> '[]'::jsonb", tableAlias, spec.RootColumn, spec.Path.JSONPathQuery()) - out = sq.And{sq.Expr(exprStr, max)} - } - } - - return out, nil -} - -func (ll *Lister[REQ, RES]) buildDynamicFilterOneof(tableAlias string, ospec *pgstore.NestedField, filter *list_j5pb.Filter) (sq.Sqlizer, error) { - var out sq.And - - if filter.GetField() == nil { - return nil, fmt.Errorf("dynamic filter: field is nil") - } - - switch ft := filter.GetField().GetType().Type.(type) { - case *list_j5pb.FieldType_Value: - val := ft.Value - - // Val is used directly here instead of passed in as an expression - // parameter. It has been sanitized by validation against the oneof - // field names. - exprStr := fmt.Sprintf("jsonb_array_length(jsonb_path_query_array(%s.%s, '%s ?? (exists(@.%s))')) > 0", tableAlias, ospec.RootColumn, ospec.Path.JSONPathQuery(), val) - out = sq.And{sq.Expr(exprStr)} - case *list_j5pb.FieldType_Range: - return nil, fmt.Errorf("oneofs cannot be filtered by range") - } - - return out, nil -} - -func validateQueryRequestFilters(message protoreflect.MessageDescriptor, filters []*list_j5pb.Filter) error { - for i := range filters { - switch filters[i].GetType().(type) { - case *list_j5pb.Filter_Field: - return validateQueryRequestFilterField(message, filters[i].GetField()) - case *list_j5pb.Filter_And: - return validateQueryRequestFilters(message, filters[i].GetAnd().GetFilters()) - case *list_j5pb.Filter_Or: - return validateQueryRequestFilters(message, filters[i].GetOr().GetFilters()) - } - } - - return nil -} - -func validateFiltersAnnotations(_ protoreflect.FieldDescriptors) error { - - // TODO: This existed pre refactor, check if it should do something. - return nil -} - -func validateQueryRequestFilterField(message protoreflect.MessageDescriptor, filterField *list_j5pb.Field) error { - - jsonPath := pgstore.ParseJSONPathSpec(filterField.GetName()) - spec, err := pgstore.NewJSONPath(message, jsonPath) - if err != nil { - return fmt.Errorf("find field: %w", err) - } - - // validate the fields are annotated correctly for the request query - // and the values are valid for the field - - switch leaf := spec.Leaf().(type) { - case protoreflect.OneofDescriptor: - filterable := false - - filterOpts := proto.GetExtension(leaf.Options().(*descriptorpb.OneofOptions), list_j5pb.E_Oneof).(*list_j5pb.OneofRules) - - if filterOpts == nil { - - fmt.Printf("No Filter Opts %s\n", filterField.GetName()) - wrapperField := spec.LeafOneofWrapper() - if wrapperField != nil { - fmt.Printf("WWWW %s\n", wrapperField.Name()) - fieldConstraint := proto.GetExtension(wrapperField.Options().(*descriptorpb.FieldOptions), list_j5pb.E_Field).(*list_j5pb.FieldConstraint) - if fieldConstraint != nil { - oneof := fieldConstraint.GetOneof() - if oneof != nil && oneof.Filtering != nil { - filterOpts = oneof - } - } - } - } - - if filterOpts != nil { - filterable = filterOpts.GetFiltering().Filterable - - if filterable { - switch filterField.Type.Type.(type) { - case *list_j5pb.FieldType_Value: - val := filterField.Type.GetValue() - - found := false - for i := range leaf.Fields().Len() { - f := leaf.Fields().Get(i) - if strings.EqualFold(string(f.Name()), val) { - found = true - break - } - } - - if !found { - return fmt.Errorf("filter value '%s' is not found in oneof '%s'", val, filterField.Name) - } - case *list_j5pb.FieldType_Range: - return fmt.Errorf("oneofs cannot be filtered by range") - } - } - } - - if !filterable { - return fmt.Errorf("requested filter field '%s' is not filterable", filterField.Name) - } - - return nil - case protoreflect.FieldDescriptor: - - filterOpts, ok := proto.GetExtension(leaf.Options().(*descriptorpb.FieldOptions), list_j5pb.E_Field).(*list_j5pb.FieldConstraint) - if !ok { - return fmt.Errorf("requested filter field '%s' does not have any filterable constraints defined", filterField.Name) - } - - filterable := false - - if filterOpts != nil { - switch leaf.Kind() { - case protoreflect.DoubleKind: - if filterOpts.GetDouble().Filtering != nil { - filterable = filterOpts.GetDouble().GetFiltering().Filterable - } - - case protoreflect.Fixed32Kind: - if filterOpts.GetFixed32().Filtering != nil { - filterable = filterOpts.GetFixed32().GetFiltering().Filterable - } - - case protoreflect.Fixed64Kind: - if filterOpts.GetFixed64().Filtering != nil { - filterable = filterOpts.GetFixed64().GetFiltering().Filterable - } - - case protoreflect.FloatKind: - if filterOpts.GetFloat().Filtering != nil { - filterable = filterOpts.GetFloat().GetFiltering().Filterable - } - case protoreflect.Int32Kind: - if filterOpts.GetInt32().Filtering != nil { - filterable = filterOpts.GetInt32().GetFiltering().Filterable - } - - case protoreflect.Int64Kind: - if filterOpts.GetInt64().Filtering != nil { - filterable = filterOpts.GetInt64().GetFiltering().Filterable - } - - case protoreflect.Sfixed32Kind: - if filterOpts.GetSfixed32().Filtering != nil { - filterable = filterOpts.GetSfixed32().GetFiltering().Filterable - } - - case protoreflect.Sfixed64Kind: - if filterOpts.GetSfixed64().Filtering != nil { - filterable = filterOpts.GetSfixed64().GetFiltering().Filterable - } - - case protoreflect.Sint32Kind: - if filterOpts.GetSint32().Filtering != nil { - filterable = filterOpts.GetSint32().GetFiltering().Filterable - } - - case protoreflect.Sint64Kind: - if filterOpts.GetSint64().Filtering != nil { - filterable = filterOpts.GetSint64().GetFiltering().Filterable - } - - case protoreflect.Uint32Kind: - if filterOpts.GetUint32().Filtering != nil { - filterable = filterOpts.GetUint32().GetFiltering().Filterable - } - - case protoreflect.Uint64Kind: - if filterOpts.GetUint64().Filtering != nil { - filterable = filterOpts.GetUint64().GetFiltering().Filterable - } - - case protoreflect.BoolKind: - if filterOpts.GetBool().Filtering != nil { - filterable = filterOpts.GetBool().GetFiltering().Filterable - } - - case protoreflect.EnumKind: - if filterOpts.GetEnum().Filtering != nil { - filterable = filterOpts.GetEnum().GetFiltering().Filterable - } - - case protoreflect.StringKind: - switch filterOpts.GetString_().WellKnown.(type) { - case *list_j5pb.StringRules_Date: - if filterOpts.GetString_().GetDate().Filtering != nil { - filterable = filterOpts.GetString_().GetDate().Filtering.Filterable - } - - case *list_j5pb.StringRules_ForeignKey: - switch filterOpts.GetString_().GetForeignKey().GetType().(type) { - case *list_j5pb.ForeignKeyRules_UniqueString: - if filterOpts.GetString_().GetForeignKey().GetUniqueString().Filtering != nil { - filterable = filterOpts.GetString_().GetForeignKey().GetUniqueString().Filtering.Filterable - } - - case *list_j5pb.ForeignKeyRules_Id62: - if filterOpts.GetString_().GetForeignKey().GetId62().Filtering != nil { - filterable = filterOpts.GetString_().GetForeignKey().GetId62().Filtering.Filterable - } - - case *list_j5pb.ForeignKeyRules_Uuid: - if filterOpts.GetString_().GetForeignKey().GetUuid().Filtering != nil { - filterable = filterOpts.GetString_().GetForeignKey().GetUuid().Filtering.Filterable - } - - } - } - case protoreflect.MessageKind: - if leaf.Message().FullName() == "google.protobuf.Timestamp" && filterOpts.GetTimestamp().Filtering != nil { - filterable = filterOpts.GetTimestamp().GetFiltering().Filterable - } - } - - if filterable { - switch filterField.Type.Type.(type) { - case *list_j5pb.FieldType_Value: - err := validateFilterFieldValue(filterOpts, leaf, filterField.Type.GetValue()) - if err != nil { - return fmt.Errorf("filter value: %w", err) - } - case *list_j5pb.FieldType_Range: - err := validateFilterFieldValue(filterOpts, leaf, filterField.Type.GetRange().GetMin()) - if err != nil { - return fmt.Errorf("filter min value: %w", err) - } - - err = validateFilterFieldValue(filterOpts, leaf, filterField.Type.GetRange().GetMax()) - if err != nil { - return fmt.Errorf("filter max value: %w", err) - } - } - } - } - - if !filterable { - return fmt.Errorf("requested filter field '%s' is not filterable", filterField.Name) - } - - return nil - default: - return fmt.Errorf("unknown leaf type %v", leaf) - } -} - -func validateFilterFieldValue(filterOpts *list_j5pb.FieldConstraint, field protoreflect.FieldDescriptor, value string) error { - if value == "" { - return nil - } - - switch field.Kind() { - case protoreflect.DoubleKind: - if filterOpts.GetDouble().GetFiltering().Filterable { - _, err := strconv.ParseFloat(value, 64) - if err != nil { - return fmt.Errorf("parsing double: %w", err) - } - } - case protoreflect.Fixed32Kind: - if filterOpts.GetFixed32().GetFiltering().Filterable { - _, err := strconv.ParseUint(value, 10, 32) - if err != nil { - return fmt.Errorf("parsing fixed32: %w", err) - } - } - case protoreflect.Fixed64Kind: - if filterOpts.GetFixed64().GetFiltering().Filterable { - _, err := strconv.ParseUint(value, 10, 64) - if err != nil { - return fmt.Errorf("parsing fixed64: %w", err) - } - } - case protoreflect.FloatKind: - if filterOpts.GetFloat().GetFiltering().Filterable { - _, err := strconv.ParseFloat(value, 32) - if err != nil { - return fmt.Errorf("parsing float: %w", err) - } - } - case protoreflect.Int32Kind: - if filterOpts.GetInt32().GetFiltering().Filterable { - _, err := strconv.ParseInt(value, 10, 32) - if err != nil { - return fmt.Errorf("parsing int32: %w", err) - } - } - case protoreflect.Int64Kind: - if filterOpts.GetInt64().GetFiltering().Filterable { - _, err := strconv.ParseInt(value, 10, 64) - if err != nil { - return fmt.Errorf("parsing int64: %w", err) - } - } - case protoreflect.Sfixed32Kind: - if filterOpts.GetSfixed32().GetFiltering().Filterable { - _, err := strconv.ParseInt(value, 10, 32) - if err != nil { - return fmt.Errorf("parsing sfixed32: %w", err) - } - } - case protoreflect.Sfixed64Kind: - if filterOpts.GetSfixed64().GetFiltering().Filterable { - _, err := strconv.ParseInt(value, 10, 64) - if err != nil { - return fmt.Errorf("parsing sfixed64: %w", err) - } - } - case protoreflect.Sint32Kind: - if filterOpts.GetSint32().GetFiltering().Filterable { - _, err := strconv.ParseInt(value, 10, 32) - if err != nil { - return fmt.Errorf("parsing sint32: %w", err) - } - } - case protoreflect.Sint64Kind: - if filterOpts.GetSint64().GetFiltering().Filterable { - _, err := strconv.ParseInt(value, 10, 64) - if err != nil { - return fmt.Errorf("parsing sint64: %w", err) - } - } - case protoreflect.Uint32Kind: - if filterOpts.GetUint32().GetFiltering().Filterable { - _, err := strconv.ParseUint(value, 10, 32) - if err != nil { - return fmt.Errorf("parsing uint32: %w", err) - } - } - case protoreflect.Uint64Kind: - if filterOpts.GetUint64().GetFiltering().Filterable { - _, err := strconv.ParseUint(value, 10, 64) - if err != nil { - return fmt.Errorf("parsing uint64: %w", err) - } - } - case protoreflect.BoolKind: - if filterOpts.GetBool().GetFiltering().Filterable { - _, err := strconv.ParseBool(value) - if err != nil { - return fmt.Errorf("parsing bool: %w", err) - } - } - case protoreflect.EnumKind: - if filterOpts.GetEnum().GetFiltering().Filterable { - name := strings.ToTitle(value) - prefix := strings.TrimSuffix(string(field.Enum().Values().Get(0).Name()), "_UNSPECIFIED") - - if !strings.HasPrefix(value, prefix) { - name = prefix + "_" + name - } - eval := field.Enum().Values().ByName(protoreflect.Name(name)) - - if eval == nil { - return fmt.Errorf("enum value %s is not a valid enum value for field", value) - } - } - case protoreflect.StringKind: - switch filterOpts.GetString_().WellKnown.(type) { - case *list_j5pb.StringRules_Date: - if filterOpts.GetString_().GetDate().Filtering.Filterable { - _, err := time.Parse("2006-01-02", value) - if err != nil { - _, err = time.Parse("2006-01", value) - if err != nil { - _, err = time.Parse("2006", value) - if err != nil { - return fmt.Errorf("parsing date: %w", err) - } - } - } - } - case *list_j5pb.StringRules_ForeignKey: - switch filterOpts.GetString_().GetForeignKey().GetType().(type) { - case *list_j5pb.ForeignKeyRules_Uuid: - if filterOpts.GetString_().GetForeignKey().GetUuid().Filtering.Filterable { - _, err := uuid.Parse(value) - if err != nil { - return fmt.Errorf("parsing uuid: %w", err) - } - } - } - } - case protoreflect.MessageKind: - if field.Message().FullName() == "google.protobuf.Timestamp" { - if filterOpts.GetTimestamp().GetFiltering().Filterable { - _, err := time.Parse(time.RFC3339, value) - if err != nil { - return fmt.Errorf("parsing timestamp: %w", err) - } - } - } - } - - return nil -} diff --git a/pquery/getter.go b/pquery/getter.go deleted file mode 100644 index d05b850..0000000 --- a/pquery/getter.go +++ /dev/null @@ -1,343 +0,0 @@ -package pquery - -import ( - "context" - "database/sql" - "errors" - "fmt" - "strings" - - "buf.build/go/protovalidate" - sq "github.com/elgris/sqrl" - "github.com/lib/pq" - "github.com/pentops/protostate/internal/dbconvert" - "github.com/pentops/sqrlx.go/sqrlx" - "google.golang.org/grpc/codes" - "google.golang.org/grpc/status" - "google.golang.org/protobuf/encoding/protojson" - "google.golang.org/protobuf/proto" - "google.golang.org/protobuf/reflect/protoreflect" -) - -type GetRequest interface { - proto.Message -} - -type GetResponse interface { - proto.Message -} - -type GetSpec[ - REQ GetRequest, - RES GetResponse, -] struct { - TableName string - DataColumn string - Auth AuthProvider - AuthJoin []*LeftJoin - - PrimaryKey func(REQ) (map[string]any, error) - - StateResponseField protoreflect.Name - - Join *GetJoinSpec -} - -// JoinConstraint defines a -// LEFT JOIN -// ON . = . -type JoinField struct { - JoinColumn string // The name of the column in the table being introduced - RootColumn string // The name of the column in the root table -} - -type JoinFields []JoinField - -func (jc JoinFields) Reverse() JoinFields { - out := make(JoinFields, 0, len(jc)) - for _, c := range jc { - out = append(out, JoinField{ - JoinColumn: c.RootColumn, - RootColumn: c.JoinColumn, - }) - } - return out -} - -func (jc JoinFields) SQL(rootAlias string, joinAlias string) string { - conditions := make([]string, 0, len(jc)) - for _, c := range jc { - conditions = append(conditions, fmt.Sprintf("%s.%s = %s.%s", - joinAlias, - c.JoinColumn, - rootAlias, - c.RootColumn, - )) - } - return strings.Join(conditions, " AND ") -} - -type GetJoinSpec struct { - TableName string - DataColumn string - On JoinFields - FieldInParent protoreflect.Name -} - -func (gc GetJoinSpec) validate() error { - if gc.TableName == "" { - return fmt.Errorf("missing TableName") - } - if gc.DataColumn == "" { - return fmt.Errorf("missing DataColumn") - } - if gc.On == nil { - return fmt.Errorf("missing On") - } - - return nil -} - -type Getter[ - REQ GetRequest, - RES proto.Message, -] struct { - stateField protoreflect.FieldDescriptor - - dataColumn string - tableName string - primaryKey func(REQ) (map[string]any, error) - auth AuthProvider - authJoin []*LeftJoin - - queryLogger QueryLogger - - validator protovalidate.Validator - - join *getJoin -} - -type getJoin struct { - dataColumn string - tableName string - fieldInParent protoreflect.FieldDescriptor // wraps the ListFooEventResponse type - on JoinFields -} - -func NewGetter[ - REQ GetRequest, - RES GetResponse, -](spec GetSpec[REQ, RES]) (*Getter[REQ, RES], error) { - descriptors := newMethodDescriptor[REQ, RES]() - resDesc := descriptors.response - - sc := &Getter[REQ, RES]{ - dataColumn: spec.DataColumn, - tableName: spec.TableName, - primaryKey: spec.PrimaryKey, - auth: spec.Auth, - authJoin: spec.AuthJoin, - } - - // TODO: Use an annotation not a passed in name - defaultState := false - if spec.StateResponseField == "" { - defaultState = true - spec.StateResponseField = protoreflect.Name("state") - } - sc.stateField = resDesc.Fields().ByName(spec.StateResponseField) - if sc.stateField == nil { - if defaultState { - return nil, fmt.Errorf("no 'state' field in proto message - did you mean to override StateResponseField?") - } - return nil, fmt.Errorf("no '%s' field in proto message", spec.StateResponseField) - } - - if spec.PrimaryKey == nil { - return nil, fmt.Errorf("missing PrimaryKey func") - } - - if spec.Join != nil { - - if err := spec.Join.validate(); err != nil { - return nil, fmt.Errorf("invalid join spec: %w", err) - } - - joinField := resDesc.Fields().ByName(protoreflect.Name(spec.Join.FieldInParent)) - if joinField == nil { - return nil, fmt.Errorf("field %s not found in response message", spec.Join.FieldInParent) - } - - if !joinField.IsList() { - return nil, fmt.Errorf("field %s, in join spec, is not a list", spec.Join.FieldInParent) - } - - sc.join = &getJoin{ - tableName: spec.Join.TableName, - dataColumn: spec.Join.DataColumn, - fieldInParent: joinField, - on: spec.Join.On, - } - } - - var err error - sc.validator, err = protovalidate.New() - if err != nil { - return nil, fmt.Errorf("failed to initialize validator: %w", err) - } - - return sc, nil -} - -func (gc *Getter[REQ, RES]) SetQueryLogger(logger QueryLogger) { - gc.queryLogger = logger -} - -func (gc *Getter[REQ, RES]) Get(ctx context.Context, db Transactor, reqMsg REQ, resMsg RES) error { - - as := newAliasSet() - rootAlias := as.Next(gc.tableName) - - resReflect := resMsg.ProtoReflect() - - if err := gc.validator.Validate(reqMsg); err != nil { - return err - } - - primaryKeyFields, err := gc.primaryKey(reqMsg) - if err != nil { - return err - } - - rootFilter, err := dbconvert.FieldsToEqMap(rootAlias, primaryKeyFields) - if err != nil { - return err - } - - selectQuery := sq. - Select(). - Column(fmt.Sprintf("%s.%s", rootAlias, gc.dataColumn)). - From(fmt.Sprintf("%s AS %s", gc.tableName, rootAlias)). - Where(rootFilter) - - for pkField := range rootFilter { - selectQuery.GroupBy(pkField) - } - - if gc.auth != nil { - authAlias := rootAlias - for _, join := range gc.authJoin { - priorAlias := authAlias - authAlias = as.Next(join.TableName) - selectQuery = selectQuery.LeftJoin(fmt.Sprintf( - "%s AS %s ON %s", - join.TableName, - authAlias, - join.On.SQL(priorAlias, authAlias), - )) - } - - authFilter, err := gc.auth.AuthFilter(ctx) - if err != nil { - return err - } - - if len(authFilter) > 0 { - claimFilter := map[string]any{} - for k, v := range authFilter { - claimFilter[fmt.Sprintf("%s.%s", authAlias, k)] = v - } - selectQuery.Where(claimFilter) - } - } - - if gc.join != nil { - joinAlias := as.Next(gc.join.tableName) - - selectQuery. - Column(fmt.Sprintf("ARRAY_AGG(%s.%s)", joinAlias, gc.join.dataColumn)). - LeftJoin(fmt.Sprintf( - "%s AS %s ON %s", - gc.join.tableName, - joinAlias, - gc.join.on.SQL(rootAlias, joinAlias), - )) - } - - var foundJSON []byte - var joinedJSON pq.StringArray - - if gc.queryLogger != nil { - gc.queryLogger(selectQuery) - } - - if err := db.Transact(ctx, &sqrlx.TxOptions{ - ReadOnly: true, - Retryable: true, - Isolation: sql.LevelReadCommitted, - }, func(ctx context.Context, tx sqrlx.Transaction) error { - row := tx.SelectRow(ctx, selectQuery) - - var err error - if gc.join != nil { - err = row.Scan(&foundJSON, &joinedJSON) - } else { - err = row.Scan(&foundJSON) - } - if err != nil { - return err - } - - return nil - }); err != nil { - - if errors.Is(err, sql.ErrNoRows) { - var pkDescription string - if len(primaryKeyFields) == 1 { - for _, v := range primaryKeyFields { - pkDescription = fmt.Sprintf("%v", v) - } - } else { - all := make([]string, 0, len(primaryKeyFields)) - for k, v := range primaryKeyFields { - all = append(all, fmt.Sprintf("%s=%v", k, v)) - } - pkDescription = strings.Join(all, ", ") - } - - return status.Errorf(codes.NotFound, "entity %s not found", pkDescription) - } - query, _, _ := selectQuery.ToSql() - - return fmt.Errorf("%s: %w", query, err) - } - - if foundJSON == nil { - return status.Error(codes.NotFound, "not found") - } - - stateMsg := resReflect.NewField(gc.stateField) - if err := protojson.Unmarshal(foundJSON, stateMsg.Message().Interface()); err != nil { - return err - } - resReflect.Set(gc.stateField, stateMsg) - - if gc.join != nil { - elementList := resReflect.Mutable(gc.join.fieldInParent).List() - for _, eventBytes := range joinedJSON { - if eventBytes == "" { - continue - } - - rowMessage := elementList.NewElement().Message() - if err := protojson.Unmarshal([]byte(eventBytes), rowMessage.Interface()); err != nil { - return fmt.Errorf("joined unmarshal: %w", err) - } - elementList.Append(protoreflect.ValueOf(rowMessage)) - } - - } - - return nil - -} diff --git a/pquery/lister.go b/pquery/lister.go deleted file mode 100644 index dd59eee..0000000 --- a/pquery/lister.go +++ /dev/null @@ -1,698 +0,0 @@ -package pquery - -import ( - "context" - "database/sql" - "encoding/base64" - "fmt" - "regexp" - "strings" - "time" - "unicode" - - "buf.build/gen/go/bufbuild/protovalidate/protocolbuffers/go/buf/validate" - "buf.build/go/protovalidate" - sq "github.com/elgris/sqrl" - "github.com/elgris/sqrl/pg" - "github.com/pentops/j5/gen/j5/list/v1/list_j5pb" - "github.com/pentops/log.go/log" - "github.com/pentops/protostate/internal/pgstore" - "github.com/pentops/sqrlx.go/sqrlx" - "google.golang.org/grpc/codes" - "google.golang.org/grpc/status" - "google.golang.org/protobuf/encoding/protojson" - "google.golang.org/protobuf/proto" - "google.golang.org/protobuf/reflect/protoreflect" - "google.golang.org/protobuf/types/descriptorpb" - "google.golang.org/protobuf/types/dynamicpb" - "google.golang.org/protobuf/types/known/timestamppb" -) - -var dateRegex = regexp.MustCompile(`^\d{4}(-\d{2}(-\d{2})?)?$`) - -type ListRequest interface { - proto.Message -} - -type ListResponse interface { - proto.Message -} - -type TableSpec struct { - TableName string - - Auth AuthProvider - AuthJoin []*LeftJoin - - DataColumn string // TODO: Replace with array Columns []Column - - // List of fields to sort by if no other unique sort is found. - FallbackSortColumns []ProtoField -} - -// ProtoField represents a field within a the root data. -type ProtoField struct { - // path from the root object to this field - pathInRoot pgstore.ProtoPathSpec - - // optional column name when the field is also stored as a scalar directly - valueColumn *string -} - -func NewProtoField(protoPath string, columnName *string) ProtoField { - pp := pgstore.ParseProtoPathSpec(protoPath) - return ProtoField{ - valueColumn: columnName, - pathInRoot: pp, - } -} - -type Column struct { - Name string - - // The point within the root element which is stored in the column. An empty - // path means this stores the root element, - MountPoint *pgstore.Path -} - -type ListSpec[REQ ListRequest, RES ListResponse] struct { - TableSpec - RequestFilter func(REQ) (map[string]any, error) -} - -type QueryLogger func(sqrlx.Sqlizer) - -type ListReflectionSet struct { - defaultPageSize uint64 - - arrayField protoreflect.FieldDescriptor - pageResponseField protoreflect.FieldDescriptor - pageRequestField protoreflect.FieldDescriptor - queryRequestField protoreflect.FieldDescriptor - - defaultSortFields []sortSpec - tieBreakerFields []sortSpec - - defaultFilterFields []filterSpec - RequestFilterFields []protoreflect.FieldDescriptor - - tsvColumnMap map[string]string - - // TODO: This should be an array/map of columns to data types, allowing - // multiple JSONB values, as well as cached field values direcrly on the - // table - dataColumn string -} - -func BuildListReflection(req protoreflect.MessageDescriptor, res protoreflect.MessageDescriptor, table TableSpec) (*ListReflectionSet, error) { - return buildListReflection(req, res, table) -} - -func buildListReflection(req protoreflect.MessageDescriptor, res protoreflect.MessageDescriptor, table TableSpec) (*ListReflectionSet, error) { - var err error - ll := ListReflectionSet{ - defaultPageSize: uint64(20), - dataColumn: table.DataColumn, - } - fields := res.Fields() - - for i := range fields.Len() { - field := fields.Get(i) - msg := field.Message() - if msg == nil { - return nil, fmt.Errorf("field %s is a '%s', but should be a message", field.Name(), field.Kind()) - } - - if msg.FullName() == "j5.list.v1.PageResponse" { - ll.pageResponseField = field - continue - } - - if field.Cardinality() == protoreflect.Repeated { - if ll.arrayField != nil { - return nil, fmt.Errorf("multiple repeated fields (%s and %s)", ll.arrayField.Name(), field.Name()) - } - - ll.arrayField = field - continue - } - - return nil, fmt.Errorf("unknown field in response: '%s' of type %s", field.Name(), field.Kind()) - } - - if ll.arrayField == nil { - return nil, fmt.Errorf("no repeated field in response, %s must have a repeated message", res.FullName()) - } - - if ll.pageResponseField == nil { - return nil, fmt.Errorf("no page field in response, %s must have a j5.list.v1.PageResponse", res.FullName()) - } - - err = validateListAnnotations(ll.arrayField.Message().Fields()) - if err != nil { - return nil, fmt.Errorf("validate list annotations on %s: %w", ll.arrayField.Message().FullName(), err) - } - - ll.defaultSortFields, err = buildDefaultSorts(ll.dataColumn, ll.arrayField.Message()) - if err != nil { - return nil, fmt.Errorf("default sorts: %w", err) - } - - ll.tieBreakerFields, err = buildTieBreakerFields(ll.dataColumn, req, ll.arrayField.Message(), table.FallbackSortColumns) - if err != nil { - return nil, fmt.Errorf("tie breaker fields: %w", err) - } - - if len(ll.defaultSortFields) == 0 && len(ll.tieBreakerFields) == 0 { - return nil, fmt.Errorf("no default sort field found, %s must have at least one field annotated as default sort, or specify a tie breaker in %s", ll.arrayField.Message().FullName(), req.FullName()) - } - - f, err := buildDefaultFilters(ll.dataColumn, ll.arrayField.Message()) - if err != nil { - return nil, fmt.Errorf("default filters: %w", err) - } - - ll.defaultFilterFields = f - - ll.tsvColumnMap = buildTsvColumnMap(ll.arrayField.Message()) - - requestFields := req.Fields() - for i := range requestFields.Len() { - field := requestFields.Get(i) - msg := field.Message() - if msg != nil { - switch msg.FullName() { - case "j5.list.v1.PageRequest": - ll.pageRequestField = field - continue - case "j5.list.v1.QueryRequest": - ll.queryRequestField = field - continue - case "j5.types.date.v1.Date": - ll.RequestFilterFields = append(ll.RequestFilterFields, field) - continue - default: - return nil, fmt.Errorf("unknown field in request: '%s' of type %s", field.Name(), field.Kind()) - } - } - - // Assume this is a filter field - switch field.Kind() { - case protoreflect.StringKind: - ll.RequestFilterFields = append(ll.RequestFilterFields, field) - case protoreflect.BoolKind: - ll.RequestFilterFields = append(ll.RequestFilterFields, field) - default: - return nil, fmt.Errorf("unsupported filter field in request: '%s' of type %s", field.Name(), field.Kind()) - } - } - - if ll.pageRequestField == nil { - return nil, fmt.Errorf("no page field in request, %s must have a j5.list.v1.PageRequest", req.FullName()) - } - - if ll.queryRequestField == nil { - return nil, fmt.Errorf("no query field in request, %s must have a j5.list.v1.QueryRequest", req.FullName()) - } - - arrayFieldOpt := ll.arrayField.Options().(*descriptorpb.FieldOptions) - validateOpt := proto.GetExtension(arrayFieldOpt, validate.E_Field).(*validate.FieldRules) - if repeated := validateOpt.GetRepeated(); repeated != nil { - if repeated.MaxItems != nil { - ll.defaultPageSize = *repeated.MaxItems - } - } - - return &ll, nil -} - -type Lister[REQ ListRequest, RES ListResponse] struct { - ListReflectionSet - - tableName string - - queryLogger QueryLogger - - auth AuthProvider - authJoin []*LeftJoin - - requestFilter func(REQ) (map[string]any, error) - - validator protovalidate.Validator -} - -func NewLister[ - REQ ListRequest, - RES ListResponse, -](spec ListSpec[REQ, RES]) (*Lister[REQ, RES], error) { - ll := &Lister[REQ, RES]{ - tableName: spec.TableName, - auth: spec.Auth, - authJoin: spec.AuthJoin, - } - - descriptors := newMethodDescriptor[REQ, RES]() - - listFields, err := buildListReflection(descriptors.request, descriptors.response, spec.TableSpec) - if err != nil { - return nil, err - } - ll.ListReflectionSet = *listFields - - ll.requestFilter = spec.RequestFilter - - ll.validator, err = protovalidate.New() - if err != nil { - return nil, fmt.Errorf("failed to initialize validator: %w", err) - } - - return ll, nil -} - -func (ll *Lister[REQ, RES]) SetQueryLogger(logger QueryLogger) { - ll.queryLogger = logger -} - -func (ll *Lister[REQ, RES]) List(ctx context.Context, db Transactor, reqMsg proto.Message, resMsg proto.Message) error { - if err := ll.validator.Validate(reqMsg); err != nil { - return fmt.Errorf("validating request %s: %w", reqMsg.ProtoReflect().Descriptor().FullName(), err) - } - - res := resMsg.ProtoReflect() - req := reqMsg.ProtoReflect() - - pageSize, err := ll.getPageSize(req) - if err != nil { - return fmt.Errorf("get page size: %w", err) - } - - selectQuery, err := ll.BuildQuery(ctx, req, res) - if err != nil { - return fmt.Errorf("build query: %w", err) - } - - txOpts := &sqrlx.TxOptions{ - ReadOnly: true, - Retryable: true, - Isolation: sql.LevelReadCommitted, - } - - var jsonRows = make([][]byte, 0, pageSize) - err = db.Transact(ctx, txOpts, func(ctx context.Context, tx sqrlx.Transaction) error { - rows, err := tx.Query(ctx, selectQuery) - if err != nil { - return fmt.Errorf("run select: %w", err) - } - defer rows.Close() - - for rows.Next() { - var json []byte - if err := rows.Scan(&json); err != nil { - return fmt.Errorf("row scan: %w", err) - } - - jsonRows = append(jsonRows, json) - } - - return rows.Err() - }) - if err != nil { - stmt, _, _ := selectQuery.ToSql() - log.WithField(ctx, "query", stmt).Error("list query") - return fmt.Errorf("list query: %w", err) - } - - if ll.queryLogger != nil { - ll.queryLogger(selectQuery) - } - - list := res.Mutable(ll.arrayField).List() - res.Set(ll.arrayField, protoreflect.ValueOf(list)) - - var nextToken string - for idx, rowBytes := range jsonRows { - rowMessage := list.NewElement().Message() - - err := protojson.Unmarshal(rowBytes, rowMessage.Interface()) - if err != nil { - return fmt.Errorf("unmarshal into %s from %s: %w", rowMessage.Descriptor().FullName(), string(rowBytes), err) - } - - if idx >= int(pageSize) { - // TODO: This works but the token is huge. - // The eventual solution will need to look at - // the sorting and filtering of the query and either encode them - // directly, or encode a subset of the message as required. - lastBytes, err := proto.Marshal(rowMessage.Interface()) - if err != nil { - return fmt.Errorf("marshalling final row: %w", err) - } - - nextToken = base64.StdEncoding.EncodeToString(lastBytes) - break - } - - list.Append(protoreflect.ValueOf(rowMessage)) - } - - if nextToken != "" { - pageResponse := &list_j5pb.PageResponse{ - NextToken: &nextToken, - } - - res.Set(ll.pageResponseField, protoreflect.ValueOf(pageResponse.ProtoReflect())) - } - - return nil -} - -func (ll *Lister[REQ, RES]) BuildQuery(ctx context.Context, req protoreflect.Message, res protoreflect.Message) (*sq.SelectBuilder, error) { - as := newAliasSet() - tableAlias := as.Next(ll.tableName) - - selectQuery := sq.Select(fmt.Sprintf("%s.%s", tableAlias, ll.dataColumn)). - From(fmt.Sprintf("%s AS %s", ll.tableName, tableAlias)) - - sortFields := ll.defaultSortFields - sortFields = append(sortFields, ll.tieBreakerFields...) - - filterFields := []sq.Sqlizer{} - if ll.requestFilter != nil { - filter, err := ll.requestFilter(req.Interface().(REQ)) - if err != nil { - return nil, err - } - - and := sq.And{} - for k := range filter { - and = append(and, sq.Expr(fmt.Sprintf("%s.%s = ?", tableAlias, k), filter[k])) - } - - if len(and) > 0 { - selectQuery.Where(and) - } - } - - reqQuery, ok := req.Get(ll.queryRequestField).Message().Interface().(*list_j5pb.QueryRequest) - if ok && reqQuery != nil { - if err := ll.validateQueryRequest(reqQuery); err != nil { - return nil, status.Errorf(codes.InvalidArgument, "query validation: %s", err) - } - - querySorts := reqQuery.GetSorts() - if len(querySorts) > 0 { - dynSorts, err := ll.buildDynamicSortSpec(querySorts) - if err != nil { - return nil, status.Errorf(codes.InvalidArgument, "build sorts: %s", err) - } - - sortFields = dynSorts - } - - queryFilters := reqQuery.GetFilters() - if len(queryFilters) > 0 { - dynFilters, err := ll.buildDynamicFilter(tableAlias, queryFilters) - if err != nil { - return nil, status.Errorf(codes.InvalidArgument, "build filters: %s", err) - } - - filterFields = append(filterFields, dynFilters...) - } - - querySearches := reqQuery.GetSearches() - if len(querySearches) > 0 { - searchFilters, err := ll.buildDynamicSearches(tableAlias, querySearches) - if err != nil { - return nil, status.Errorf(codes.InvalidArgument, "build searches: %s", err) - } - - filterFields = append(filterFields, searchFilters...) - } - } - - for i := range filterFields { - selectQuery.Where(filterFields[i]) - } - - // apply default filters if no filters have been requested - if ll.defaultFilterFields != nil && len(filterFields) == 0 { - and := sq.And{} - for _, spec := range ll.defaultFilterFields { - or := sq.Or{} - for _, val := range spec.filterVals { - or = append(or, sq.Expr(fmt.Sprintf("jsonb_path_query_array(%s.%s, '%s') @> ?", tableAlias, ll.dataColumn, spec.Path.JSONPathQuery()), pg.JSONB(val))) - } - - and = append(and, or) - } - - if len(and) > 0 { - selectQuery.Where(and) - } - } - - for _, sortField := range sortFields { - direction := "ASC" - if sortField.desc { - direction = "DESC" - } - selectQuery.OrderBy(fmt.Sprintf("%s %s", sortField.Selector(tableAlias), direction)) - } - - if ll.auth != nil { - authAlias := tableAlias - for _, join := range ll.authJoin { - priorAlias := authAlias - authAlias = as.Next(join.TableName) - selectQuery = selectQuery.LeftJoin(fmt.Sprintf( - "%s AS %s ON %s", - join.TableName, - authAlias, - join.On.SQL(priorAlias, authAlias), - )) - } - - authFilter, err := ll.auth.AuthFilter(ctx) - if err != nil { - return nil, err - } - - if len(authFilter) > 0 { - claimFilter := map[string]any{} - for k, v := range authFilter { - claimFilter[fmt.Sprintf("%s.%s", authAlias, k)] = v - } - selectQuery.Where(claimFilter) - } - } - - pageSize, err := ll.getPageSize(req) - if err != nil { - return nil, err - } - - selectQuery.Limit(pageSize + 1) - - reqPage, ok := req.Get(ll.pageRequestField).Message().Interface().(*list_j5pb.PageRequest) - if ok && reqPage != nil && reqPage.GetToken() != "" { - rowMessage := dynamicpb.NewMessage(ll.arrayField.Message()) - - rowBytes, err := base64.StdEncoding.DecodeString(reqPage.GetToken()) - if err != nil { - return nil, fmt.Errorf("decode token: %w", err) - } - - if err := proto.Unmarshal(rowBytes, rowMessage.Interface()); err != nil { - return nil, fmt.Errorf("unmarshal into %s from %s: %w", rowMessage.Descriptor().FullName(), string(rowBytes), err) - } - - lhsFields := make([]string, 0, len(sortFields)) - rhsValues := make([]any, 0, len(sortFields)) - rhsPlaceholders := make([]string, 0, len(sortFields)) - - for _, sortField := range sortFields { - rowSelecter := sortField.Selector(tableAlias) - valuePlaceholder := "?" - - fieldVal, err := sortField.Path.GetValue(rowMessage) - if err != nil { - return nil, fmt.Errorf("sort field %s: %w", sortField.errorName(), err) - } - - dbVal := fieldVal.Interface() - switch subType := dbVal.(type) { - case *dynamicpb.Message: - name := subType.Descriptor().FullName() - msgBytes, err := proto.Marshal(subType) - if err != nil { - return nil, fmt.Errorf("marshal %s: %w", name, err) - } - - switch name { - case "google.protobuf.Timestamp": - ts := timestamppb.Timestamp{} - if err := proto.Unmarshal(msgBytes, &ts); err != nil { - return nil, fmt.Errorf("unmarshal %s: %w", name, err) - } - intVal := ts.AsTime().Round(time.Microsecond).UnixMicro() - // Go rounds half-up. - // Postgres is undocumented, but can only handle - // microseconds. - rowSelecter = fmt.Sprintf("(EXTRACT(epoch FROM (%s)::timestamp) * 1000000)::bigint", rowSelecter) - if sortField.desc { - intVal = intVal * -1 - rowSelecter = fmt.Sprintf("-1 * %s", rowSelecter) - } - dbVal = intVal - default: - return nil, fmt.Errorf("sort field %s is a message of type %s", sortField.errorName(), name) - } - - case string: - dbVal = subType - if sortField.desc { - // String fields aren't valid for sorting, they can only be used - // for the tie-breaker so the order itself is not important, only - // that it is consistent - return nil, fmt.Errorf("sort field %s is a string, strings cannot be sorted DESC", sortField.errorName()) - } - - case int64: - if sortField.desc { - dbVal = dbVal.(int64) * -1 - rowSelecter = fmt.Sprintf("-1 * (%s)::bigint", rowSelecter) - } - case int32: - if sortField.desc { - dbVal = dbVal.(int32) * -1 - rowSelecter = fmt.Sprintf("-1 * (%s)::integer", rowSelecter) - } - case float32: - if sortField.desc { - dbVal = dbVal.(float32) * -1 - rowSelecter = fmt.Sprintf("-1 * (%s)::real", rowSelecter) - } - case float64: - if sortField.desc { - dbVal = dbVal.(float64) * -1 - rowSelecter = fmt.Sprintf("-1 * (%s)::double precision", rowSelecter) - } - case bool: - if sortField.desc { - dbVal = !dbVal.(bool) - rowSelecter = fmt.Sprintf("NOT (%s)::boolean", rowSelecter) - } - - // TODO: Reversals for the other types that are sortable - - default: - return nil, fmt.Errorf("sort field %s is of type %T", sortField.errorName(), dbVal) - } - - lhsFields = append(lhsFields, rowSelecter) - rhsValues = append(rhsValues, dbVal) - rhsPlaceholders = append(rhsPlaceholders, valuePlaceholder) - } - - // From https://www.postgresql.org/docs/current/functions-comparisons.html#ROW-WISE-COMPARISON - // >> for the <, <=, > and >= cases, the row elements are compared left-to-right, stopping as soon - // >> as an unequal or null pair of elements is found. If either of this pair of elements is null, - // >> the result of the row comparison is unknown (null); otherwise comparison of this pair of elements - // >> determines the result. For example, ROW(1,2,NULL) < ROW(1,3,0) yields true, not null, because the - // >> third pair of elements are not considered. - // - // This means that we can use the row comparison with the same fields as - // the sort fields to exclude the exact record we want, rather than the - // filter being applied to all fields equally which takes out valid - // records. - // `(1, 30) >= (1, 20)` is true, so is `1 >= 1 AND 30 >= 20` - // `(2, 10) >= (1, 20)` is also true, but `2 >= 1 AND 10 >= 20` is false - // Since the tuple comparison starts from the left and stops at the first term. - // - // The downside is that we have to negate the values to sort in reverse - // order, as we don't get an operator per term. This gets strange for - // some data types and will create some crazy indexes. - // - // TODO: Optimize the cases when the order is ASC and therefore we don't - // need to flip, but also the cases where we can just reverse the whole - // comparison and reverse all flips to simplify, noting again that it - // does not actually matter in which order the string field is sorted... - // or don't because indexes. - - selectQuery = selectQuery.Where( - fmt.Sprintf("(%s) >= (%s)", - strings.Join(lhsFields, ","), - strings.Join(rhsPlaceholders, ","), - ), rhsValues...) - } - - return selectQuery, nil -} - -func (ll *Lister[REQ, RES]) getPageSize(req protoreflect.Message) (uint64, error) { - pageSize := ll.defaultPageSize - - pageReq, ok := req.Get(ll.pageRequestField).Message().Interface().(*list_j5pb.PageRequest) - if ok && pageReq != nil && pageReq.PageSize != nil { - pageSize = uint64(*pageReq.PageSize) - - if pageSize > ll.defaultPageSize { - return 0, fmt.Errorf("page size exceeds the maximum allowed size of %d", ll.defaultPageSize) - } - } - - return pageSize, nil -} - -func camelToSnake(jsonName string) string { - var out strings.Builder - for i, r := range jsonName { - if unicode.IsUpper(r) { - if i > 0 { - out.WriteRune('_') - } - out.WriteRune(unicode.ToLower(r)) - } else { - out.WriteRune(r) - } - } - return out.String() -} - -func validateListAnnotations(fields protoreflect.FieldDescriptors) error { - err := validateSortsAnnotations(fields) - if err != nil { - return fmt.Errorf("sort: %w", err) - } - - err = validateFiltersAnnotations(fields) - if err != nil { - return fmt.Errorf("filter: %w", err) - } - - err = validateSearchesAnnotations(fields) - if err != nil { - return fmt.Errorf("search: %w", err) - } - - return nil -} - -func (ll *Lister[REQ, RES]) validateQueryRequest(query *list_j5pb.QueryRequest) error { - err := validateQueryRequestSorts(ll.arrayField.Message(), query.GetSorts()) - if err != nil { - return fmt.Errorf("sort validation: %w", err) - } - - err = validateQueryRequestFilters(ll.arrayField.Message(), query.GetFilters()) - if err != nil { - return fmt.Errorf("filter validation: %w", err) - } - - err = validateQueryRequestSearches(ll.arrayField.Message(), query.GetSearches()) - if err != nil { - return fmt.Errorf("search validation: %w", err) - } - - return nil -} diff --git a/pquery/lister_test.go b/pquery/lister_test.go deleted file mode 100644 index e8522a4..0000000 --- a/pquery/lister_test.go +++ /dev/null @@ -1,460 +0,0 @@ -package pquery - -import ( - "strings" - "testing" - - "github.com/pentops/flowtest/prototest" - "github.com/pentops/golib/gl" - "github.com/pentops/protostate/internal/pgstore" - "github.com/stretchr/testify/assert" - "google.golang.org/protobuf/reflect/protoreflect" -) - -type composed struct { - FooListRequest string - FooListResponse string - Foo string -} - -func (c composed) toString() string { - out := "" - if c.FooListRequest != "" { - out += "message FooListRequest {\n" + c.FooListRequest + "\n}\n" - } else { - out += `message FooListRequest { - j5.list.v1.PageRequest page = 1; - j5.list.v1.QueryRequest query = 2; - - option (j5.list.v1.list_request) = { - sort_tiebreaker: ["id"] - }; - }` - } - - if c.FooListResponse != "" { - out += "message FooListResponse {\n" + c.FooListResponse + "\n}\n" - } else { - out += `message FooListResponse { - repeated Foo foos = 1; - j5.list.v1.PageResponse page = 2; - }` - } - - if c.Foo != "" { - out += "message Foo {\n" + c.Foo + "\n}\n" - } else { - out += `message Foo { - string id = 1; - }` - } - return out -} - -func TestBuildListReflection(t *testing.T) { - - type tableMod func(t testing.TB, spec *TableSpec, req, res protoreflect.MessageDescriptor) - - build := func(t testing.TB, input string, spec tableMod) (*ListReflectionSet, error) { - pdf := prototest.DescriptorsFromSource(t, map[string]string{ - "test.proto": ` - syntax = "proto3"; - - package test; - - // Import everything which may be used - import "j5/ext/v1/annotations.proto"; - import "j5/list/v1/page.proto"; - import "j5/list/v1/query.proto"; - import "j5/list/v1/annotations.proto"; - import "j5/types/date/v1/date.proto"; - import "buf/validate/validate.proto"; - import "google/protobuf/timestamp.proto"; - ` + input, - }) - - requestDesc := pdf.MessageByName(t, "test.FooListRequest") - responseDesc := pdf.MessageByName(t, "test.FooListResponse") - - table := &TableSpec{} - if spec != nil { - spec(t, table, requestDesc, responseDesc) - } - - return buildListReflection(requestDesc, responseDesc, *table) - } - - runHappy := func(name string, input string, spec tableMod, callback func(*testing.T, *ListReflectionSet)) { - t.Run(name, func(t *testing.T) { - t.Helper() - set, err := build(t, input, spec) - - if err != nil { - t.Fatal(err) - } - callback(t, set) - }) - } - runSad := func(name string, input string, spec tableMod, wantError string) { - t.Helper() - t.Run(name, func(t *testing.T) { - t.Helper() - _, err := build(t, input, spec) - if err == nil { - t.Fatal("expected error") - } - if !strings.Contains(err.Error(), wantError) { - t.Errorf("expected error to contain '%s', got '%s'", wantError, err.Error()) - } - }) - } - - // Successes - - runHappy("full success", ` - message FooListRequest { - j5.list.v1.PageRequest page = 1; - j5.list.v1.QueryRequest query = 2; - - option (j5.list.v1.list_request) = { - sort_tiebreaker: ["id"] - }; - } - - message FooListResponse { - repeated Foo foos = 1; - j5.list.v1.PageResponse page = 2; - } - - message Foo { - string id = 1; - } - `, - nil, - func(t *testing.T, lr *ListReflectionSet) { - if len(lr.tieBreakerFields) != 1 { - t.Error("expected one sort tiebreaker") - } else { - field := lr.tieBreakerFields[0] - assert.Equal(t, "->>'id'", field.Path.JSONBArrowPath()) - } - assert.Equal(t, uint64(20), lr.defaultPageSize) - }) - - runHappy("override default page size by validation", ` - message FooListRequest { - j5.list.v1.PageRequest page = 1; - j5.list.v1.QueryRequest query = 2; - - option (j5.list.v1.list_request) = { - sort_tiebreaker: ["id"] - }; - } - - message FooListResponse { - repeated Foo foos = 1 [ - (buf.validate.field).repeated.max_items = 10 - ]; - j5.list.v1.PageResponse page = 2; - } - - message Foo { - string id = 1; - } - `, - nil, - func(t *testing.T, lr *ListReflectionSet) { - assert.EqualValues(t, int(10), int(lr.defaultPageSize)) - }) - - runHappy("tie breaker fallback", composed{ - FooListRequest: ` - j5.list.v1.PageRequest page = 1; - j5.list.v1.QueryRequest query = 2; - `, - }.toString(), - func(t testing.TB, table *TableSpec, req, res protoreflect.MessageDescriptor) { - table.FallbackSortColumns = []ProtoField{{ - valueColumn: gl.Ptr("id"), - pathInRoot: pgstore.ProtoPathSpec{"id"}, - }} - }, - func(t *testing.T, lr *ListReflectionSet) { - if len(lr.tieBreakerFields) != 1 { - t.Error("expected one sort tiebreaker") - } else { - field := lr.tieBreakerFields[0] - assert.Equal(t, "->>'id'", field.Path.JSONBArrowPath()) - } - }) - - runHappy("sort by bar", ` - message FooListRequest { - j5.list.v1.PageRequest page = 1; - j5.list.v1.QueryRequest query = 2; - - option (j5.list.v1.list_request) = { - sort_tiebreaker: ["bar.id"] - }; - } - - message FooListResponse { - repeated Foo foos = 1; - j5.list.v1.PageResponse page = 2; - } - - message Foo { - string id = 1; - Bar bar = 2; - } - - message Bar { - string id = 1; - } - `, - nil, - func(t *testing.T, lr *ListReflectionSet) { - if len(lr.tieBreakerFields) != 1 { - t.Error("expected one sort tiebreaker") - } else { - field := lr.tieBreakerFields[0] - assert.Equal(t, "->'bar'->>'id'", field.Path.JSONBArrowPath()) - assert.Equal(t, "$.bar.id", field.Path.JSONPathQuery()) - } - }) - - runHappy("sort by bar by walking", ` - message FooListRequest { - j5.list.v1.PageRequest page = 1; - j5.list.v1.QueryRequest query = 2; - - option (j5.list.v1.list_request) = { - }; - } - - message FooListResponse { - repeated Foo foos = 1; - j5.list.v1.PageResponse page = 2; - } - - message Foo { - string id = 1; - Bar bar = 2; - } - - message Bar { - string id = 1; - google.protobuf.Timestamp timestamp = 2 [ - (j5.list.v1.field).timestamp = { - sorting: { - default_sort: true - } - } - ]; - } - `, - nil, - func(t *testing.T, lr *ListReflectionSet) { - if len(lr.tieBreakerFields) != 0 { - t.Error("expected no sort tiebreaker") - } - - if len(lr.defaultSortFields) != 1 { - t.Error("expected one sort tiebreaker, got", len(lr.tieBreakerFields)) - } else { - field := lr.defaultSortFields[0] - assert.Equal(t, "->'bar'->>'timestamp'", field.Path.JSONBArrowPath()) - assert.Equal(t, "$.bar.timestamp", field.Path.JSONPathQuery()) - } - }) - - runHappy("filter by bar date", ` - message FooListRequest { - j5.list.v1.PageRequest page = 1; - j5.list.v1.QueryRequest query = 2; - - option (j5.list.v1.list_request) = { - sort_tiebreaker: ["bar.id"] - }; - } - - message FooListResponse { - repeated Foo foos = 1; - j5.list.v1.PageResponse page = 2; - } - - message Foo { - string id = 1; - Bar bar = 2; - } - - message Bar { - string id = 1; - j5.types.date.v1.Date date = 2 [ - (j5.list.v1.field).date.filtering = { - filterable: true, - default_filters: ["2025-01-01"] - } - ]; - } - `, - nil, - func(t *testing.T, lr *ListReflectionSet) { - if len(lr.defaultFilterFields) != 1 { - t.Error("expected one filter field, got", len(lr.defaultFilterFields)) - } else { - field := lr.defaultFilterFields[0] - assert.Equal(t, "->'bar'->>'date'", field.Path.JSONBArrowPath()) - assert.Equal(t, "$.bar.date", field.Path.JSONPathQuery()) - } - }) - - // Response Errors - - runSad("non message field in response", composed{ - FooListResponse: ` - repeated Foo foos = 1; - j5.list.v1.PageResponse page = 2; - Foo dangling = 3; - `, - }.toString(), - nil, - "unknown field") - - runSad("non message in response", composed{ - FooListResponse: ` - repeated Foo foos = 1; - j5.list.v1.PageResponse page = 2; - string dangling = 3; - `, - }.toString(), - nil, - "should be a message", - ) - - runSad("extra array field in response", composed{ - FooListResponse: ` - repeated Foo foos = 1; - j5.list.v1.PageResponse page = 2; - repeated Foo dangling = 3; - `, - }.toString(), - nil, - "multiple repeated fields") - - runSad("no array field in response", composed{ - FooListResponse: ` - j5.list.v1.PageResponse page = 2; - `, - }.toString(), - nil, - "no repeated field in response", - ) - - runSad("no page field in response", composed{ - FooListResponse: ` - repeated Foo foos = 1; - `, - }.toString(), - nil, - "no page field in response", - ) - - // Request Errors - - runSad("no fallback sort field", composed{ - FooListRequest: ` - j5.list.v1.PageRequest page = 1; - j5.list.v1.QueryRequest query = 2; - `, - }.toString(), - nil, - "no default sort field", - ) - - runSad("tie breaker not in response", composed{ - FooListRequest: ` - j5.list.v1.PageRequest page = 1; - j5.list.v1.QueryRequest query = 2; - option (j5.list.v1.list_request) = { - sort_tiebreaker: ["missing"] - }; - `, - }.toString(), - nil, - "no field named 'missing'", - ) - - runSad("no page field", composed{ - FooListRequest: ` - j5.list.v1.QueryRequest query = 2; - - option (j5.list.v1.list_request) = { - sort_tiebreaker: ["id"] - }; - `, - }.toString(), - nil, - "no page field in request", - ) - - runSad("no query field", composed{ - FooListRequest: ` - j5.list.v1.PageRequest page = 1; - - option (j5.list.v1.list_request) = { - sort_tiebreaker: ["id"] - }; - `, - }.toString(), - nil, - "no query field in request", - ) - - runSad("repeated field sort", ` - message FooListRequest { - j5.list.v1.PageRequest page = 1; - j5.list.v1.QueryRequest query = 2; - } - - message FooListResponse { - repeated Foo foos = 1; - j5.list.v1.PageResponse page = 2; - } - - message Foo { - string id = 1; - int64 seq = 2 [(j5.list.v1.field).int64.sorting = {sortable: true, default_sort: true}]; - repeated int64 weight = 3 [(j5.list.v1.field).int64.sorting.sortable = true]; - } - `, - nil, - "sorting not allowed on repeated field", - ) - - runSad("repeated sub field sort", ` - message FooListRequest { - j5.list.v1.PageRequest page = 1; - j5.list.v1.QueryRequest query = 2; - } - - message FooListResponse { - repeated Foo foos = 1; - j5.list.v1.PageResponse page = 2; - } - - message Foo { - string id = 1; - int64 seq = 2 [(j5.list.v1.field).int64.sorting = {sortable: true, default_sort: true}]; - repeated Profile profiles = 3; - } - - message Profile { - string name = 1; - int64 weight = 2 [(j5.list.v1.field).int64.sorting.sortable = true]; - } - `, - nil, - "sorting not allowed on subfield of repeated parent", - ) -} diff --git a/pquery/query.go b/pquery/query.go deleted file mode 100644 index 7306609..0000000 --- a/pquery/query.go +++ /dev/null @@ -1,58 +0,0 @@ -package pquery - -import ( - "context" - "fmt" - - "github.com/pentops/sqrlx.go/sqrlx" - "google.golang.org/protobuf/proto" - "google.golang.org/protobuf/reflect/protoreflect" -) - -type aliasSet int - -func (as *aliasSet) Next(name string) string { - *as++ - return fmt.Sprintf("_%s__a%d", name, *as) -} - -func newAliasSet() *aliasSet { - return new(aliasSet) -} - -type Transactor interface { - Transact(ctx context.Context, opts *sqrlx.TxOptions, callback sqrlx.Callback) error -} - -type AuthProvider interface { - AuthFilter(ctx context.Context) (map[string]string, error) -} - -type AuthProviderFunc func(ctx context.Context) (map[string]string, error) - -func (f AuthProviderFunc) AuthFilter(ctx context.Context) (map[string]string, error) { - return f(ctx) -} - -// LeftJoin is a specification for joining in the form -// ON . =
. -// Main is defined in the outer struct holding this LeftJoin -type LeftJoin struct { - TableName string - On JoinFields -} - -// MethodDescriptor is the RequestResponse pair in the gRPC Method -type methodDescriptor[REQ proto.Message, RES proto.Message] struct { - request protoreflect.MessageDescriptor - response protoreflect.MessageDescriptor -} - -func newMethodDescriptor[REQ proto.Message, RES proto.Message]() *methodDescriptor[REQ, RES] { - req := *new(REQ) - res := *new(RES) - return &methodDescriptor[REQ, RES]{ - request: req.ProtoReflect().Descriptor(), - response: res.ProtoReflect().Descriptor(), - } -} diff --git a/pquery/search.go b/pquery/search.go deleted file mode 100644 index 3602252..0000000 --- a/pquery/search.go +++ /dev/null @@ -1,242 +0,0 @@ -package pquery - -import ( - "fmt" - - sq "github.com/elgris/sqrl" - "github.com/pentops/j5/gen/j5/list/v1/list_j5pb" - "github.com/pentops/protostate/internal/pgstore" - "google.golang.org/protobuf/proto" - "google.golang.org/protobuf/reflect/protoreflect" - "google.golang.org/protobuf/types/descriptorpb" - "maps" -) - -func validateSearchesAnnotations(msg protoreflect.FieldDescriptors) error { - fields := make([]protoreflect.FieldDescriptor, msg.Len()) - for i := range msg.Len() { - fields[i] = msg.Get(i) - } - _, err := validateSearchesAnnotationsInner(fields) - if err != nil { - return fmt.Errorf("search validation: %w", err) - } - return nil -} - -func validateSearchesAnnotationsInner(fields []protoreflect.FieldDescriptor) (map[string]protoreflect.Name, error) { - // search annotations have a 'field_identifier' which specifies the database column name to use for the text-search-vector. - // This function validates that the field_identifier is unique for the given field-set - // In cases where the field is a message, it will recurse into the message to validate the field identifiers - - // When the same message is used in multiple places, any field of that - // message or a child message will, by definition, have the same field - // identifier, and is therefore invalid. - - // Search annotation cannot be used in repeated or map fields - // Recursion is already invalid. - - // Oneof fields, however, can have the same field identifier on different - // branches. - - ids := make(map[string]protoreflect.Name) - oneofs := map[string][]protoreflect.FieldDescriptor{} - - for _, field := range fields { - - if oneof := field.ContainingOneof(); oneof != nil { - name := string(oneof.Name()) - oneofs[name] = append(oneofs[name], field) - continue - } - - err := validateSearchAnnotationsField(ids, field) - if err != nil { - return nil, err - } - } - - for _, oneofFields := range oneofs { - - // oneof fields can have the same field identifier on different branches but must be unique: - // - within the branch, as before - // - with existing parent keys - // - with other oneofs - - combinedBranchIDs := make(map[string]protoreflect.Name) - - for _, field := range oneofFields { - - // collect a new set of IDs for this branch as if it is the root of - // the message. - branchIDs := make(map[string]protoreflect.Name) - err := validateSearchAnnotationsField(branchIDs, field) - if err != nil { - return nil, err - } - - // Compare the branch IDs to the root IDs, if a branch conflicts - // with the root that is an error. - for searchKey, usedIn := range branchIDs { - if existing, ok := ids[searchKey]; ok { - return nil, fmt.Errorf("field identifier '%s' is used at %s and %s within the same oneof branch", searchKey, existing, usedIn) - } - - // the latest wins here, this makes a worse error message but - // doesn't actually effect the outcome. - combinedBranchIDs[searchKey] = usedIn - } - - } - - // The IDs inside each branch are unique, and unique with the parent keys. - - // We still need to ensure that the IDs are unique with other oneofs. - maps.Copy(ids, combinedBranchIDs) - - } - - return ids, nil -} - -func validateSearchAnnotationsField(ids map[string]protoreflect.Name, field protoreflect.FieldDescriptor) error { - - switch field.Kind() { - case protoreflect.StringKind: - fieldOpts, ok := proto.GetExtension(field.Options().(*descriptorpb.FieldOptions), list_j5pb.E_Field).(*list_j5pb.FieldConstraint) - if !ok { - return nil - } - - switch fieldOpts.GetString_().GetWellKnown().(type) { - case *list_j5pb.StringRules_OpenText: - searchOpts := fieldOpts.GetString_().GetOpenText().GetSearching() - if searchOpts == nil || !searchOpts.Searchable { - return nil - } - - if searchOpts.GetFieldIdentifier() == "" { - return fmt.Errorf("field '%s' is missing a field identifier", field.FullName()) - } - - if existing, ok := ids[searchOpts.GetFieldIdentifier()]; ok { - return fmt.Errorf("field identifier '%s' is already used at %s", searchOpts.GetFieldIdentifier(), existing) - } - - ids[searchOpts.GetFieldIdentifier()] = field.Name() - } - - return nil - case protoreflect.MessageKind: - msg := field.Message().Fields() - fields := make([]protoreflect.FieldDescriptor, msg.Len()) - for i := range msg.Len() { - fields[i] = msg.Get(i) - } - - searchIdentifiers, err := validateSearchesAnnotationsInner(fields) - if err != nil { - return fmt.Errorf("message search validation: %w", err) - } - for searchKey, usedIn := range searchIdentifiers { - if existing, ok := ids[searchKey]; ok { - return fmt.Errorf("field identifier '%s' is already used at %s", searchKey, existing) - } - - ids[searchKey] = protoreflect.Name(fmt.Sprintf("%s.%s", field.Name(), usedIn)) - } - } - - return nil - -} - -func validateQueryRequestSearches(message protoreflect.MessageDescriptor, searches []*list_j5pb.Search) error { - for _, search := range searches { - - spec, err := pgstore.NewJSONPath(message, pgstore.ParseJSONPathSpec(search.GetField())) - if err != nil { - return fmt.Errorf("field spec: %w", err) - } - - field := spec.LeafField() - if field == nil { - return fmt.Errorf("leaf '%s' is not a field", spec.JSONPathQuery()) - } - - // validate the fields are annotated correctly for the request query - searchOpts, ok := proto.GetExtension(field.Options().(*descriptorpb.FieldOptions), list_j5pb.E_Field).(*list_j5pb.FieldConstraint) - if !ok { - return fmt.Errorf("requested search field '%s' does not have any searchable constraints defined", search.Field) - } - - searchable := false - if searchOpts != nil { - switch field.Kind() { - case protoreflect.StringKind: - switch searchOpts.GetString_().WellKnown.(type) { - case *list_j5pb.StringRules_OpenText: - searchable = searchOpts.GetString_().GetOpenText().GetSearching().Searchable - } - } - } - - if !searchable { - return fmt.Errorf("requested search field '%s' is not searchable", search.Field) - } - } - - return nil -} - -func buildTsvColumnMap(message protoreflect.MessageDescriptor) map[string]string { - out := make(map[string]string) - - for i := range message.Fields().Len() { - field := message.Fields().Get(i) - - switch field.Kind() { - case protoreflect.StringKind: - fieldOpts, ok := proto.GetExtension(field.Options().(*descriptorpb.FieldOptions), list_j5pb.E_Field).(*list_j5pb.FieldConstraint) - if !ok { - continue - } - - switch fieldOpts.GetString_().GetWellKnown().(type) { - case *list_j5pb.StringRules_OpenText: - searchOpts := fieldOpts.GetString_().GetOpenText().GetSearching() - if searchOpts == nil || !searchOpts.Searchable { - continue - } - - out[field.TextName()] = searchOpts.GetFieldIdentifier() - } - - continue - case protoreflect.MessageKind: - nestedMap := buildTsvColumnMap(field.Message()) - - for nk, nv := range nestedMap { - k := fmt.Sprintf("%s.%s", field.TextName(), nk) - out[k] = nv - } - } - } - - return out -} - -func (ll *Lister[REQ, RES]) buildDynamicSearches(tableAlias string, searches []*list_j5pb.Search) ([]sq.Sqlizer, error) { - out := []sq.Sqlizer{} - - for i := range searches { - col, ok := ll.tsvColumnMap[camelToSnake(searches[i].GetField())] - if !ok { - return nil, fmt.Errorf("unknown field name '%s'", searches[i].GetField()) - } - - out = append(out, sq.And{sq.Expr(fmt.Sprintf("%s.%s @@ phraseto_tsquery(?)", tableAlias, col), searches[i].GetValue())}) - } - - return out, nil -} diff --git a/pquery/search_test.go b/pquery/search_test.go deleted file mode 100644 index 5298166..0000000 --- a/pquery/search_test.go +++ /dev/null @@ -1,219 +0,0 @@ -package pquery - -import ( - "testing" - - "github.com/pentops/flowtest/prototest" - "github.com/stretchr/testify/assert" -) - -func TestBuildTsvColumnMap(t *testing.T) { - descFiles := prototest.DescriptorsFromSource(t, map[string]string{ - "test.proto": ` - syntax = "proto3"; - - import "j5/list/v1/annotations.proto"; - - package test; - - message Foo { - string unoptioned_field = 1; - string optioned_field = 2 [(j5.list.v1.field).string.open_text.searching = { - searchable: true, - field_identifier: "optioned_field" - }]; - Bar bar = 3; - } - - message Bar { - string unoptioned_field = 1; - string optioned_field = 2 [(j5.list.v1.field).string.open_text.searching = { - searchable: true, - field_identifier: "bar_optioned_field" - }]; - } - `}) - - fooDesc := descFiles.MessageByName(t, "test.Foo") - - columnMap := buildTsvColumnMap(fooDesc) - assert.Len(t, columnMap, 2) - - for f, c := range columnMap { - t.Log("field: ", f, "\tcolumn: ", c) - } -} - -func TestValidateSearchAnnotations(t *testing.T) { - t.Run("happy path", func(t *testing.T) { - descFiles := prototest.DescriptorsFromSource(t, map[string]string{ - "test.proto": ` - syntax = "proto3"; - - import "j5/list/v1/annotations.proto"; - - package test; - - message Foo { - string unoptioned_field = 1; - string optioned_field = 2 [(j5.list.v1.field).string.open_text.searching = { - searchable: true, - field_identifier: "optioned_field" - }]; - Bar bar = 3; - } - - message Bar { - string unoptioned_field = 1; - string optioned_field = 2 [(j5.list.v1.field).string.open_text.searching = { - searchable: true, - field_identifier: "bar_optioned_field" - }]; - } - - `}) - - fooDesc := descFiles.MessageByName(t, "test.Foo") - - err := validateSearchesAnnotations(fooDesc.Fields()) - assert.NoError(t, err) - }) - - t.Run("multi path duplicates", func(t *testing.T) { - descFiles := prototest.DescriptorsFromSource(t, map[string]string{ - "test.proto": ` - syntax = "proto3"; - - import "j5/list/v1/annotations.proto"; - - package test; - - message Msg { - string unoptioned_field = 1; - string optioned_field = 2 [(j5.list.v1.field).string.open_text.searching = { - searchable: true, - field_identifier: "optioned_field" - }]; - } - - message Foo { - oneof type { - Type1 type1 = 1; - Type2 type2 = 2; - } - } - - message Bar { - Type1 type1 = 1; - Type2 type2 = 2; - } - - message Baz { - oneof set1 { - Type1 s1type1 = 1; - Type2 s1type2 = 2; - } - - oneof set2 { - Type3 type3 = 3; - Type4 type4 = 4; - } - - } - - message Type1 { - Msg msg = 1; - } - message Type2 { - Msg msg = 1; - } - message Type3 { - Msg msg = 1; - } - message Type4 { - Msg msg = 1; - } - `}) - - // Both Type1 and Type2 import the same Msg messaage, which means they - // have the same field identifier. - - // Bar is not mutually exclusive, so is not OK - - // This will be a common pattern for event messages. - - // The two instances of Msg within Foo are mutually exclusive, so is OK - fooDesc := descFiles.MessageByName(t, "test.Foo") - err := validateSearchesAnnotations(fooDesc.Fields()) - assert.NoError(t, err) - - // The two instances of Msg within Bar are NOT mutually exclusive, this - // is not OK. - barDesc := descFiles.MessageByName(t, "test.Bar") - err = validateSearchesAnnotations(barDesc.Fields()) - assert.Error(t, err) - - // Each oneof in Baz is OK by itself, but Type1 and Type3 can be set - // together, and so the search key can be duplicated. - bazDesc := descFiles.MessageByName(t, "test.Baz") - err = validateSearchesAnnotations(bazDesc.Fields()) - assert.Error(t, err) - }) - - t.Run("duplicate field identifier", func(t *testing.T) { - descFiles := prototest.DescriptorsFromSource(t, map[string]string{ - "test.proto": ` - syntax = "proto3"; - - import "j5/list/v1/annotations.proto"; - - package test; - - message Foo { - string unoptioned_field = 1; - string optioned_field = 2 [(j5.list.v1.field).string.open_text.searching = { - searchable: true, - field_identifier: "optioned_field" - }]; - Bar bar = 3; - } - - message Bar { - string unoptioned_field = 1; - string optioned_field = 2 [(j5.list.v1.field).string.open_text.searching = { - searchable: true, - field_identifier: "optioned_field" - }]; - } - `}) - - fooDesc := descFiles.MessageByName(t, "test.Foo") - - err := validateSearchesAnnotations(fooDesc.Fields()) - assert.Error(t, err) - }) - - t.Run("missing field identifier", func(t *testing.T) { - descFiles := prototest.DescriptorsFromSource(t, map[string]string{ - "test.proto": ` - syntax = "proto3"; - - import "j5/list/v1/annotations.proto"; - - package test; - - message Foo { - string unoptioned_field = 1; - string optioned_field = 2 [(j5.list.v1.field).string.open_text.searching = { - searchable: true, - field_identifier: "" - }]; - } - `}) - - fooDesc := descFiles.MessageByName(t, "test.Foo") - - err := validateSearchesAnnotations(fooDesc.Fields()) - assert.Error(t, err) - }) -} diff --git a/pquery/sort.go b/pquery/sort.go deleted file mode 100644 index 9ce48fc..0000000 --- a/pquery/sort.go +++ /dev/null @@ -1,356 +0,0 @@ -package pquery - -import ( - "fmt" - - "github.com/pentops/j5/gen/j5/list/v1/list_j5pb" - "github.com/pentops/protostate/internal/pgstore" - "google.golang.org/protobuf/proto" - "google.golang.org/protobuf/reflect/protoreflect" - "google.golang.org/protobuf/types/descriptorpb" -) - -type sortSpec struct { - *pgstore.NestedField - desc bool -} - -func (ss sortSpec) errorName() string { - return ss.Path.JSONPathQuery() -} - -func buildTieBreakerFields(dataColumn string, req protoreflect.MessageDescriptor, arrayField protoreflect.MessageDescriptor, fallback []ProtoField) ([]sortSpec, error) { - listRequestAnnotation, ok := proto.GetExtension(req.Options().(*descriptorpb.MessageOptions), list_j5pb.E_ListRequest).(*list_j5pb.ListRequestMessage) - if ok && listRequestAnnotation != nil && len(listRequestAnnotation.SortTiebreaker) > 0 { - tieBreakerFields := make([]sortSpec, 0, len(listRequestAnnotation.SortTiebreaker)) - for _, tieBreaker := range listRequestAnnotation.SortTiebreaker { - spec, err := pgstore.NewProtoPath(arrayField, pgstore.ParseProtoPathSpec(tieBreaker)) - if err != nil { - return nil, fmt.Errorf("field %s in annotated sort tiebreaker for %s: %w", tieBreaker, req.FullName(), err) - } - - tieBreakerFields = append(tieBreakerFields, sortSpec{ - NestedField: &pgstore.NestedField{ - RootColumn: dataColumn, - Path: *spec, - }, - desc: false, - }) - } - - return tieBreakerFields, nil - } - - if len(fallback) == 0 { - return []sortSpec{}, nil - } - - tieBreakerFields := make([]sortSpec, 0, len(fallback)) - for _, tieBreaker := range fallback { - - path, err := pgstore.NewProtoPath(arrayField, tieBreaker.pathInRoot) - if err != nil { - return nil, fmt.Errorf("field %s in fallback sort tiebreaker for %s: %w", tieBreaker.pathInRoot, req.FullName(), err) - } - - tieBreakerFields = append(tieBreakerFields, sortSpec{ - NestedField: &pgstore.NestedField{ - Path: *path, - RootColumn: dataColumn, - ValueColumn: tieBreaker.valueColumn, - }, - desc: false, - }) - } - - return tieBreakerFields, nil -} - -func buildDefaultSorts(columnName string, message protoreflect.MessageDescriptor) ([]sortSpec, error) { - var defaultSortFields []sortSpec - - err := pgstore.WalkPathNodes(message, func(path pgstore.Path) error { - field := path.LeafField() - if field == nil { - return nil // oneof or something - } - - fieldOpts := proto.GetExtension(field.Options().(*descriptorpb.FieldOptions), list_j5pb.E_Field).(*list_j5pb.FieldConstraint) - - if fieldOpts == nil { - return nil - } - isDefaultSort := false - - switch fieldOps := fieldOpts.Type.(type) { - case *list_j5pb.FieldConstraint_Double: - if fieldOps.Double.Sorting != nil && fieldOps.Double.Sorting.DefaultSort { - isDefaultSort = true - } - case *list_j5pb.FieldConstraint_Fixed32: - if fieldOps.Fixed32.Sorting != nil && fieldOps.Fixed32.Sorting.DefaultSort { - isDefaultSort = true - } - case *list_j5pb.FieldConstraint_Fixed64: - if fieldOps.Fixed64.Sorting != nil && fieldOps.Fixed64.Sorting.DefaultSort { - isDefaultSort = true - } - case *list_j5pb.FieldConstraint_Float: - if fieldOps.Float.Sorting != nil && fieldOps.Float.Sorting.DefaultSort { - isDefaultSort = true - } - case *list_j5pb.FieldConstraint_Int32: - if fieldOps.Int32.Sorting != nil && fieldOps.Int32.Sorting.DefaultSort { - isDefaultSort = true - } - case *list_j5pb.FieldConstraint_Int64: - if fieldOps.Int64.Sorting != nil && fieldOps.Int64.Sorting.DefaultSort { - isDefaultSort = true - } - case *list_j5pb.FieldConstraint_Sfixed32: - if fieldOps.Sfixed32.Sorting != nil && fieldOps.Sfixed32.Sorting.DefaultSort { - isDefaultSort = true - } - case *list_j5pb.FieldConstraint_Sfixed64: - if fieldOps.Sfixed64.Sorting != nil && fieldOps.Sfixed64.Sorting.DefaultSort { - isDefaultSort = true - } - case *list_j5pb.FieldConstraint_Sint32: - if fieldOps.Sint32.Sorting != nil && fieldOps.Sint32.Sorting.DefaultSort { - isDefaultSort = true - } - case *list_j5pb.FieldConstraint_Sint64: - if fieldOps.Sint64.Sorting != nil && fieldOps.Sint64.Sorting.DefaultSort { - isDefaultSort = true - } - case *list_j5pb.FieldConstraint_Uint32: - if fieldOps.Uint32.Sorting != nil && fieldOps.Uint32.Sorting.DefaultSort { - isDefaultSort = true - } - case *list_j5pb.FieldConstraint_Uint64: - if fieldOps.Uint64.Sorting != nil && fieldOps.Uint64.Sorting.DefaultSort { - isDefaultSort = true - } - case *list_j5pb.FieldConstraint_Timestamp: - if fieldOps.Timestamp.Sorting != nil && fieldOps.Timestamp.Sorting.DefaultSort { - isDefaultSort = true - } - } - if isDefaultSort { - defaultSortFields = append(defaultSortFields, sortSpec{ - NestedField: &pgstore.NestedField{ - RootColumn: columnName, - Path: path, - }, - desc: true, - }) - } - return nil - }) - if err != nil { - return nil, err - } - - return defaultSortFields, nil -} - -func (ll *Lister[REQ, RES]) buildDynamicSortSpec(sorts []*list_j5pb.Sort) ([]sortSpec, error) { - results := []sortSpec{} - direction := "" - for _, sort := range sorts { - pathSpec := pgstore.ParseJSONPathSpec(sort.Field) - spec, err := pgstore.NewJSONPath(ll.arrayField.Message(), pathSpec) - if err != nil { - return nil, fmt.Errorf("dynamic filter: find field: %w", err) - } - - biggerSpec := &pgstore.NestedField{ - Path: *spec, - RootColumn: ll.dataColumn, - } - - results = append(results, sortSpec{ - NestedField: biggerSpec, - desc: sort.Descending, - }) - - // TODO: Remove this constraint, we can sort by different directions once we have the reversal logic in place - // validate direction of all the fields is the same - if direction == "" { - direction = "ASC" - if sort.Descending { - direction = "DESC" - } - } else { - if (direction == "DESC" && !sort.Descending) || (direction == "ASC" && sort.Descending) { - return nil, fmt.Errorf("requested sorts have conflicting directions, they must all be the same") - } - } - } - - return results, nil -} - -func validateSortsAnnotations(fields protoreflect.FieldDescriptors) error { - for i := range fields.Len() { - field := fields.Get(i) - - if field.Kind() == protoreflect.MessageKind { - subFields := field.Message().Fields() - - for i := range subFields.Len() { - subField := subFields.Get(i) - - if subField.Kind() == protoreflect.MessageKind { - err := validateSortsAnnotations(subField.Message().Fields()) - if err != nil { - return fmt.Errorf("message sort validation: %w", err) - } - } else { - if field.Cardinality() == protoreflect.Repeated { - // check options of subfield for sorting - fieldOpts := proto.GetExtension(subField.Options().(*descriptorpb.FieldOptions), list_j5pb.E_Field).(*list_j5pb.FieldConstraint) - if isSortingAnnotated(fieldOpts) { - return fmt.Errorf("sorting not allowed on subfield of repeated parent: %s", field.Name()) - } - } - } - } - } else { - if field.Cardinality() == protoreflect.Repeated { - // check options of parent field for sorting - fieldOpts := proto.GetExtension(field.Options().(*descriptorpb.FieldOptions), list_j5pb.E_Field).(*list_j5pb.FieldConstraint) - if isSortingAnnotated(fieldOpts) { - return fmt.Errorf("sorting not allowed on repeated field, must be a scalar: %s", field.Name()) - } - } - - } - } - - return nil -} - -func isSortingAnnotated(opts *list_j5pb.FieldConstraint) bool { - annotated := false - - if opts != nil { - switch opts.Type.(type) { - case *list_j5pb.FieldConstraint_Double: - if opts.GetDouble().Sorting != nil { - annotated = true - } - case *list_j5pb.FieldConstraint_Fixed32: - if opts.GetFixed32().Sorting != nil { - annotated = true - } - case *list_j5pb.FieldConstraint_Fixed64: - if opts.GetFixed64().Sorting != nil { - annotated = true - } - case *list_j5pb.FieldConstraint_Float: - if opts.GetFloat().Sorting != nil { - annotated = true - } - case *list_j5pb.FieldConstraint_Int32: - if opts.GetInt32().Sorting != nil { - annotated = true - } - case *list_j5pb.FieldConstraint_Int64: - if opts.GetInt64().Sorting != nil { - annotated = true - } - case *list_j5pb.FieldConstraint_Sfixed32: - if opts.GetSfixed32().Sorting != nil { - annotated = true - } - case *list_j5pb.FieldConstraint_Sfixed64: - if opts.GetSfixed64().Sorting != nil { - annotated = true - } - case *list_j5pb.FieldConstraint_Sint32: - if opts.GetSint32().Sorting != nil { - annotated = true - } - case *list_j5pb.FieldConstraint_Sint64: - if opts.GetSint64().Sorting != nil { - annotated = true - } - case *list_j5pb.FieldConstraint_Uint32: - if opts.GetUint32().Sorting != nil { - annotated = true - } - case *list_j5pb.FieldConstraint_Uint64: - if opts.GetUint64().Sorting != nil { - annotated = true - } - case *list_j5pb.FieldConstraint_Timestamp: - if opts.GetTimestamp().Sorting != nil { - annotated = true - } - } - } - - return annotated -} - -func validateQueryRequestSorts(message protoreflect.MessageDescriptor, sorts []*list_j5pb.Sort) error { - for _, sort := range sorts { - pathSpec := pgstore.ParseJSONPathSpec(sort.Field) - spec, err := pgstore.NewJSONPath(message, pathSpec) - if err != nil { - return fmt.Errorf("find field %s: %w", sort.Field, err) - } - - field := spec.LeafField() - if field == nil { - return fmt.Errorf("node %s is not a field", spec.DebugName()) - } - - // validate the fields are annotated correctly for the request query - sortOpts, ok := proto.GetExtension(field.Options().(*descriptorpb.FieldOptions), list_j5pb.E_Field).(*list_j5pb.FieldConstraint) - if !ok { - return fmt.Errorf("requested sort field '%s' does not have any sortable constraints defined", sort.Field) - } - - sortable := false - if sortOpts != nil { - switch field.Kind() { - case protoreflect.DoubleKind: - sortable = sortOpts.GetDouble().GetSorting().Sortable - case protoreflect.Fixed32Kind: - sortable = sortOpts.GetFixed32().GetSorting().Sortable - case protoreflect.Fixed64Kind: - sortable = sortOpts.GetFixed64().GetSorting().Sortable - case protoreflect.FloatKind: - sortable = sortOpts.GetFloat().GetSorting().Sortable - case protoreflect.Int32Kind: - sortable = sortOpts.GetInt32().GetSorting().Sortable - case protoreflect.Int64Kind: - sortable = sortOpts.GetInt64().GetSorting().Sortable - case protoreflect.Sfixed32Kind: - sortable = sortOpts.GetSfixed32().GetSorting().Sortable - case protoreflect.Sfixed64Kind: - sortable = sortOpts.GetSfixed64().GetSorting().Sortable - case protoreflect.Sint32Kind: - sortable = sortOpts.GetSint32().GetSorting().Sortable - case protoreflect.Sint64Kind: - sortable = sortOpts.GetSint64().GetSorting().Sortable - case protoreflect.Uint32Kind: - sortable = sortOpts.GetUint32().GetSorting().Sortable - case protoreflect.Uint64Kind: - sortable = sortOpts.GetUint64().GetSorting().Sortable - case protoreflect.MessageKind: - if field.Message().FullName() == "google.protobuf.Timestamp" { - sortable = sortOpts.GetTimestamp().GetSorting().Sortable - } - } - } - - if !sortable { - return fmt.Errorf("requested sort field '%s' is not sortable", sort.Field) - } - } - - return nil -} diff --git a/psm/builder.go b/psm/builder.go index cc0cc2c..066fe5a 100644 --- a/psm/builder.go +++ b/psm/builder.go @@ -2,7 +2,9 @@ package psm import ( "context" + "fmt" + "github.com/pentops/j5/lib/j5schema" "github.com/pentops/sqrlx.go/sqrlx" ) @@ -64,11 +66,34 @@ func (smc *StateMachineConfig[K, S, ST, SD, E, IE]) InitialStateFunc(cbFunc func return smc } +func (smc *StateMachineConfig[K, S, ST, SD, E, IE]) schemas() (*j5schema.ObjectSchema, *j5schema.ObjectSchema, error) { + state, ok := newJ5Message[S]().J5Reflect().RootSchema() + if !ok { + return nil, nil, fmt.Errorf("invalid state type %T, must be a j5 root schema", new(S)) + } + stateObject, ok := state.(*j5schema.ObjectSchema) + if !ok { + return nil, nil, fmt.Errorf("invalid state type %T, must be a j5 object schema", state) + } + event, ok := newJ5Message[E]().J5Reflect().RootSchema() + if !ok { + return nil, nil, fmt.Errorf("invalid event type %T, must be a j5 root schema", new(E)) + } + eventObject, ok := event.(*j5schema.ObjectSchema) + if !ok { + return nil, nil, fmt.Errorf("invalid event type %T, must be a j5 object schema", event) + } + return stateObject, eventObject, nil + +} + func (smc *StateMachineConfig[K, S, ST, SD, E, IE]) apply() error { if smc.tableMap == nil { - state := (*new(S)).ProtoReflect().Descriptor() - event := (*new(E)).ProtoReflect().Descriptor() - tableMap, err := tableMapFromStateAndEvent(state, event) + stateObject, eventObject, err := smc.schemas() + if err != nil { + return err + } + tableMap, err := tableMapFromStateAndEvent(stateObject, eventObject) if err != nil { return err } @@ -94,8 +119,10 @@ func (smc *StateMachineConfig[K, S, ST, SD, E, IE]) BuildQueryTableSpec() (*Quer return nil, err } - state := (*new(S)).ProtoReflect().Descriptor() - event := (*new(E)).ProtoReflect().Descriptor() + state, event, err := smc.schemas() + if err != nil { + return nil, err + } return &QueryTableSpec{ TableMap: *smc.tableMap, diff --git a/psm/gen_interfaces.go b/psm/gen_interfaces.go index 3064aeb..c3f5af8 100644 --- a/psm/gen_interfaces.go +++ b/psm/gen_interfaces.go @@ -6,6 +6,7 @@ import ( "time" "github.com/pentops/j5/gen/j5/state/v1/psm_j5pb" + "github.com/pentops/j5/lib/j5reflect" "github.com/pentops/o5-messaging/o5msg" "github.com/pentops/sqrlx.go/sqrlx" "google.golang.org/protobuf/proto" @@ -42,9 +43,21 @@ K, S, ST, E, and IE are set to one single type for the entire state machine SE is set to a single type for each transition. */ +type J5Message interface { + J5Reflect() j5reflect.Root + Clone() any // returns the same type + proto.Message +} + +func newJ5Message[T J5Message]() T { + val := (*new(T)).ProtoReflect().New().Interface().(T) + return val +} + // IGenericProtoMessage is the base extensions shared by all message entities in the PSM generated code type IPSMMessage interface { - proto.Message + //proto.Message + J5Message PSMIsSet() bool } @@ -87,7 +100,8 @@ type IEvent[ SD IStateData, Inner any, ] interface { - proto.Message + //proto.Message + J5Message UnwrapPSMEvent() Inner SetPSMEvent(Inner) error PSMKeys() K @@ -137,7 +151,7 @@ type TransitionMutation[ func (f TransitionMutation[K, S, ST, SD, E, IE, SE]) runMutation(state S, event E) error { // nolint:unused // Used by genereted code. asType, ok := any(event.UnwrapPSMEvent()).(SE) if !ok { - name := event.ProtoReflect().Descriptor().FullName() + name := event.J5Reflect().SchemaName() return fmt.Errorf("unexpected event type in transition: %s [IE] does not match [SE] (%T)", name, new(SE)) } diff --git a/psm/run.go b/psm/run.go index fd88a15..137b59b 100644 --- a/psm/run.go +++ b/psm/run.go @@ -8,7 +8,6 @@ import ( "github.com/pentops/log.go/log" "github.com/pentops/o5-messaging/outbox" "github.com/pentops/sqrlx.go/sqrlx" - "google.golang.org/protobuf/proto" ) type captureStateType int @@ -28,7 +27,7 @@ func (sm *StateMachine[K, S, ST, SD, E, IE]) runEvent( ) (*S, error) { if err := sm.validateEvent(event); err != nil { - return nil, fmt.Errorf("validating event %s: %w", event.ProtoReflect().Descriptor().FullName(), err) + return nil, err } typeKey := event.UnwrapPSMEvent().PSMEventKey() @@ -63,7 +62,7 @@ func (sm *StateMachine[K, S, ST, SD, E, IE]) runEvent( var returnState *S switch captureState { case captureInitialState: - rsVal := proto.Clone(state).(S) + rsVal := state.Clone().(S) //proto.Clone(state).(S) returnState = &rsVal case captureFinalState: returnState = &state @@ -78,7 +77,7 @@ func (sm *StateMachine[K, S, ST, SD, E, IE]) runEvent( } for _, se := range baton.sideEffects { - err = sm.validator.Validate(se.msg) + err = sm.protoValidator.Validate(se.msg) if err != nil { return nil, fmt.Errorf("validate side effect: %s %w", se.msg.ProtoReflect().Descriptor().FullName(), err) } @@ -187,7 +186,7 @@ func (sm *StateMachine[K, S, ST, SD, E, IE]) storeAfterMutation( return fmt.Errorf("state machine transitioned to zero status") } - err := sm.validator.Validate(state) + err := sm.validator.Validate(state.J5Reflect()) if err != nil { return err } diff --git a/psm/state_query.go b/psm/state_query.go index a7b0e14..ab4b0b7 100644 --- a/psm/state_query.go +++ b/psm/state_query.go @@ -4,35 +4,64 @@ import ( "context" "fmt" - "github.com/pentops/j5/gen/j5/schema/v1/schema_j5pb" - "github.com/pentops/protostate/pquery" - "google.golang.org/protobuf/proto" - "google.golang.org/protobuf/reflect/protoreflect" + pquery "github.com/pentops/j5/lib/j5query" + "github.com/pentops/j5/lib/j5reflect" + "github.com/pentops/j5/lib/j5schema" ) // QueryTableSpec the TableMap with descriptors for the messages, without using // generic parameters. type QueryTableSpec struct { - EventType protoreflect.MessageDescriptor - StateType protoreflect.MessageDescriptor + EventType *j5schema.ObjectSchema + StateType *j5schema.ObjectSchema TableMap } +func (ts *QueryTableSpec) StateTable() pquery.TableSpec { + return pquery.TableSpec{ + TableName: ts.State.TableName, + DataColumn: ts.State.Root.ColumnName, + RootObject: ts.StateType, + } +} + +func (ts *QueryTableSpec) EventTable() pquery.TableSpec { + return pquery.TableSpec{ + TableName: ts.Event.TableName, + DataColumn: ts.Event.Root.ColumnName, + RootObject: ts.EventType, + } +} + // QuerySpec is the configuration for the query service side of the state // machine. Can be partially derived from the state machine table spec, but // contains types relating to the query service so cannot be fully derived. -type QuerySpec[ - GetREQ pquery.GetRequest, - GetRES pquery.GetResponse, - ListREQ pquery.ListRequest, - ListRES pquery.ListResponse, - ListEventsREQ pquery.ListRequest, - ListEventsRES pquery.ListResponse, -] struct { +type QuerySpec struct { QueryTableSpec - ListRequestFilter func(ListREQ) (map[string]any, error) - ListEventsRequestFilter func(ListEventsREQ) (map[string]any, error) + GetMethod *j5schema.MethodSchema + ListMethod *j5schema.MethodSchema + ListEventsMethod *j5schema.MethodSchema + + ListRequestFilter func(j5reflect.Object) (map[string]any, error) + ListEventsRequestFilter func(j5reflect.Object) (map[string]any, error) +} + +func (qs *QuerySpec) Validate() error { + err := qs.QueryTableSpec.Validate() + if err != nil { + return fmt.Errorf("validate QueryTableSpec: %w", err) + } + if qs.GetMethod == nil { + return fmt.Errorf("missing GetMethod in QuerySpec") + } + if qs.ListMethod == nil { + return fmt.Errorf("missing ListMethod in QuerySpec") + } + if qs.ListEventsMethod == nil { + return fmt.Errorf("missing ListEventsMethod in QuerySpec") + } + return nil } // StateQuerySet is a shortcut for manually specifying three different query @@ -40,50 +69,27 @@ type QuerySpec[ // 1. A getter for a single state // 2. A lister for the main state // 3. A lister for the events of the main state -type StateQuerySet[ - GetREQ pquery.GetRequest, - GetRES pquery.GetResponse, - ListREQ pquery.ListRequest, - ListRES pquery.ListResponse, - ListEventsREQ pquery.ListRequest, - ListEventsRES pquery.ListResponse, -] struct { - Getter *pquery.Getter[GetREQ, GetRES] - MainLister *pquery.Lister[ListREQ, ListRES] - EventLister *pquery.Lister[ListEventsREQ, ListEventsRES] +type StateQuerySet struct { + Getter *pquery.Getter + MainLister *pquery.Lister + EventLister *pquery.Lister } -func (gc *StateQuerySet[ - GetREQ, GetRES, - ListREQ, ListRES, - ListEventsREQ, ListEventsRES, -]) SetQueryLogger(logger pquery.QueryLogger) { +func (gc *StateQuerySet) SetQueryLogger(logger pquery.QueryLogger) { gc.Getter.SetQueryLogger(logger) gc.MainLister.SetQueryLogger(logger) gc.EventLister.SetQueryLogger(logger) } -func (gc *StateQuerySet[ - GetREQ, GetRES, - ListREQ, ListRES, - ListEventsREQ, ListEventsRES, -]) Get(ctx context.Context, db Transactor, reqMsg GetREQ, resMsg GetRES) error { +func (gc *StateQuerySet) Get(ctx context.Context, db Transactor, reqMsg, resMsg j5reflect.Object) error { return gc.Getter.Get(ctx, db, reqMsg, resMsg) } -func (gc *StateQuerySet[ - GetREQ, GetRES, - ListREQ, ListRES, - ListEventsREQ, ListEventsRES, -]) List(ctx context.Context, db Transactor, reqMsg proto.Message, resMsg proto.Message) error { +func (gc *StateQuerySet) List(ctx context.Context, db Transactor, reqMsg, resMsg j5reflect.Object) error { return gc.MainLister.List(ctx, db, reqMsg, resMsg) } -func (gc *StateQuerySet[ - GetREQ, GetRES, - ListREQ, ListRES, - ListEventsREQ, ListEventsRES, -]) ListEvents(ctx context.Context, db Transactor, reqMsg proto.Message, resMsg proto.Message) error { +func (gc *StateQuerySet) ListEvents(ctx context.Context, db Transactor, reqMsg, resMsg j5reflect.Object) error { return gc.EventLister.List(ctx, db, reqMsg, resMsg) } @@ -103,57 +109,30 @@ func (f TenantFilterProviderFunc) GetRequiredTenantKeys(ctx context.Context) (ma return f(ctx) } -func BuildStateQuerySet[ - GetREQ pquery.GetRequest, - GetRES pquery.GetResponse, - ListREQ pquery.ListRequest, - ListRES pquery.ListResponse, - ListEventsREQ pquery.ListRequest, - ListEventsRES pquery.ListResponse, -]( - smSpec QuerySpec[GetREQ, GetRES, ListREQ, ListRES, ListEventsREQ, ListEventsRES], +func BuildStateQuerySet( + smSpec QuerySpec, options StateQueryOptions, -) (*StateQuerySet[GetREQ, GetRES, ListREQ, ListRES, ListEventsREQ, ListEventsRES], error) { - - eventJoinMap := pquery.JoinFields{} - requestReflect := (*new(GetREQ)).ProtoReflect().Descriptor() +) (*StateQuerySet, error) { - unmappedRequestFields := map[protoreflect.Name]protoreflect.FieldDescriptor{} - reqFields := requestReflect.Fields() - for i := range reqFields.Len() { - field := requestReflect.Fields().Get(i) - unmappedRequestFields[field.Name()] = field + if err := smSpec.Validate(); err != nil { + return nil, fmt.Errorf("validate state machine spec: %w", err) } - pkFields := map[string]func(req protoreflect.Message) any{} //protoreflect.FieldDescriptor{} - for _, keyColumn := range smSpec.KeyColumns { - matchingRequestField, ok := unmappedRequestFields[keyColumn.ProtoName] - if ok { - delete(unmappedRequestFields, keyColumn.ProtoName) - var callback func(req protoreflect.Message) any - switch keyColumn.Schema.Type.(type) { - case *schema_j5pb.Field_String_, *schema_j5pb.Field_Key: - callback = func(req protoreflect.Message) any { - return req.Get(matchingRequestField).String() - } - case *schema_j5pb.Field_Date: - callback = func(req protoreflect.Message) any { - return req.Get(matchingRequestField).Message().Interface() - } - default: - return nil, fmt.Errorf("unsupported key type '%T' for column '%s'", keyColumn.Schema.Type, keyColumn.ColumnName) - } + requestReflect := smSpec.GetMethod.Request - pkFields[keyColumn.ColumnName] = callback - } + unmappedRequestFields := map[string]*j5schema.ObjectProperty{} + for _, field := range requestReflect.Properties { + unmappedRequestFields[field.JSONName] = field + } - if keyColumn.Primary { - eventJoinMap = append(eventJoinMap, pquery.JoinField{ - RootColumn: keyColumn.ColumnName, - JoinColumn: keyColumn.ColumnName, - }) + getPkFields := []KeyColumn{} + for _, keyColumn := range smSpec.KeyColumns { + _, ok := unmappedRequestFields[keyColumn.JSONFieldName] + if !ok { + continue } - + delete(unmappedRequestFields, keyColumn.JSONFieldName) + getPkFields = append(getPkFields, keyColumn) } if len(unmappedRequestFields) > 0 { @@ -164,7 +143,8 @@ func BuildStateQuerySet[ return nil, fmt.Errorf("unmapped fields in Get request: %v", fieldNames) } - getSpec := pquery.GetSpec[GetREQ, GetRES]{ + getSpec := pquery.GetSpec{ + Method: smSpec.GetMethod, TableName: smSpec.State.TableName, DataColumn: smSpec.State.Root.ColumnName, } @@ -177,33 +157,77 @@ func BuildStateQuerySet[ getSpec.Auth = options.Auth } - getSpec.PrimaryKey = func(req GetREQ) (map[string]any, error) { - refl := req.ProtoReflect() + getSpec.PrimaryKey = func(req j5reflect.Object) (map[string]any, error) { out := map[string]any{} - for k, v := range pkFields { - out[k] = v(refl) + for _, col := range getPkFields { + value, ok, err := req.GetField(col.JSONFieldName) + if err != nil { + return nil, fmt.Errorf("get primary key field '%s': %w", col.JSONFieldName, err) + } + if !ok { + return nil, fmt.Errorf("missing primary key field '%s'", col.JSONFieldName) + } + scalarValue, ok := value.AsScalar() + if !ok { + return nil, fmt.Errorf("primary key field '%s' is not a scalar value", col.JSONFieldName) + } + out[col.ColumnName], err = scalarValue.ToGoValue() + if err != nil { + return nil, fmt.Errorf("convert primary key field '%s' to Go value: %w", col.JSONFieldName, err) + } } return out, nil } - var eventsInGet protoreflect.Name + var eventsInGet *j5schema.ObjectProperty - getResponseReflect := (*new(GetRES)).ProtoReflect().Descriptor() - for i := range getResponseReflect.Fields().Len() { - field := getResponseReflect.Fields().Get(i) - msg := field.Message() - if msg == nil { - continue + getResponseReflect := smSpec.GetMethod.Response + for _, field := range getResponseReflect.Properties { + switch ft := field.Schema.(type) { + case *j5schema.ObjectField: + name := ft.ObjectSchema().FullName() + if name == smSpec.StateType.FullName() { + if getSpec.StateResponseField != "" { + return nil, fmt.Errorf("multiple state fields in Get response: %s and %s", getSpec.StateResponseField, field.FullName()) + } + getSpec.StateResponseField = field.JSONName + } else { + return nil, fmt.Errorf("unexpected object field in Get response: %s", name) + } + + case *j5schema.ArrayField: + itemSchema, ok := ft.ItemSchema.(*j5schema.ObjectField) + if !ok { + return nil, fmt.Errorf("expected array field in Get response, got: %T", ft.ItemSchema) + } + + name := itemSchema.ObjectSchema().FullName() + if name == smSpec.EventType.FullName() { + if eventsInGet != nil { + return nil, fmt.Errorf("multiple event fields in Get response: %s and %s", eventsInGet.FullName(), field.FullName()) + } + eventsInGet = field + } else { + return nil, fmt.Errorf("unexpected array field in Get response: %s", name) + } + + default: + return nil, fmt.Errorf("unexpected field in Get response: %s", field.FullName()) } + } - if msg.FullName() == smSpec.EventType.FullName() { - eventsInGet = field.Name() - } else if msg.FullName() == smSpec.StateType.FullName() { - getSpec.StateResponseField = field.Name() + eventJoinMap := pquery.JoinFields{} + for _, keyColumn := range smSpec.KeyColumns { + if keyColumn.Primary { + eventJoinMap = append(eventJoinMap, pquery.JoinField{ + RootColumn: keyColumn.ColumnName, + JoinColumn: keyColumn.ColumnName, + }) } + } - if eventsInGet != "" { + if eventsInGet != nil { if smSpec.Event.TableName == "" { return nil, fmt.Errorf("missing EventTable in state spec for %s", smSpec.State.TableName) } @@ -213,7 +237,7 @@ func BuildStateQuerySet[ getSpec.Join = &pquery.GetJoinSpec{ TableName: smSpec.Event.TableName, DataColumn: smSpec.Event.Root.ColumnName, - FieldInParent: eventsInGet, + FieldInParent: eventsInGet.JSONName, On: eventJoinMap, } } @@ -229,16 +253,18 @@ func BuildStateQuerySet[ if !field.Primary { continue } - keyPath := fmt.Sprintf("keys.%s", field.ProtoName) + keyPath := field.JSONFieldName statePrimaryKeys = append(statePrimaryKeys, - pquery.NewProtoField(keyPath, &field.ColumnName), + pquery.NewJSONField(keyPath, &field.ColumnName), ) } - listSpec := pquery.ListSpec[ListREQ, ListRES]{ + listSpec := pquery.ListSpec{ + Method: smSpec.ListMethod, TableSpec: pquery.TableSpec{ TableName: smSpec.State.TableName, DataColumn: smSpec.State.Root.ColumnName, + RootObject: smSpec.StateType, FallbackSortColumns: statePrimaryKeys, Auth: getSpec.Auth, AuthJoin: getSpec.AuthJoin, @@ -251,7 +277,7 @@ func BuildStateQuerySet[ return nil, fmt.Errorf("build main lister for state query '%s': %w", smSpec.State.TableName, err) } - querySet := &StateQuerySet[GetREQ, GetRES, ListREQ, ListRES, ListEventsREQ, ListEventsRES]{ + querySet := &StateQuerySet{ Getter: getter, MainLister: lister, } @@ -260,14 +286,16 @@ func BuildStateQuerySet[ return querySet, nil } - eventListSpec := pquery.ListSpec[ListEventsREQ, ListEventsRES]{ + eventListSpec := pquery.ListSpec{ + Method: smSpec.ListEventsMethod, TableSpec: pquery.TableSpec{ TableName: smSpec.Event.TableName, DataColumn: smSpec.Event.Root.ColumnName, + RootObject: smSpec.EventType, Auth: getSpec.Auth, AuthJoin: getSpec.AuthJoin, FallbackSortColumns: []pquery.ProtoField{ - pquery.NewProtoField("metadata.event_id", &smSpec.Event.ID.ColumnName), + pquery.NewJSONField("metadata.eventId", &smSpec.Event.ID.ColumnName), }, }, RequestFilter: smSpec.ListEventsRequestFilter, diff --git a/psm/statemachine.go b/psm/statemachine.go index 8e1acda..80ea863 100644 --- a/psm/statemachine.go +++ b/psm/statemachine.go @@ -5,20 +5,23 @@ import ( "database/sql" "errors" "fmt" - "github.com/pentops/j5/j5types/date_j5t" "time" + "github.com/pentops/j5/j5types/date_j5t" + "github.com/pentops/j5/lib/j5codec" + "github.com/pentops/j5/lib/j5reflect" + "github.com/pentops/j5/lib/j5schema" + "github.com/pentops/j5/lib/j5validate" + "buf.build/go/protovalidate" sq "github.com/elgris/sqrl" "github.com/google/uuid" "github.com/pentops/j5/gen/j5/state/v1/psm_j5pb" "github.com/pentops/log.go/log" - "github.com/pentops/protostate/internal/dbconvert" "github.com/pentops/sqrlx.go/sqrlx" "google.golang.org/grpc/codes" "google.golang.org/grpc/status" - "google.golang.org/protobuf/encoding/protojson" - "google.golang.org/protobuf/proto" + "google.golang.org/protobuf/encoding/prototext" "google.golang.org/protobuf/types/known/timestamppb" ) @@ -29,11 +32,6 @@ type Transactor interface { Transact(context.Context, *sqrlx.TxOptions, sqrlx.Callback) error } -// DEPRECATED: This does nothing. -func MustSystemActor(id string) struct{} { - return struct{}{} -} - // StateMachine is a database wrapper around the eventer. Using sane defaults // with overrides for table configuration. type StateMachine[ @@ -52,7 +50,13 @@ type StateMachine[ tableMap *TableMap - validator protovalidate.Validator + stateSchema *j5schema.ObjectSchema + eventSchema *j5schema.ObjectSchema + + validator *j5validate.Validator + protoValidator protovalidate.Validator + + codec *j5codec.Codec } func NewStateMachine[ @@ -65,12 +69,14 @@ func NewStateMachine[ ]( cb *StateMachineConfig[K, S, ST, SD, E, IE], ) (*StateMachine[K, S, ST, SD, E, IE], error) { + stateSchema, eventSchema, err := cb.schemas() + if err != nil { + return nil, fmt.Errorf("schemas: %w", err) + } if cb.tableMap == nil { - tableMap, err := tableMapFromStateAndEvent( - (*new(S)).ProtoReflect().Descriptor(), - (*new(E)).ProtoReflect().Descriptor(), - ) + + tableMap, err := tableMapFromStateAndEvent(stateSchema, eventSchema) if err != nil { return nil, err } @@ -81,19 +87,37 @@ func NewStateMachine[ return nil, err } + pv, err := protovalidate.New() + if err != nil { + return nil, fmt.Errorf("failed to initialize protovalidate: %w", err) + } + + codec := j5codec.NewCodec(j5codec.WithIncludeEmpty()) return &StateMachine[K, S, ST, SD, E, IE]{ keyValueFunc: cb.keyValues, initialStateFunc: cb.initialStateFunc, tableMap: cb.tableMap, - //SystemActor: cb.systemActor, + stateSchema: stateSchema, + eventSchema: eventSchema, + validator: j5validate.NewValidator(), + protoValidator: pv, + codec: codec, }, nil } +func (sm StateMachine[K, S, ST, SD, E, IE]) marshalJ5(obj j5reflect.Root) ([]byte, error) { + return sm.codec.ReflectToJSON(obj) +} + +func (sm StateMachine[K, S, ST, SD, E, IE]) unmarshalJ5(data []byte, obj j5reflect.Root) error { + return sm.codec.JSONToReflect(data, obj) +} + func (sm StateMachine[K, S, ST, SD, E, IE]) StateTableSpec() QueryTableSpec { return QueryTableSpec{ TableMap: *sm.tableMap, - EventType: (*new(E)).ProtoReflect().Descriptor(), - StateType: (*new(S)).ProtoReflect().Descriptor(), + EventType: sm.eventSchema, + StateType: sm.stateSchema, } } @@ -207,7 +231,8 @@ func (sm *DBStateMachine[K, S, ST, SD, E, IE]) FollowEvents(ctx context.Context, } func (sm *StateMachine[K, S, ST, SD, E, IE]) getCurrentState(ctx context.Context, tx sqrlx.Transaction, keys K) (S, error) { - state := (*new(S)).ProtoReflect().New().Interface().(S) + state := newJ5Message[S]() + stateRefl := state.J5Reflect() selectQuery := sq. Select(sm.tableMap.State.Root.ColumnName). @@ -227,7 +252,7 @@ func (sm *StateMachine[K, S, ST, SD, E, IE]) getCurrentState(ctx context.Context var stateJSON []byte err = tx.SelectRow(ctx, selectQuery).Scan(&stateJSON) if errors.Is(err, sql.ErrNoRows) { - state.SetPSMKeys(proto.Clone(keys).(K)) + state.SetPSMKeys(keys.Clone().(K)) if len(allKeys.missingRequired) > 0 { return state, fmt.Errorf("missing required key(s) %v in initial event", allKeys.missingRequired) @@ -241,7 +266,7 @@ func (sm *StateMachine[K, S, ST, SD, E, IE]) getCurrentState(ctx context.Context return state, fmt.Errorf("selecting current state (%s): %w", qq, err) } - if err := protojson.Unmarshal(stateJSON, state); err != nil { + if err := sm.unmarshalJ5(stateJSON, stateRefl); err != nil { return state, err } @@ -319,12 +344,12 @@ func (sm *StateMachine[K, S, ST, SD, E, IE]) store( event E, ) error { - stateDBValue, err := dbconvert.MarshalProto(state) + stateDBValue, err := sm.marshalJ5(state.J5Reflect()) if err != nil { return fmt.Errorf("state field: %w", err) } - eventDBValue, err := dbconvert.MarshalProto(event) + eventDBValue, err := sm.marshalJ5(event.J5Reflect()) if err != nil { return fmt.Errorf("event field: %w", err) } @@ -425,12 +450,12 @@ func (sm *StateMachine[K, S, ST, SD, E, IE]) followEventDeduplicate(ctx context. return false, fmt.Errorf("selecting event for deduplication: %w", err) } - existing := (*new(E)).ProtoReflect().New().Interface().(E) - if err := protojson.Unmarshal(eventData, existing); err != nil { + existing := newJ5Message[E]() + if err := sm.unmarshalJ5(eventData, existing.J5Reflect()); err != nil { return true, fmt.Errorf("unmarshalling event: %w", err) } - if !proto.Equal(existing, event) { + if !j5reflect.DeepEqual(existing.J5Reflect(), event.J5Reflect()) { return true, fmt.Errorf("event %s already exists with different data", existing.PSMMetadata().EventId) } @@ -442,7 +467,7 @@ func (sm *StateMachine[K, S, ST, SD, E, IE]) followEvents(ctx context.Context, t for _, event := range events { if err := sm.validateEvent(event); err != nil { - return fmt.Errorf("validating event %s: %w", event.ProtoReflect().Descriptor().FullName(), err) + return fmt.Errorf("validating event: %w", err) } exists, err := sm.followEventDeduplicate(ctx, tx, event) @@ -470,7 +495,7 @@ func (sm *StateMachine[K, S, ST, SD, E, IE]) followEvents(ctx context.Context, t err = sm.followEvent(ctx, tx, state, event) if err != nil { - return fmt.Errorf("run event %s (%s): %w", event.PSMMetadata().EventId, event.UnwrapPSMEvent().PSMEventKey(), err) + return fmt.Errorf("follow event %s (%s): %w", event.PSMMetadata().EventId, event.UnwrapPSMEvent().PSMEventKey(), err) } } @@ -508,7 +533,7 @@ func (sm *StateMachine[K, S, ST, SD, E, IE]) runInitialEvent(ctx context.Context // RunEvent modifies state in place returnState, err := sm.runEvent(ctx, tx, state, prepared, captureFinalState) if err != nil { - return state, fmt.Errorf("input event %s: %w", eventSpec.Event.PSMEventKey(), err) + return state, fmt.Errorf("run event %s: %w", eventSpec.Event.PSMEventKey(), err) } return *returnState, nil @@ -549,7 +574,7 @@ func assertPresentKeysMatch[K IKeyset](existing, event K) error { func (sm *StateMachine[K, S, ST, SD, E, IE]) runTx(ctx context.Context, tx sqrlx.Transaction, outerEvent *EventSpec[K, S, ST, SD, E, IE]) (S, error) { if err := outerEvent.validateAndPrepare(); err != nil { - return *new(S), fmt.Errorf("event %s: %w", outerEvent.Event.ProtoReflect().Descriptor().FullName(), err) + return *new(S), err } if outerEvent.EventID == "" { @@ -595,9 +620,8 @@ func (sm *StateMachine[K, S, ST, SD, E, IE]) runTx(ctx context.Context, tx sqrlx // RunEvent modifies state in place returnState, err := sm.runEvent(ctx, tx, state, prepared, captureInitialState) // return the state after the first transition - if err != nil { - return state, fmt.Errorf("input event %s: %w", outerEvent.Event.PSMEventKey(), err) + return state, fmt.Errorf("run event %s: %w", outerEvent.Event.PSMEventKey(), err) } return *returnState, nil @@ -622,22 +646,25 @@ func (sm *StateMachine[K, S, ST, SD, E, IE]) firstEventUniqueCheck(ctx context.C return s, false, fmt.Errorf("selecting event: %w", err) } - existing := (*new(E)).ProtoReflect().New().Interface().(E) + existing := newJ5Message[E]() - if err := protojson.Unmarshal(eventData, existing); err != nil { + if err := sm.unmarshalJ5(eventData, existing.J5Reflect()); err != nil { return s, false, fmt.Errorf("unmarshalling event: %w", err) } - if !proto.Equal(existing.UnwrapPSMEvent(), data) { + existingContent := existing.UnwrapPSMEvent() + + if !j5reflect.DeepEqual(existingContent.J5Reflect(), data.J5Reflect()) { + log.WithFields(ctx, "existing", prototext.Format(existingContent), "new", prototext.Format(data)).Info("event data does not match existing event data") return s, false, ErrDuplicateEventID } - state := (*new(S)).ProtoReflect().New() - if err := protojson.Unmarshal(stateData, state.Interface()); err != nil { + state := newJ5Message[S]() + if err := sm.unmarshalJ5(stateData, state.J5Reflect()); err != nil { return s, false, fmt.Errorf("unmarshalling state: %w", err) } - return state.Interface().(S), true, nil + return state, true, nil } func (sm *StateMachine[K, S, ST, SD, E, IE]) eventsMustBeUnique(ctx context.Context, tx sqrlx.Transaction, events ...*EventSpec[K, S, ST, SD, E, IE]) error { @@ -663,19 +690,23 @@ func (sm *StateMachine[K, S, ST, SD, E, IE]) eventsMustBeUnique(ctx context.Cont func (sm *StateMachine[K, S, ST, SD, E, IE]) validateEvent(event E) error { if sm.validator == nil { - v, err := protovalidate.New() - if err != nil { - fmt.Println("failed to initialize validator:", err) - } + v := j5validate.NewValidator() sm.validator = v } - return sm.validator.Validate(event) + err := sm.validator.Validate(event.J5Reflect()) + if err != nil { + txt := prototext.Format(event) + fmt.Printf("Event validation failed: %s\n%s\n", err.Error(), txt) + + return fmt.Errorf("validate event: %w", err) + } + return nil } func (sm *StateMachine[K, S, ST, SD, E, IE]) prepareEvent(state S, spec *EventSpec[K, S, ST, SD, E, IE]) (built E, err error) { - built = (*new(E)).ProtoReflect().New().Interface().(E) + built = newJ5Message[E]() if err := built.SetPSMEvent(spec.Event); err != nil { return built, fmt.Errorf("set event: %w", err) } diff --git a/psm/table_map.go b/psm/table_map.go index 696a652..64e2ec3 100644 --- a/psm/table_map.go +++ b/psm/table_map.go @@ -6,12 +6,9 @@ import ( "unicode" "github.com/iancoleman/strcase" - "github.com/pentops/j5/gen/j5/ext/v1/ext_j5pb" "github.com/pentops/j5/gen/j5/schema/v1/schema_j5pb" + "github.com/pentops/j5/lib/j5query" "github.com/pentops/j5/lib/j5schema" - "github.com/pentops/protostate/internal/pgstore" - "google.golang.org/protobuf/proto" - "google.golang.org/protobuf/reflect/protoreflect" ) type TableMap struct { @@ -82,14 +79,17 @@ type EventTableSpec struct { type StateTableSpec struct { TableName string + KeyField *j5schema.ObjectProperty + // The entire state message, as a JSONB Root *FieldSpec } type KeyColumn struct { ColumnName string - ProtoField protoreflect.FieldNumber - ProtoName protoreflect.Name + //ProtoField protoreflect.FieldNumber + //ProtoName protoreflect.Name + JSONFieldName string Primary bool Required bool @@ -105,7 +105,7 @@ type KeyField struct { ColumnName *string // Optional, stores in the table as a column. Primary bool Unique bool - Path *pgstore.Path + Path *j5query.Path } func safeTableName(name string) string { @@ -117,7 +117,7 @@ func safeTableName(name string) string { }, name) } -func BuildQueryTableSpec(stateMessage, eventMessage protoreflect.MessageDescriptor) (QueryTableSpec, error) { +func BuildQueryTableSpec(stateMessage, eventMessage *j5schema.ObjectSchema) (QueryTableSpec, error) { tableMap, err := tableMapFromStateAndEvent(stateMessage, eventMessage) if err != nil { return QueryTableSpec{}, err @@ -130,24 +130,23 @@ func BuildQueryTableSpec(stateMessage, eventMessage protoreflect.MessageDescript }, nil } -var globalSchemaCache = j5schema.NewSchemaCache() - -func buildDefaultTableMap(keyMessage protoreflect.MessageDescriptor) (*TableMap, error) { - stateObjectAnnotation, ok := proto.GetExtension(keyMessage.Options(), ext_j5pb.E_Psm).(*ext_j5pb.PSMOptions) - if !ok || stateObjectAnnotation == nil { - return nil, fmt.Errorf("message %s has no PSM Key field", keyMessage.Name()) +func buildDefaultTableMap(stateKeyField *j5schema.ObjectProperty, keyMessage *j5schema.ObjectSchema) (*TableMap, error) { + if keyMessage.Entity == nil { + return nil, fmt.Errorf("key message %s has no PSM Entity annotation", keyMessage.Name()) } + entity := keyMessage.Entity tm := &TableMap{ State: StateTableSpec{ - TableName: safeTableName(stateObjectAnnotation.EntityName), + TableName: safeTableName(entity.Entity), + KeyField: stateKeyField, Root: &FieldSpec{ ColumnName: "state", //PathFromRoot: psm.PathSpec{}, }, }, Event: EventTableSpec{ - TableName: safeTableName(stateObjectAnnotation.EntityName + "_event"), + TableName: safeTableName(entity.Entity + "_event"), Root: &FieldSpec{ ColumnName: "data", //PathFromRoot: psm.PathSpec{}, @@ -170,22 +169,9 @@ func buildDefaultTableMap(keyMessage protoreflect.MessageDescriptor) (*TableMap, }, } - schema, err := globalSchemaCache.Schema(keyMessage) - if err != nil { - return nil, nil - } - objSchema, ok := schema.(*j5schema.ObjectSchema) - if !ok { - return nil, fmt.Errorf("expected object schema, got %T", schema) - } - keyFields := keyMessage.Fields() - for _, field := range objSchema.Properties { - if len(field.ProtoField) != 1 { - return nil, fmt.Errorf("key field %s: expected one proto field, got %d", field.FullName(), len(field.ProtoField)) - } - desc := keyFields.ByNumber(field.ProtoField[0]) + for _, field := range keyMessage.Properties { - keyColumn, err := keyFieldColumn(field, desc) + keyColumn, err := keyFieldColumn(field) if err != nil { return nil, fmt.Errorf("field %s: %w", field.FullName(), err) } @@ -196,14 +182,15 @@ func buildDefaultTableMap(keyMessage protoreflect.MessageDescriptor) (*TableMap, return tm, nil } -func keyFieldColumn(field *j5schema.ObjectProperty, desc protoreflect.FieldDescriptor) (*KeyColumn, error) { +func keyFieldColumn(field *j5schema.ObjectProperty) (*KeyColumn, error) { isPrimary := field.Entity != nil && field.Entity.Primary kc := &KeyColumn{ - ColumnName: strcase.ToSnake(field.JSONName), - ProtoField: desc.Number(), - ProtoName: desc.Name(), + ColumnName: strcase.ToSnake(field.JSONName), + JSONFieldName: field.JSONName, + //ProtoField: desc.Number(), + //ProtoName: desc.Name(), Primary: isPrimary, Required: isPrimary || field.Required, ExplicitlyOptional: field.ExplicitlyOptional, @@ -213,59 +200,65 @@ func keyFieldColumn(field *j5schema.ObjectProperty, desc protoreflect.FieldDescr return kc, nil } -func tableMapFromStateAndEvent(stateMessage, eventMessage protoreflect.MessageDescriptor) (*TableMap, error) { +func tableMapFromStateAndEvent(stateMessage, eventMessage *j5schema.ObjectSchema) (*TableMap, error) { - var stateKeyField protoreflect.FieldDescriptor - var keyMessage protoreflect.MessageDescriptor + var stateKeyField *j5schema.ObjectProperty + var keyMessage *j5schema.ObjectSchema - fields := stateMessage.Fields() - for idx := range fields.Len() { - field := fields.Get(idx) - if field.Kind() != protoreflect.MessageKind { + for _, field := range stateMessage.Properties { + obj, ok := field.Schema.(*j5schema.ObjectField) + if !ok { + continue + } + objSchema := obj.ObjectSchema() + if objSchema.Entity == nil { continue } - msg := field.Message() - stateObjectAnnotation, ok := proto.GetExtension(msg.Options(), ext_j5pb.E_Psm).(*ext_j5pb.PSMOptions) - if ok && stateObjectAnnotation != nil { - keyMessage = msg - stateKeyField = field - break + if objSchema.Entity.Part != schema_j5pb.EntityPart_KEYS { + continue } + keyMessage = objSchema + stateKeyField = field + break } if stateKeyField == nil { return nil, fmt.Errorf("state message %s has no PSM Keys", stateMessage.FullName()) } - var eventKeysField protoreflect.FieldDescriptor + var eventKeyField *j5schema.ObjectProperty + var eventKeyMessage *j5schema.ObjectSchema + + for _, field := range eventMessage.Properties { - fields = eventMessage.Fields() - for idx := range fields.Len() { - field := fields.Get(idx) - if field.Kind() != protoreflect.MessageKind { + obj, ok := field.Schema.(*j5schema.ObjectField) + if !ok { + continue + } + objSchema := obj.ObjectSchema() + if objSchema.Entity == nil { continue } - msg := field.Message() - - stateObjectAnnotation, ok := proto.GetExtension(msg.Options(), ext_j5pb.E_Psm).(*ext_j5pb.PSMOptions) - if ok && stateObjectAnnotation != nil { - if keyMessage.FullName() != msg.FullName() { - return nil, fmt.Errorf("%s.%s is a %s, but %s.%s is a %s, these should be the same", - stateMessage.FullName(), - stateKeyField.Name(), - keyMessage.FullName(), - eventMessage.FullName(), - field.Name(), - msg.FullName(), - ) - } - eventKeysField = field + if objSchema.Entity.Part != schema_j5pb.EntityPart_KEYS { continue } + eventKeyMessage = objSchema + eventKeyField = field + } - if eventKeysField == nil { + if eventKeyField == nil { return nil, fmt.Errorf("event message %s has no PSM Keys", eventMessage.FullName()) } - return buildDefaultTableMap(keyMessage) + + if eventKeyMessage.FullName() != keyMessage.FullName() { + return nil, fmt.Errorf("state message %s has keys %s, but event message %s has keys %s", + stateMessage.FullName(), + stateKeyField.FullName(), + eventMessage.FullName(), + eventKeyField.FullName(), + ) + } + + return buildDefaultTableMap(stateKeyField, keyMessage) } diff --git a/psmigrate/migrate.go b/psmigrate/migrate.go deleted file mode 100644 index dde64e6..0000000 --- a/psmigrate/migrate.go +++ /dev/null @@ -1,10 +0,0 @@ -package psmigrate - -import ( - "github.com/pentops/protostate/internal/pgstore/pgmigrate" - "github.com/pentops/protostate/psm" -) - -func BuildStateMachineMigrations(specs ...psm.QueryTableSpec) ([]byte, error) { - return pgmigrate.BuildStateMachineMigrations(specs...) -} diff --git a/psmigrate/psm.go b/psmigrate/psm.go new file mode 100644 index 0000000..d6faa49 --- /dev/null +++ b/psmigrate/psm.go @@ -0,0 +1,165 @@ +package psmigrate + +import ( + "context" + "fmt" + + sq "github.com/elgris/sqrl" + "github.com/pentops/j5/lib/j5query/pgmigrate" + "github.com/pentops/protostate/psm" + "github.com/pentops/sqrlx.go/sqrlx" +) + +func BuildStateMachineMigrations(specs ...psm.QueryTableSpec) ([]byte, error) { + + allMigrations := make([]pgmigrate.MigrationItem, 0, len(specs)*4) + + for _, spec := range specs { + if err := spec.Validate(); err != nil { + return nil, fmt.Errorf("validate spec: %w", err) + } + stateTable, eventTable, err := BuildPSMTables(spec) + if err != nil { + return nil, err + } + allMigrations = append(allMigrations, stateTable, eventTable) + + stateList := spec.StateTable() + indexes, err := pgmigrate.IndexMigrations(stateList) + if err != nil { + return nil, err + } + + allMigrations = append(allMigrations, indexes...) + + eventList := spec.EventTable() + indexes, err = pgmigrate.IndexMigrations(eventList) + if err != nil { + return nil, err + } + + allMigrations = append(allMigrations, indexes...) + } + + fileData, err := pgmigrate.PrintMigrations(allMigrations...) + if err != nil { + return nil, err + } + return fileData, nil +} + +func CreateStateMachines(ctx context.Context, conn sqrlx.Connection, specs ...psm.QueryTableSpec) error { + db, err := sqrlx.New(conn, sq.Dollar) + if err != nil { + return err + } + + tables := make([]*pgmigrate.Table, 0, len(specs)) + for _, spec := range specs { + stateTable, eventTable, err := BuildPSMTables(spec) + if err != nil { + return err + } + tables = append(tables, stateTable, eventTable) + } + + return db.Transact(ctx, nil, func(ctx context.Context, tx sqrlx.Transaction) error { + if _, err := conn.BeginTx(ctx, nil); err != nil { + return err + } + + for _, table := range tables { + + statement, err := table.ToSQL() + if err != nil { + return err + } + _, err = tx.ExecRaw(ctx, statement) + if err != nil { + return err + } + } + return nil + }) +} + +func AddIndexes(ctx context.Context, conn sqrlx.Transactor, specs ...psm.QueryTableSpec) error { + indexes, err := BuildIndexes(specs...) + if err != nil { + return fmt.Errorf("build indexes: %w", err) + } + return pgmigrate.RunMigrations(ctx, conn, indexes) +} + +func BuildIndexes(specs ...psm.QueryTableSpec) ([]pgmigrate.MigrationItem, error) { + allIndexes := make([]pgmigrate.MigrationItem, 0) + for _, spec := range specs { + if err := spec.Validate(); err != nil { + return nil, fmt.Errorf("validate spec: %w", err) + } + stateList := spec.StateTable() + indexes, err := pgmigrate.IndexMigrations(stateList) + if err != nil { + return nil, err + } + allIndexes = append(allIndexes, indexes...) + + eventList := spec.EventTable() + indexes, err = pgmigrate.IndexMigrations(eventList) + if err != nil { + return nil, err + } + allIndexes = append(allIndexes, indexes...) + } + + return allIndexes, nil +} + +func BuildPSMTables(spec psm.QueryTableSpec) (*pgmigrate.Table, *pgmigrate.Table, error) { + + stateTable := pgmigrate.CreateTable(spec.State.TableName) + + eventTable := pgmigrate.CreateTable(spec.Event.TableName). + Column(spec.Event.ID.ColumnName, pgmigrate.UUID, pgmigrate.PrimaryKey) + + eventForeignKey := eventTable.ForeignKey("state", spec.State.TableName) + for _, key := range spec.KeyColumns { + format, err := pgmigrate.FieldFormat(key.Schema) + if err != nil { + return nil, nil, fmt.Errorf("key %s: %w", key.ColumnName, err) + } + + if key.Primary { + stateTable.Column(key.ColumnName, format, pgmigrate.PrimaryKey) + eventTable.Column(key.ColumnName, format, pgmigrate.NotNull) + eventForeignKey.Column(key.ColumnName, key.ColumnName) + continue + } + if key.Required { + stateTable.Column(key.ColumnName, format, pgmigrate.NotNull) + eventTable.Column(key.ColumnName, format, pgmigrate.NotNull) + continue + } + stateTable.Column(key.ColumnName, format) + eventTable.Column(key.ColumnName, format) + } + + stateTable.Column(spec.State.Root.ColumnName, pgmigrate.JSONB, pgmigrate.NotNull) + + eventTable.Column(spec.Event.Timestamp.ColumnName, pgmigrate.Timestamptz, pgmigrate.NotNull). + Column(spec.Event.Sequence.ColumnName, pgmigrate.Int, pgmigrate.NotNull). + Column(spec.Event.Root.ColumnName, pgmigrate.JSONB, pgmigrate.NotNull). + Column(spec.Event.StateSnapshot.ColumnName, pgmigrate.JSONB, pgmigrate.NotNull) + + state, err := stateTable.Build() + if err != nil { + return nil, nil, err + } + + event, err := eventTable.Build() + if err != nil { + return nil, nil, err + } + + return state, event, nil +} diff --git a/internal/pgstore/pgmigrate/psm_test.go b/psmigrate/psm_test.go similarity index 66% rename from internal/pgstore/pgmigrate/psm_test.go rename to psmigrate/psm_test.go index 33947af..b6e7db8 100644 --- a/internal/pgstore/pgmigrate/psm_test.go +++ b/psmigrate/psm_test.go @@ -1,4 +1,4 @@ -package pgmigrate +package psmigrate import ( "testing" @@ -10,16 +10,16 @@ import ( func TestBuildStateMachineOneKey(t *testing.T) { fooSpec, err := psm.BuildQueryTableSpec( - (&test_pb.FooState{}).ProtoReflect().Descriptor(), - (&test_pb.FooEvent{}).ProtoReflect().Descriptor(), + (&test_pb.FooState{}).J5Object().ObjectSchema(), + (&test_pb.FooEvent{}).J5Object().ObjectSchema(), ) if err != nil { t.Fatal(err) } barSpec, err := psm.BuildQueryTableSpec( - (&test_pb.BarState{}).ProtoReflect().Descriptor(), - (&test_pb.BarEvent{}).ProtoReflect().Descriptor(), + (&test_pb.BarState{}).J5Object().ObjectSchema(), + (&test_pb.BarEvent{}).J5Object().ObjectSchema(), ) if err != nil { t.Fatal(err)