From 4f31d35adae3d89f9c9f7e8f49757a4ba6570822 Mon Sep 17 00:00:00 2001 From: Tolga Ozen Date: Wed, 1 Nov 2023 21:27:57 +0300 Subject: [PATCH 1/3] test(#789): better test cases for postgres --- internal/servers/dataServer.go | 2 +- internal/storage/postgres/dataReader.go | 12 +- internal/storage/postgres/dataReader_test.go | 594 ++++++++++++++++++ internal/storage/postgres/dataWriter_test.go | 276 ++++++++ internal/storage/postgres/postgres_test.go | 107 ++++ .../storage/postgres/schemaReader_test.go | 190 ++++++ .../storage/postgres/schemaWriter_test.go | 92 +++ .../storage/postgres/tenantReader_test.go | 68 ++ .../storage/postgres/tenantWriter_test.go | 74 +++ .../storage/postgres/tests/dataReader_test.go | 100 --- .../storage/postgres/tests/dataWriter_test.go | 97 --- .../storage/postgres/tests/postgres_test.go | 93 --- .../tests/schemaReader_integration_test.go | 54 -- .../postgres/tests/schemaReader_test.go | 148 ----- .../tests/schemaWriter_integration_test.go | 47 -- .../postgres/tests/schemaWriter_test.go | 44 -- .../tests/tenantReader_integration_test.go | 58 -- .../postgres/tests/tenantReader_test.go | 44 -- .../tests/tenantWriter_integration_test.go | 49 -- .../postgres/tests/tenantWriter_test.go | 82 --- 20 files changed, 1410 insertions(+), 821 deletions(-) create mode 100644 internal/storage/postgres/dataReader_test.go create mode 100644 internal/storage/postgres/dataWriter_test.go create mode 100644 internal/storage/postgres/postgres_test.go create mode 100644 internal/storage/postgres/schemaReader_test.go create mode 100644 internal/storage/postgres/schemaWriter_test.go create mode 100644 internal/storage/postgres/tenantReader_test.go create mode 100644 internal/storage/postgres/tenantWriter_test.go delete mode 100644 internal/storage/postgres/tests/dataReader_test.go delete mode 100644 internal/storage/postgres/tests/dataWriter_test.go delete mode 100644 internal/storage/postgres/tests/postgres_test.go delete mode 100644 internal/storage/postgres/tests/schemaReader_integration_test.go delete mode 100644 internal/storage/postgres/tests/schemaReader_test.go delete mode 100644 internal/storage/postgres/tests/schemaWriter_integration_test.go delete mode 100644 internal/storage/postgres/tests/schemaWriter_test.go delete mode 100644 internal/storage/postgres/tests/tenantReader_integration_test.go delete mode 100644 internal/storage/postgres/tests/tenantReader_test.go delete mode 100644 internal/storage/postgres/tests/tenantWriter_integration_test.go delete mode 100644 internal/storage/postgres/tests/tenantWriter_test.go diff --git a/internal/servers/dataServer.go b/internal/servers/dataServer.go index 0a38df454..a65800b5d 100644 --- a/internal/servers/dataServer.go +++ b/internal/servers/dataServer.go @@ -258,7 +258,7 @@ func (r *DataServer) Delete(ctx context.Context, request *v1.DataDeleteRequest) return nil, v } - err := validation.ValidateTupleFilter(request.GetTupleFilter()) + err := validation.ValidateFilters(request.GetTupleFilter(), request.GetAttributeFilter()) if err != nil { return nil, v } diff --git a/internal/storage/postgres/dataReader.go b/internal/storage/postgres/dataReader.go index 261e25287..3b03e8b53 100644 --- a/internal/storage/postgres/dataReader.go +++ b/internal/storage/postgres/dataReader.go @@ -263,7 +263,7 @@ func (r *DataReader) QuerySingleAttribute(ctx context.Context, tenantID string, // Build the relationships query based on the provided filter and snapshot value. var args []interface{} - builder := r.database.Builder.Select("id, entity_type, entity_id, attribute, value").From(AttributesTable).Where(squirrel.Eq{"tenant_id": tenantID}) + builder := r.database.Builder.Select("entity_type, entity_id, attribute, value").From(AttributesTable).Where(squirrel.Eq{"tenant_id": tenantID}) builder = utils.AttributesFilterQueryForSelectBuilder(builder, filter) builder = utils.SnapshotQuery(builder, st.(snapshot.Token).Value.Uint) @@ -284,7 +284,7 @@ func (r *DataReader) QuerySingleAttribute(ctx context.Context, tenantID string, var valueStr string // Scan the row from the database into the fields of `rt` and `valueStr`. - err = row.Scan(&rt.ID, &rt.EntityType, &rt.EntityID, &rt.Attribute, &valueStr) + err = row.Scan(&rt.EntityType, &rt.EntityID, &rt.Attribute, &valueStr) if err != nil { if errors.Is(err, sql.ErrNoRows) { return nil, nil @@ -376,7 +376,7 @@ func (r *DataReader) QueryAttributes(ctx context.Context, tenantID string, filte var valueStr string // Scan the row from the database into the fields of `rt` and `valueStr`. - err := rows.Scan(&rt.ID, &rt.EntityType, &rt.EntityID, &rt.Attribute, &valueStr) + err := rows.Scan(&rt.EntityType, &rt.EntityID, &rt.Attribute, &valueStr) if err != nil { span.RecordError(err) span.SetStatus(codes.Error, err.Error()) @@ -665,7 +665,11 @@ func (r *DataReader) QueryUniqueSubjectReferences(ctx context.Context, tenantID defer utils.Rollback(tx) // Build the relationships query based on the provided filter, snapshot value, and pagination settings. - builder := r.database.Builder.Select("id, subject_id").Distinct().From(RelationTuplesTable).Where(squirrel.Eq{"tenant_id": tenantID}) + builder := r.database.Builder. + Select("MIN(id) as id, subject_id"). // This will pick the smallest `id` for each unique `subject_id`. + From(RelationTuplesTable). + Where(squirrel.Eq{"tenant_id": tenantID}). + GroupBy("subject_id") builder = utils.TuplesFilterQueryForSelectBuilder(builder, &base.TupleFilter{Subject: &base.SubjectFilter{Type: subjectReference.GetType(), Relation: subjectReference.GetRelation()}}) builder = utils.SnapshotQuery(builder, st.(snapshot.Token).Value.Uint) diff --git a/internal/storage/postgres/dataReader_test.go b/internal/storage/postgres/dataReader_test.go new file mode 100644 index 000000000..a42eabc56 --- /dev/null +++ b/internal/storage/postgres/dataReader_test.go @@ -0,0 +1,594 @@ +package postgres + +import ( + "context" + "os" + "time" + + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" + + "github.com/Permify/permify/pkg/attribute" + "github.com/Permify/permify/pkg/database" + PQDatabase "github.com/Permify/permify/pkg/database/postgres" + base "github.com/Permify/permify/pkg/pb/base/v1" + "github.com/Permify/permify/pkg/token" + "github.com/Permify/permify/pkg/tuple" +) + +var _ = Describe("DataReader", func() { + var db database.Database + var dataWriter *DataWriter + var dataReader *DataReader + + BeforeEach(func() { + version := os.Getenv("POSTGRES_VERSION") + + if version == "" { + version = "14" + } + + db = postgresDB(version) + dataWriter = NewDataWriter(db.(*PQDatabase.Postgres)) + dataReader = NewDataReader(db.(*PQDatabase.Postgres)) + }) + + AfterEach(func() { + err := db.Close() + Expect(err).ShouldNot(HaveOccurred()) + }) + + Context("Head Snapshot", func() { + It("should retrieve the most recent snapshot for a tenant", func() { + ctx := context.Background() + + var mostRecentSnapshot token.EncodedSnapToken + + // Insert multiple snapshots for a single tenant + for i := 0; i < 3; i++ { + + tup1, err := tuple.Tuple("organization:organization-1#admin@user:user-1") + Expect(err).ShouldNot(HaveOccurred()) + + tup2, err := tuple.Tuple("organization:organization-2#admin@user:user-1") + Expect(err).ShouldNot(HaveOccurred()) + + tuples := database.NewTupleCollection([]*base.Tuple{ + tup1, + tup2, + }...) + + attr1, err := attribute.Attribute("organization:1$public|boolean:true") + Expect(err).ShouldNot(HaveOccurred()) + + attr2, err := attribute.Attribute("organization:2$public|boolean:false") + Expect(err).ShouldNot(HaveOccurred()) + + attributes := database.NewAttributeCollection([]*base.Attribute{ + attr1, + attr2, + }...) + + token, err := dataWriter.Write(ctx, "t1", tuples, attributes) + Expect(err).ShouldNot(HaveOccurred()) + + mostRecentSnapshot = token + + time.Sleep(time.Millisecond * 2) + } + + // Attempt to retrieve the head snapshot from DataReader + headSnapshot, err := dataReader.HeadSnapshot(ctx, "t1") + Expect(err).ShouldNot(HaveOccurred()) + + // Validate that the retrieved head snapshot matches the most recently inserted snapshot + Expect(headSnapshot.Encode()).Should(Equal(mostRecentSnapshot), "The retrieved head snapshot should be the most recently written one.") + }) + }) + + Context("Query Relationships", func() { + It("should write relationships and query relationships correctly", func() { + ctx := context.Background() + + tup1, err := tuple.Tuple("organization:organization-1#admin@user:user-1") + Expect(err).ShouldNot(HaveOccurred()) + + tup2, err := tuple.Tuple("organization:organization-2#admin@user:user-1") + Expect(err).ShouldNot(HaveOccurred()) + + tuples1 := database.NewTupleCollection([]*base.Tuple{ + tup1, + tup2, + }...) + + token1, err := dataWriter.Write(ctx, "t1", tuples1, database.NewAttributeCollection()) + Expect(err).ShouldNot(HaveOccurred()) + + tup3, err := tuple.Tuple("organization:organization-1#admin@user:user-2") + Expect(err).ShouldNot(HaveOccurred()) + + tuples2 := database.NewTupleCollection([]*base.Tuple{ + tup3, + }...) + + token2, err := dataWriter.Write(ctx, "t1", tuples2, database.NewAttributeCollection()) + Expect(err).ShouldNot(HaveOccurred()) + + it1, err := dataReader.QueryRelationships(ctx, "t1", &base.TupleFilter{ + Entity: &base.EntityFilter{ + Type: "organization", + Ids: []string{"organization-1"}, + }, + }, token1.String()) + Expect(err).ShouldNot(HaveOccurred()) + + Expect(it1.HasNext()).Should(Equal(true)) + Expect(it1.GetNext()).Should(Equal(tup1)) + Expect(it1.HasNext()).Should(Equal(false)) + + it2, err := dataReader.QueryRelationships(ctx, "t1", &base.TupleFilter{ + Entity: &base.EntityFilter{ + Type: "organization", + Ids: []string{"organization-1"}, + }, + }, token2.String()) + Expect(err).ShouldNot(HaveOccurred()) + + Expect(it2.HasNext()).Should(Equal(true)) + Expect(it2.GetNext()).Should(Equal(tup1)) + Expect(it2.HasNext()).Should(Equal(true)) + Expect(it2.GetNext()).Should(Equal(tup3)) + Expect(it2.HasNext()).Should(Equal(false)) + }) + }) + + Context("Read Relationships", func() { + It("should write relationships and read relationships correctly", func() { + ctx := context.Background() + + tup1, err := tuple.Tuple("organization:organization-1#admin@user:user-1") + Expect(err).ShouldNot(HaveOccurred()) + + tup2, err := tuple.Tuple("organization:organization-2#admin@user:user-1") + Expect(err).ShouldNot(HaveOccurred()) + + tup3, err := tuple.Tuple("organization:organization-1#admin@user:user-2") + Expect(err).ShouldNot(HaveOccurred()) + + tup4, err := tuple.Tuple("organization:organization-1#admin@user:user-3") + Expect(err).ShouldNot(HaveOccurred()) + + tup5, err := tuple.Tuple("organization:organization-1#admin@user:user-4") + Expect(err).ShouldNot(HaveOccurred()) + + tup6, err := tuple.Tuple("organization:organization-1#admin@user:user-5") + Expect(err).ShouldNot(HaveOccurred()) + + tuples1 := database.NewTupleCollection([]*base.Tuple{ + tup1, + tup2, + tup3, + tup4, + tup5, + tup6, + }...) + + token1, err := dataWriter.Write(ctx, "t1", tuples1, database.NewAttributeCollection()) + Expect(err).ShouldNot(HaveOccurred()) + + col1, ct1, err := dataReader.ReadRelationships(ctx, "t1", &base.TupleFilter{ + Entity: &base.EntityFilter{ + Type: "organization", + Ids: []string{"organization-1"}, + }, + }, token1.String(), database.NewPagination(database.Size(2), database.Token(""))) + Expect(err).ShouldNot(HaveOccurred()) + + Expect(len(col1.GetTuples())).Should(Equal(2)) + + col2, ct2, err := dataReader.ReadRelationships(ctx, "t1", &base.TupleFilter{ + Entity: &base.EntityFilter{ + Type: "organization", + Ids: []string{"organization-1"}, + }, + }, token1.String(), database.NewPagination(database.Size(3), database.Token(ct1.String()))) + Expect(err).ShouldNot(HaveOccurred()) + + Expect(len(col2.GetTuples())).Should(Equal(3)) + Expect(ct2.String()).Should(Equal("")) + + token3, err := dataWriter.Delete(ctx, "t1", &base.TupleFilter{ + Entity: &base.EntityFilter{ + Type: "organization", + Ids: []string{"organization-1"}, + }, + Relation: "", + Subject: &base.SubjectFilter{ + Type: "user", + Ids: []string{"user-5"}, + }, + }, &base.AttributeFilter{}) + Expect(err).ShouldNot(HaveOccurred()) + + col3, ct3, err := dataReader.ReadRelationships(ctx, "t1", &base.TupleFilter{ + Entity: &base.EntityFilter{ + Type: "organization", + Ids: []string{"organization-1"}, + }, + }, token3.String(), database.NewPagination(database.Size(4), database.Token(""))) + Expect(err).ShouldNot(HaveOccurred()) + + Expect(len(col3.GetTuples())).Should(Equal(4)) + Expect(ct3.String()).Should(Equal("")) + }) + }) + + Context("Query Single Attribute", func() { + It("should write attributes and query single attributes correctly", func() { + ctx := context.Background() + + attr1, err := attribute.Attribute("organization:organization-1$public|boolean:true") + Expect(err).ShouldNot(HaveOccurred()) + + attr2, err := attribute.Attribute("organization:organization-2$public|boolean:false") + Expect(err).ShouldNot(HaveOccurred()) + + attributes := database.NewAttributeCollection([]*base.Attribute{ + attr1, + attr2, + }...) + + token1, err := dataWriter.Write(ctx, "t1", database.NewTupleCollection(), attributes) + Expect(err).ShouldNot(HaveOccurred()) + + attribute1, err := dataReader.QuerySingleAttribute(ctx, "t1", &base.AttributeFilter{ + Entity: &base.EntityFilter{ + Type: "organization", + Ids: []string{"organization-1"}, + }, + Attributes: []string{"public"}, + }, token1.String()) + Expect(err).ShouldNot(HaveOccurred()) + + Expect(attr1).Should(Equal(attribute1)) + + token2, err := dataWriter.Delete(ctx, "t1", + &base.TupleFilter{}, + &base.AttributeFilter{ + Entity: &base.EntityFilter{ + Type: "organization", + Ids: []string{"organization-1"}, + }, + Attributes: []string{"public"}, + }) + Expect(err).ShouldNot(HaveOccurred()) + + attribute2, err := dataReader.QuerySingleAttribute(ctx, "t1", &base.AttributeFilter{ + Entity: &base.EntityFilter{ + Type: "organization", + Ids: []string{"organization-1"}, + }, + Attributes: []string{"public"}, + }, token2.String()) + Expect(err).ShouldNot(HaveOccurred()) + Expect(attribute2).Should(BeNil()) + }) + }) + + Context("Query Attributes", func() { + It("should write attributes and query attributes correctly", func() { + ctx := context.Background() + + attr1, err := attribute.Attribute("organization:organization-2$public|boolean:true") + Expect(err).ShouldNot(HaveOccurred()) + + attr2, err := attribute.Attribute("organization:organization-1$ip_addresses|string[]:127.0.0.1,127.0.0.2") + Expect(err).ShouldNot(HaveOccurred()) + + attributes1 := database.NewAttributeCollection([]*base.Attribute{ + attr1, + attr2, + }...) + + token1, err := dataWriter.Write(ctx, "t1", database.NewTupleCollection(), attributes1) + Expect(err).ShouldNot(HaveOccurred()) + + attr3, err := attribute.Attribute("organization:organization-1$balance|integer:3000") + Expect(err).ShouldNot(HaveOccurred()) + + attributes2 := database.NewAttributeCollection([]*base.Attribute{ + attr3, + }...) + + token2, err := dataWriter.Write(ctx, "t1", database.NewTupleCollection(), attributes2) + Expect(err).ShouldNot(HaveOccurred()) + + it1, err := dataReader.QueryAttributes(ctx, "t1", &base.AttributeFilter{ + Entity: &base.EntityFilter{ + Type: "organization", + Ids: []string{"organization-1"}, + }, + }, token1.String()) + Expect(err).ShouldNot(HaveOccurred()) + + Expect(it1.HasNext()).Should(Equal(true)) + Expect(it1.GetNext()).Should(Equal(attr2)) + Expect(it1.HasNext()).Should(Equal(false)) + + it2, err := dataReader.QueryAttributes(ctx, "t1", &base.AttributeFilter{ + Entity: &base.EntityFilter{ + Type: "organization", + Ids: []string{"organization-1"}, + }, + }, token2.String()) + Expect(err).ShouldNot(HaveOccurred()) + + Expect(it2.HasNext()).Should(Equal(true)) + Expect(it2.GetNext()).Should(Equal(attr3)) + Expect(it2.HasNext()).Should(Equal(true)) + Expect(it2.GetNext()).Should(Equal(attr2)) + Expect(it2.HasNext()).Should(Equal(false)) + }) + }) + + Context("Read Attributes", func() { + It("should write attributes and read attributes correctly", func() { + ctx := context.Background() + + attr1, err := attribute.Attribute("organization:organization-1$public|boolean:true") + Expect(err).ShouldNot(HaveOccurred()) + + attr2, err := attribute.Attribute("organization:organization-2$ip_addresses|string[]:127.0.0.1,127.0.0.2") + Expect(err).ShouldNot(HaveOccurred()) + + attr3, err := attribute.Attribute("organization:organization-1$ip_addresses|string[]:127.0.0.1,127.0.0.2") + Expect(err).ShouldNot(HaveOccurred()) + + attr4, err := attribute.Attribute("organization:organization-1$balance|integer:3000") + Expect(err).ShouldNot(HaveOccurred()) + + attr5, err := attribute.Attribute("organization:organization-1$private|boolean:false") + Expect(err).ShouldNot(HaveOccurred()) + + attr6, err := attribute.Attribute("organization:organization-1$ppp|boolean[]:true,false") + Expect(err).ShouldNot(HaveOccurred()) + + attributes1 := database.NewAttributeCollection([]*base.Attribute{ + attr1, + attr2, + attr3, + attr4, + attr5, + attr6, + }...) + + token1, err := dataWriter.Write(ctx, "t1", database.NewTupleCollection(), attributes1) + Expect(err).ShouldNot(HaveOccurred()) + + col1, ct1, err := dataReader.ReadAttributes(ctx, "t1", &base.AttributeFilter{ + Entity: &base.EntityFilter{ + Type: "organization", + Ids: []string{"organization-1"}, + }, + }, token1.String(), database.NewPagination(database.Size(2), database.Token(""))) + Expect(err).ShouldNot(HaveOccurred()) + + Expect(len(col1.GetAttributes())).Should(Equal(2)) + + col2, ct2, err := dataReader.ReadAttributes(ctx, "t1", &base.AttributeFilter{ + Entity: &base.EntityFilter{ + Type: "organization", + Ids: []string{"organization-1"}, + }, + }, token1.String(), database.NewPagination(database.Size(3), database.Token(ct1.String()))) + Expect(err).ShouldNot(HaveOccurred()) + + Expect(len(col2.GetAttributes())).Should(Equal(3)) + Expect(ct2.String()).Should(Equal("")) + + token3, err := dataWriter.Delete(ctx, "t1", + &base.TupleFilter{}, + &base.AttributeFilter{ + Entity: &base.EntityFilter{ + Type: "organization", + Ids: []string{"organization-1"}, + }, + Attributes: []string{"ppp"}, + }) + Expect(err).ShouldNot(HaveOccurred()) + + col3, ct3, err := dataReader.ReadAttributes(ctx, "t1", &base.AttributeFilter{ + Entity: &base.EntityFilter{ + Type: "organization", + Ids: []string{"organization-1"}, + }, + }, token3.String(), database.NewPagination(database.Size(4), database.Token(""))) + Expect(err).ShouldNot(HaveOccurred()) + + Expect(len(col3.GetAttributes())).Should(Equal(4)) + Expect(ct3.String()).Should(Equal("")) + }) + }) + + Context("Query Unique Entities", func() { + It("should write entities and query unique entities correctly", func() { + ctx := context.Background() + + attr1, err := attribute.Attribute("organization:organization-1$public|boolean:true") + Expect(err).ShouldNot(HaveOccurred()) + + attr2, err := attribute.Attribute("organization:organization-2$ip_addresses|string[]:127.0.0.1,127.0.0.2") + Expect(err).ShouldNot(HaveOccurred()) + + attr3, err := attribute.Attribute("organization:organization-3$ip_addresses|string[]:127.0.0.1,127.0.0.5") + Expect(err).ShouldNot(HaveOccurred()) + + attr4, err := attribute.Attribute("organization:organization-16$balance|integer:3000") + Expect(err).ShouldNot(HaveOccurred()) + + attr5, err := attribute.Attribute("organization:organization-28$private|boolean:false") + Expect(err).ShouldNot(HaveOccurred()) + + attr6, err := attribute.Attribute("organization:organization-17$ppp|boolean[]:true,false") + Expect(err).ShouldNot(HaveOccurred()) + + attr7, err := attribute.Attribute("organization:organization-1$ip_addresses|string[]:127.0.0.1,127.0.0.2") + Expect(err).ShouldNot(HaveOccurred()) + + tup1, err := tuple.Tuple("organization:organization-1#admin@user:user-1") + Expect(err).ShouldNot(HaveOccurred()) + + tup2, err := tuple.Tuple("organization:organization-28#admin@user:user-1") + Expect(err).ShouldNot(HaveOccurred()) + + tup3, err := tuple.Tuple("organization:organization-19#admin@user:user-2") + Expect(err).ShouldNot(HaveOccurred()) + + tup4, err := tuple.Tuple("organization:organization-10#admin@user:user-3") + Expect(err).ShouldNot(HaveOccurred()) + + tup5, err := tuple.Tuple("organization:organization-14#admin@user:user-4") + Expect(err).ShouldNot(HaveOccurred()) + + tup6, err := tuple.Tuple("repository:repository-13#admin@user:user-5") + Expect(err).ShouldNot(HaveOccurred()) + + attributes1 := database.NewAttributeCollection([]*base.Attribute{ + attr1, + attr2, + attr3, + attr4, + attr5, + attr6, + attr7, + }...) + + tuples1 := database.NewTupleCollection([]*base.Tuple{ + tup1, + tup2, + tup3, + tup4, + tup5, + tup6, + }...) + + token1, err := dataWriter.Write(ctx, "t1", tuples1, attributes1) + Expect(err).ShouldNot(HaveOccurred()) + + ids1, ct1, err := dataReader.QueryUniqueEntities(ctx, "t1", "organization", token1.String(), database.NewPagination(database.Size(8), database.Token(""))) + Expect(err).ShouldNot(HaveOccurred()) + Expect(len(ids1)).Should(Equal(8)) + + ids2, ct2, err := dataReader.QueryUniqueEntities(ctx, "t1", "organization", token1.String(), database.NewPagination(database.Size(8), database.Token(ct1.String()))) + Expect(err).ShouldNot(HaveOccurred()) + Expect(len(ids2)).Should(Equal(1)) + Expect(ct2.String()).Should(Equal("")) + + ids3, ct3, err := dataReader.QueryUniqueEntities(ctx, "t1", "organization", token1.String(), database.NewPagination(database.Size(20), database.Token(""))) + Expect(err).ShouldNot(HaveOccurred()) + Expect(len(ids3)).Should(Equal(9)) + Expect(ct3.String()).Should(Equal("")) + + Expect(isSameArray(ids3, []string{"organization-1", "organization-2", "organization-3", "organization-19", "organization-10", "organization-16", "organization-14", "organization-28", "organization-17"})).Should(BeTrue()) + + token2, err := dataWriter.Delete(ctx, "t1", + &base.TupleFilter{}, + &base.AttributeFilter{ + Entity: &base.EntityFilter{ + Type: "organization", + Ids: []string{"organization-17"}, + }, + }) + Expect(err).ShouldNot(HaveOccurred()) + + ids4, ct4, err := dataReader.QueryUniqueEntities(ctx, "t1", "organization", token2.String(), database.NewPagination(database.Size(20), database.Token(""))) + Expect(err).ShouldNot(HaveOccurred()) + Expect(len(ids4)).Should(Equal(8)) + Expect(ct4.String()).Should(Equal("")) + + Expect(isSameArray(ids4, []string{"organization-1", "organization-2", "organization-3", "organization-19", "organization-10", "organization-16", "organization-14", "organization-28"})).Should(BeTrue()) + }) + }) + + Context("Query Unique Subject References", func() { + It("should write tuples and query unique subject references correctly", func() { + ctx := context.Background() + + attr1, err := attribute.Attribute("organization:organization-1$public|boolean:true") + Expect(err).ShouldNot(HaveOccurred()) + + attr2, err := attribute.Attribute("organization:organization-2$ip_addresses|string[]:127.0.0.1,127.0.0.2") + Expect(err).ShouldNot(HaveOccurred()) + + tup1, err := tuple.Tuple("organization:organization-1#admin@user:user-1") + Expect(err).ShouldNot(HaveOccurred()) + + tup2, err := tuple.Tuple("organization:organization-3#admin@user:user-1") + Expect(err).ShouldNot(HaveOccurred()) + + tup3, err := tuple.Tuple("organization:organization-19#admin@user:user-2") + Expect(err).ShouldNot(HaveOccurred()) + + tup4, err := tuple.Tuple("organization:organization-10#admin@user:user-3") + Expect(err).ShouldNot(HaveOccurred()) + + tup5, err := tuple.Tuple("organization:organization-14#admin@organization:organization-8#member") + Expect(err).ShouldNot(HaveOccurred()) + + tup6, err := tuple.Tuple("repository:repository-13#admin@user:user-5") + Expect(err).ShouldNot(HaveOccurred()) + + attributes1 := database.NewAttributeCollection([]*base.Attribute{ + attr1, + attr2, + }...) + + tuples1 := database.NewTupleCollection([]*base.Tuple{ + tup1, + tup2, + tup3, + tup4, + tup5, + tup6, + }...) + + token1, err := dataWriter.Write(ctx, "t1", tuples1, attributes1) + Expect(err).ShouldNot(HaveOccurred()) + + refs1, ct1, err := dataReader.QueryUniqueSubjectReferences(ctx, "t1", &base.RelationReference{ + Type: "user", + Relation: "", + }, token1.String(), database.NewPagination(database.Size(2), database.Token(""))) + Expect(err).ShouldNot(HaveOccurred()) + Expect(len(refs1)).Should(Equal(2)) + + refs2, ct2, err := dataReader.QueryUniqueSubjectReferences(ctx, "t1", &base.RelationReference{ + Type: "user", + Relation: "", + }, token1.String(), database.NewPagination(database.Size(2), database.Token(ct1.String()))) + Expect(err).ShouldNot(HaveOccurred()) + Expect(len(refs2)).Should(Equal(2)) + Expect(ct2.String()).Should(Equal("")) + + refs3, ct3, err := dataReader.QueryUniqueSubjectReferences(ctx, "t1", &base.RelationReference{ + Type: "user", + Relation: "", + }, token1.String(), database.NewPagination(database.Size(20), database.Token(""))) + Expect(err).ShouldNot(HaveOccurred()) + Expect(len(refs3)).Should(Equal(4)) + Expect(ct3.String()).Should(Equal("")) + + Expect(isSameArray(refs3, []string{"user-1", "user-2", "user-3", "user-5"})).Should(BeTrue()) + + refs4, ct4, err := dataReader.QueryUniqueSubjectReferences(ctx, "t1", &base.RelationReference{ + Type: "organization", + Relation: "member", + }, token1.String(), database.NewPagination(database.Size(20), database.Token(""))) + Expect(err).ShouldNot(HaveOccurred()) + Expect(len(refs4)).Should(Equal(1)) + Expect(ct4.String()).Should(Equal("")) + + Expect(isSameArray(refs4, []string{"organization-8"})).Should(BeTrue()) + }) + }) +}) diff --git a/internal/storage/postgres/dataWriter_test.go b/internal/storage/postgres/dataWriter_test.go new file mode 100644 index 000000000..6b5f5cbbb --- /dev/null +++ b/internal/storage/postgres/dataWriter_test.go @@ -0,0 +1,276 @@ +package postgres + +import ( + "context" + "os" + + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" + + "github.com/Permify/permify/pkg/attribute" + "github.com/Permify/permify/pkg/database" + PQDatabase "github.com/Permify/permify/pkg/database/postgres" + base "github.com/Permify/permify/pkg/pb/base/v1" + "github.com/Permify/permify/pkg/tuple" +) + +var _ = Describe("DataWriter", func() { + var db database.Database + var dataWriter *DataWriter + var dataReader *DataReader + + BeforeEach(func() { + version := os.Getenv("POSTGRES_VERSION") + + if version == "" { + version = "14" + } + + db = postgresDB(version) + dataWriter = NewDataWriter(db.(*PQDatabase.Postgres)) + dataReader = NewDataReader(db.(*PQDatabase.Postgres)) + }) + + AfterEach(func() { + err := db.Close() + Expect(err).ShouldNot(HaveOccurred()) + }) + + Context("Write", func() { + It("The test case verifies that an attribute's value for an entity can be updated and subsequently retrieved correctly using MVCC tokens", func() { + ctx := context.Background() + + attr1, err := attribute.Attribute("organization:organization-1$public|boolean:true") + Expect(err).ShouldNot(HaveOccurred()) + + tup1, err := tuple.Tuple("organization:organization-1#admin@user:user-1") + Expect(err).ShouldNot(HaveOccurred()) + + attributes1 := database.NewAttributeCollection([]*base.Attribute{ + attr1, + }...) + + tuples1 := database.NewTupleCollection([]*base.Tuple{ + tup1, + }...) + + token1, err := dataWriter.Write(ctx, "t1", tuples1, attributes1) + Expect(err).ShouldNot(HaveOccurred()) + Expect(token1.String()).ShouldNot(BeNil()) + + attrRes1, err := dataReader.QuerySingleAttribute(ctx, "t1", &base.AttributeFilter{ + Entity: &base.EntityFilter{ + Type: "organization", + Ids: []string{"organization-1"}, + }, + Attributes: []string{"public"}, + }, token1.String()) + Expect(err).ShouldNot(HaveOccurred()) + + var msg1 base.BooleanValue + err = attrRes1.GetValue().UnmarshalTo(&msg1) + Expect(err).ShouldNot(HaveOccurred()) + + Expect(msg1.GetData()).Should(Equal(true)) + + attr2, err := attribute.Attribute("organization:organization-1$public|boolean:false") + Expect(err).ShouldNot(HaveOccurred()) + + attributes2 := database.NewAttributeCollection([]*base.Attribute{ + attr2, + }...) + + token2, err := dataWriter.Write(ctx, "t1", database.NewTupleCollection(), attributes2) + Expect(err).ShouldNot(HaveOccurred()) + Expect(token2.String()).ShouldNot(Equal("")) + + attrRes2, err := dataReader.QuerySingleAttribute(ctx, "t1", &base.AttributeFilter{ + Entity: &base.EntityFilter{ + Type: "organization", + Ids: []string{"organization-1"}, + }, + Attributes: []string{"public"}, + }, token2.String()) + Expect(err).ShouldNot(HaveOccurred()) + + var msg2 base.BooleanValue + err = attrRes2.GetValue().UnmarshalTo(&msg2) + Expect(err).ShouldNot(HaveOccurred()) + Expect(msg2.GetData()).Should(Equal(false)) + }) + + It("should write attributes and tuples correctly", func() { + ctx := context.Background() + + attr1, err := attribute.Attribute("organization:organization-1$public|boolean:true") + Expect(err).ShouldNot(HaveOccurred()) + + attr2, err := attribute.Attribute("organization:organization-2$ip_addresses|string[]:127.0.0.1,127.0.0.2") + Expect(err).ShouldNot(HaveOccurred()) + + attr3, err := attribute.Attribute("organization:organization-3$ip_addresses|double:234.344") + Expect(err).ShouldNot(HaveOccurred()) + + attr4, err := attribute.Attribute("organization:organization-16$balance|integer:3000") + Expect(err).ShouldNot(HaveOccurred()) + + attr5, err := attribute.Attribute("organization:organization-28$private|boolean:false") + Expect(err).ShouldNot(HaveOccurred()) + + attr6, err := attribute.Attribute("organization:organization-17$ppp|boolean[]:true,false") + Expect(err).ShouldNot(HaveOccurred()) + + attr7, err := attribute.Attribute("organization:organization-1$ip_addresses|integer[]:167,878") + Expect(err).ShouldNot(HaveOccurred()) + + tup1, err := tuple.Tuple("organization:organization-1#admin@user:user-1") + Expect(err).ShouldNot(HaveOccurred()) + + tup2, err := tuple.Tuple("organization:organization-28#admin@user:user-1") + Expect(err).ShouldNot(HaveOccurred()) + + tup3, err := tuple.Tuple("organization:organization-19#admin@user:user-2") + Expect(err).ShouldNot(HaveOccurred()) + + tup4, err := tuple.Tuple("organization:organization-10#admin@user:user-3") + Expect(err).ShouldNot(HaveOccurred()) + + tup5, err := tuple.Tuple("organization:organization-14#admin@user:user-4") + Expect(err).ShouldNot(HaveOccurred()) + + tup6, err := tuple.Tuple("repository:repository-13#admin@user:user-5") + Expect(err).ShouldNot(HaveOccurred()) + + attributes1 := database.NewAttributeCollection([]*base.Attribute{ + attr1, + attr2, + attr3, + attr4, + attr5, + attr6, + attr7, + }...) + + tuples1 := database.NewTupleCollection([]*base.Tuple{ + tup1, + tup2, + tup3, + tup4, + tup5, + tup6, + }...) + + token1, err := dataWriter.Write(ctx, "t1", tuples1, attributes1) + Expect(err).ShouldNot(HaveOccurred()) + Expect(token1.String()).ShouldNot(Equal("")) + }) + + It("should write empty attributes and empty tuples correctly", func() { + ctx := context.Background() + token1, err := dataWriter.Write(ctx, "t1", database.NewTupleCollection(), database.NewAttributeCollection()) + Expect(err).ShouldNot(HaveOccurred()) + Expect(token1.String()).ShouldNot(Equal("")) + }) + }) + + Context("Delete", func() { + It("should delete, read relationships and read attributes correctly", func() { + ctx := context.Background() + + attr1, err := attribute.Attribute("organization:organization-1$public|boolean:true") + Expect(err).ShouldNot(HaveOccurred()) + + attr2, err := attribute.Attribute("organization:organization-1$ip_addresses|string[]:127.0.0.1,127.0.0.2") + Expect(err).ShouldNot(HaveOccurred()) + + attr3, err := attribute.Attribute("organization:organization-3$balance|double:234.344") + Expect(err).ShouldNot(HaveOccurred()) + + tup1, err := tuple.Tuple("organization:organization-1#admin@user:user-1") + Expect(err).ShouldNot(HaveOccurred()) + + tup2, err := tuple.Tuple("organization:organization-1#admin@user:user-4") + Expect(err).ShouldNot(HaveOccurred()) + + tup3, err := tuple.Tuple("organization:organization-1#admin@user:user-2") + Expect(err).ShouldNot(HaveOccurred()) + + attributes1 := database.NewAttributeCollection([]*base.Attribute{ + attr1, + attr2, + attr3, + }...) + + tuples1 := database.NewTupleCollection([]*base.Tuple{ + tup1, + tup2, + tup3, + }...) + + token1, err := dataWriter.Write(ctx, "t1", tuples1, attributes1) + Expect(err).ShouldNot(HaveOccurred()) + Expect(token1.String()).ShouldNot(Equal("")) + + col1, ct1, err := dataReader.ReadRelationships(ctx, "t1", &base.TupleFilter{ + Entity: &base.EntityFilter{ + Type: "organization", + Ids: []string{"organization-1"}, + }, + }, token1.String(), database.NewPagination(database.Size(10), database.Token(""))) + Expect(err).ShouldNot(HaveOccurred()) + Expect(ct1.String()).Should(Equal("")) + Expect(len(col1.GetTuples())).Should(Equal(3)) + + col2, ct2, err := dataReader.ReadAttributes(ctx, "t1", &base.AttributeFilter{ + Entity: &base.EntityFilter{ + Type: "organization", + Ids: []string{"organization-1"}, + }, + }, token1.String(), database.NewPagination(database.Size(10), database.Token(""))) + Expect(err).ShouldNot(HaveOccurred()) + Expect(ct2.String()).Should(Equal("")) + Expect(len(col2.GetAttributes())).Should(Equal(2)) + + token2, err := dataWriter.Delete(ctx, "t1", + &base.TupleFilter{ + Entity: &base.EntityFilter{ + Type: "organization", + Ids: []string{"organization-1"}, + }, + Relation: "admin", + Subject: &base.SubjectFilter{ + Type: "user", + Ids: []string{"user-1"}, + }, + }, + &base.AttributeFilter{ + Entity: &base.EntityFilter{ + Type: "organization", + Ids: []string{"organization-1"}, + }, + Attributes: []string{"public"}, + }) + Expect(err).ShouldNot(HaveOccurred()) + + col3, ct3, err := dataReader.ReadRelationships(ctx, "t1", &base.TupleFilter{ + Entity: &base.EntityFilter{ + Type: "organization", + Ids: []string{"organization-1"}, + }, + }, token2.String(), database.NewPagination(database.Size(10), database.Token(""))) + Expect(err).ShouldNot(HaveOccurred()) + Expect(ct3.String()).Should(Equal("")) + Expect(len(col3.GetTuples())).Should(Equal(2)) + + col4, ct5, err := dataReader.ReadAttributes(ctx, "t1", &base.AttributeFilter{ + Entity: &base.EntityFilter{ + Type: "organization", + Ids: []string{"organization-1"}, + }, + }, token2.String(), database.NewPagination(database.Size(10), database.Token(""))) + Expect(err).ShouldNot(HaveOccurred()) + Expect(ct5.String()).Should(Equal("")) + Expect(len(col4.GetAttributes())).Should(Equal(1)) + }) + }) +}) diff --git a/internal/storage/postgres/postgres_test.go b/internal/storage/postgres/postgres_test.go new file mode 100644 index 000000000..825ad798e --- /dev/null +++ b/internal/storage/postgres/postgres_test.go @@ -0,0 +1,107 @@ +package postgres + +import ( + "context" + "fmt" + "sort" + "testing" + + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" + "github.com/testcontainers/testcontainers-go" + "github.com/testcontainers/testcontainers-go/wait" + + "github.com/Permify/permify/internal/config" + "github.com/Permify/permify/internal/storage" + "github.com/Permify/permify/pkg/database" + PQDatabase "github.com/Permify/permify/pkg/database/postgres" +) + +func TestPostgres14(t *testing.T) { + RegisterFailHandler(Fail) + RunSpecs(t, "postgres-suite") +} + +func postgresDB(postgresVersion string) database.Database { + ctx := context.Background() + + image := fmt.Sprintf("postgres:%s-alpine", postgresVersion) + + postgres, err := testcontainers.GenericContainer(ctx, testcontainers.GenericContainerRequest{ + ContainerRequest: testcontainers.ContainerRequest{ + Image: image, + ExposedPorts: []string{"5432/tcp"}, + WaitingFor: wait.ForLog("database system is ready to accept connections"), + Env: map[string]string{"POSTGRES_USER": "postgres", "POSTGRES_PASSWORD": "postgres", "POSTGRES_DB": "permify"}, + }, + Started: true, + }) + if err != nil { + Expect(err).ShouldNot(HaveOccurred()) + } + + cmd := []string{"sh", "-c", "export PGPASSWORD=postgres" + "; psql -U postgres -d permify -c 'DROP SCHEMA public CASCADE; CREATE SCHEMA public;'"} + + _, _, err = postgres.Exec(context.Background(), cmd) + if err != nil { + Expect(err).ShouldNot(HaveOccurred()) + } + + host, err := postgres.Host(ctx) + if err != nil { + Expect(err).ShouldNot(HaveOccurred()) + } + + port, err := postgres.MappedPort(ctx, "5432") + if err != nil { + Expect(err).ShouldNot(HaveOccurred()) + } + dbAddr := fmt.Sprintf("%s:%s", host, port.Port()) + postgresDSN := fmt.Sprintf("postgres://%s:%s@%s/%s?sslmode=disable", "postgres", "postgres", dbAddr, "permify") + + cfg := config.Database{ + Engine: "postgres", + URI: postgresDSN, + AutoMigrate: true, + MaxOpenConnections: 20, + MaxIdleConnections: 1, + MaxConnectionLifetime: 300, + MaxConnectionIdleTime: 60, + } + + err = storage.Migrate(cfg) + Expect(err).ShouldNot(HaveOccurred()) + + var db database.Database + db, err = PQDatabase.New(cfg.URI, + PQDatabase.MaxOpenConnections(cfg.MaxOpenConnections), + PQDatabase.MaxIdleConnections(cfg.MaxIdleConnections), + PQDatabase.MaxConnectionIdleTime(cfg.MaxConnectionIdleTime), + PQDatabase.MaxConnectionLifeTime(cfg.MaxConnectionLifetime), + ) + + return db +} + +// isSameArray - check if two arrays are the same +func isSameArray(a, b []string) bool { + if len(a) != len(b) { + return false + } + + sortedA := make([]string, len(a)) + copy(sortedA, a) + sort.Strings(sortedA) + + sortedB := make([]string, len(b)) + copy(sortedB, b) + sort.Strings(sortedB) + + for i := range sortedA { + if sortedA[i] != sortedB[i] { + return false + } + } + + return true +} diff --git a/internal/storage/postgres/schemaReader_test.go b/internal/storage/postgres/schemaReader_test.go new file mode 100644 index 000000000..da0cec87a --- /dev/null +++ b/internal/storage/postgres/schemaReader_test.go @@ -0,0 +1,190 @@ +package postgres + +import ( + "context" + "os" + "time" + + "github.com/rs/xid" + + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" + + "github.com/Permify/permify/internal/storage" + "github.com/Permify/permify/pkg/database" + PQDatabase "github.com/Permify/permify/pkg/database/postgres" + base "github.com/Permify/permify/pkg/pb/base/v1" +) + +var _ = Describe("SchemaReader", func() { + var db database.Database + var schemaWriter *SchemaWriter + var schemaReader *SchemaReader + + BeforeEach(func() { + version := os.Getenv("POSTGRES_VERSION") + + if version == "" { + version = "14" + } + + db = postgresDB(version) + schemaWriter = NewSchemaWriter(db.(*PQDatabase.Postgres)) + schemaReader = NewSchemaReader(db.(*PQDatabase.Postgres)) + }) + + AfterEach(func() { + err := db.Close() + Expect(err).ShouldNot(HaveOccurred()) + }) + + Context("Head Version", func() { + It("should retrieve the most recent schema version for a tenant", func() { + ctx := context.Background() + + var mostRecentVersion string + + // Insert multiple schema versions for a single tenant + for i := 0; i < 3; i++ { + version := xid.New().String() + schema := []storage.SchemaDefinition{ + {TenantID: "t1", Name: "organization", SerializedDefinition: []byte("entity organization {}"), Version: version}, + {TenantID: "t1", Name: "user", SerializedDefinition: []byte("entity user {}"), Version: version}, + } + err := schemaWriter.WriteSchema(ctx, schema) + Expect(err).ShouldNot(HaveOccurred()) + mostRecentVersion = version // Keep track of the last inserted version + // Sleep to ensure the version is different (if versions are time-based) + time.Sleep(time.Millisecond * 2) + } + + // Attempt to retrieve the head version from SchemaReader + headVersion, err := schemaReader.HeadVersion(ctx, "t1") + Expect(err).ShouldNot(HaveOccurred()) + + // Validate that the retrieved head version matches the most recently inserted version + Expect(headVersion).Should(Equal(mostRecentVersion), "The retrieved head version should be the most recently written one.") + }) + }) + + Context("Read Schema", func() { + It("should write and then read the schema for a tenant", func() { + ctx := context.Background() + + version := xid.New().String() + + schema := []storage.SchemaDefinition{ + {TenantID: "t1", Name: "user", SerializedDefinition: []byte("entity user {}"), Version: version}, + {TenantID: "t1", Name: "organization", SerializedDefinition: []byte("entity organization { relation admin @user}"), Version: version}, + } + + err := schemaWriter.WriteSchema(ctx, schema) + Expect(err).ShouldNot(HaveOccurred()) + + sch, err := schemaReader.ReadSchema(ctx, "t1", version) + Expect(err).ShouldNot(HaveOccurred()) + + Expect(sch.EntityDefinitions["user"]).Should(Equal(&base.EntityDefinition{ + Name: "user", + Relations: map[string]*base.RelationDefinition{}, + Permissions: map[string]*base.PermissionDefinition{}, + Attributes: map[string]*base.AttributeDefinition{}, + References: map[string]base.EntityDefinition_Reference{}, + })) + + Expect(sch.EntityDefinitions["organization"]).Should(Equal(&base.EntityDefinition{ + Name: "organization", + Relations: map[string]*base.RelationDefinition{ + "admin": { + Name: "admin", + RelationReferences: []*base.RelationReference{ + { + Type: "user", + Relation: "", + }, + }, + }, + }, + Permissions: map[string]*base.PermissionDefinition{}, + Attributes: map[string]*base.AttributeDefinition{}, + References: map[string]base.EntityDefinition_Reference{ + "admin": base.EntityDefinition_REFERENCE_RELATION, + }, + }, + )) + + Expect(sch.RuleDefinitions).Should(Equal(map[string]*base.RuleDefinition{})) + + Expect(sch.References["user"]).Should(Equal(base.SchemaDefinition_REFERENCE_ENTITY)) + Expect(sch.References["organization"]).Should(Equal(base.SchemaDefinition_REFERENCE_ENTITY)) + }) + }) + + Context("Read Entity Definition", func() { + It("should write and then read the entity definition for a tenant", func() { + ctx := context.Background() + + version := xid.New().String() + + schema := []storage.SchemaDefinition{ + {TenantID: "t1", Name: "user", SerializedDefinition: []byte("entity user {}"), Version: version}, + {TenantID: "t1", Name: "organization", SerializedDefinition: []byte("entity organization { relation admin @user}"), Version: version}, + } + + err := schemaWriter.WriteSchema(ctx, schema) + Expect(err).ShouldNot(HaveOccurred()) + + en, v, err := schemaReader.ReadEntityDefinition(ctx, "t1", "organization", version) + Expect(err).ShouldNot(HaveOccurred()) + + Expect(version).Should(Equal(v)) + + Expect(en).Should(Equal(&base.EntityDefinition{ + Name: "organization", + Relations: map[string]*base.RelationDefinition{ + "admin": { + Name: "admin", + RelationReferences: []*base.RelationReference{ + { + Type: "user", + Relation: "", + }, + }, + }, + }, + Permissions: map[string]*base.PermissionDefinition{}, + Attributes: map[string]*base.AttributeDefinition{}, + References: map[string]base.EntityDefinition_Reference{ + "admin": base.EntityDefinition_REFERENCE_RELATION, + }, + }, + )) + }) + }) + + Context("Read Rule Definition", func() { + It("should write and then read the rule definition for a tenant", func() { + ctx := context.Background() + + version := xid.New().String() + + schema := []storage.SchemaDefinition{ + {TenantID: "t1", Name: "user", SerializedDefinition: []byte("entity user {}"), Version: version}, + {TenantID: "t1", Name: "organization", SerializedDefinition: []byte("entity organization { relation admin @user}"), Version: version}, + {TenantID: "t1", Name: "check_ip_range", SerializedDefinition: []byte("rule check_ip_range(ip_address string, ip_range string[]) {\n ip_address in ip_range\n}"), Version: version}, + } + + err := schemaWriter.WriteSchema(ctx, schema) + Expect(err).ShouldNot(HaveOccurred()) + + ru, v, err := schemaReader.ReadRuleDefinition(ctx, "t1", "check_ip_range", version) + Expect(err).ShouldNot(HaveOccurred()) + Expect(version).Should(Equal(v)) + Expect(ru.Name).Should(Equal("check_ip_range")) + Expect(ru.Arguments).Should(Equal(map[string]base.AttributeType{ + "ip_address": base.AttributeType_ATTRIBUTE_TYPE_STRING, + "ip_range": base.AttributeType_ATTRIBUTE_TYPE_STRING_ARRAY, + })) + }) + }) +}) diff --git a/internal/storage/postgres/schemaWriter_test.go b/internal/storage/postgres/schemaWriter_test.go new file mode 100644 index 000000000..60e48b9d3 --- /dev/null +++ b/internal/storage/postgres/schemaWriter_test.go @@ -0,0 +1,92 @@ +package postgres + +import ( + "context" + "os" + + "github.com/rs/xid" + + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" + + "github.com/Permify/permify/internal/storage" + "github.com/Permify/permify/pkg/database" + PQDatabase "github.com/Permify/permify/pkg/database/postgres" + base "github.com/Permify/permify/pkg/pb/base/v1" +) + +var _ = Describe("SchemaReader", func() { + var db database.Database + var schemaWriter *SchemaWriter + var schemaReader *SchemaReader + + BeforeEach(func() { + version := os.Getenv("POSTGRES_VERSION") + + if version == "" { + version = "14" + } + + db = postgresDB(version) + schemaWriter = NewSchemaWriter(db.(*PQDatabase.Postgres)) + schemaReader = NewSchemaReader(db.(*PQDatabase.Postgres)) + }) + + AfterEach(func() { + err := db.Close() + Expect(err).ShouldNot(HaveOccurred()) + }) + + Context("Write Schema", func() { + It("should write schema for a tenant", func() { + ctx := context.Background() + + version := xid.New().String() + + schema := []storage.SchemaDefinition{ + {TenantID: "t1", Name: "user", SerializedDefinition: []byte("entity user {}"), Version: version}, + {TenantID: "t1", Name: "organization", SerializedDefinition: []byte("entity organization { relation admin @user}"), Version: version}, + } + + err := schemaWriter.WriteSchema(ctx, schema) + Expect(err).ShouldNot(HaveOccurred()) + + sch, err := schemaReader.ReadSchema(ctx, "t1", version) + Expect(err).ShouldNot(HaveOccurred()) + + Expect(sch.EntityDefinitions["user"]).Should(Equal(&base.EntityDefinition{ + Name: "user", + Relations: map[string]*base.RelationDefinition{}, + Permissions: map[string]*base.PermissionDefinition{}, + Attributes: map[string]*base.AttributeDefinition{}, + References: map[string]base.EntityDefinition_Reference{}, + })) + + Expect(sch.EntityDefinitions["organization"]).Should(Equal(&base.EntityDefinition{ + Name: "organization", + Relations: map[string]*base.RelationDefinition{ + "admin": { + Name: "admin", + RelationReferences: []*base.RelationReference{ + { + Type: "user", + Relation: "", + }, + }, + }, + }, + Permissions: map[string]*base.PermissionDefinition{}, + Attributes: map[string]*base.AttributeDefinition{}, + References: map[string]base.EntityDefinition_Reference{ + "admin": base.EntityDefinition_REFERENCE_RELATION, + }, + }, + )) + + Expect(sch.RuleDefinitions).Should(Equal(map[string]*base.RuleDefinition{})) + + Expect(sch.References["user"]).Should(Equal(base.SchemaDefinition_REFERENCE_ENTITY)) + Expect(sch.References["organization"]).Should(Equal(base.SchemaDefinition_REFERENCE_ENTITY)) + }) + }) +}) diff --git a/internal/storage/postgres/tenantReader_test.go b/internal/storage/postgres/tenantReader_test.go new file mode 100644 index 000000000..d4de10534 --- /dev/null +++ b/internal/storage/postgres/tenantReader_test.go @@ -0,0 +1,68 @@ +package postgres + +import ( + "context" + "os" + + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" + + "github.com/Permify/permify/pkg/database" + PQDatabase "github.com/Permify/permify/pkg/database/postgres" +) + +var _ = Describe("TenantWriter", func() { + var db database.Database + var tenantWriter *TenantWriter + var tenantReader *TenantReader + + BeforeEach(func() { + version := os.Getenv("POSTGRES_VERSION") + + if version == "" { + version = "14" + } + + db = postgresDB(version) + tenantWriter = NewTenantWriter(db.(*PQDatabase.Postgres)) + tenantReader = NewTenantReader(db.(*PQDatabase.Postgres)) + }) + + AfterEach(func() { + err := db.Close() + Expect(err).ShouldNot(HaveOccurred()) + }) + + Context("List Tenants", func() { + It("should get tenants", func() { + ctx := context.Background() + + _, err := tenantWriter.CreateTenant(ctx, "test_id_1", "test name 1") + Expect(err).ShouldNot(HaveOccurred()) + + _, err = tenantWriter.CreateTenant(ctx, "test_id_2", "test name 2") + Expect(err).ShouldNot(HaveOccurred()) + + _, err = tenantWriter.CreateTenant(ctx, "test_id_3", "test name 3") + Expect(err).ShouldNot(HaveOccurred()) + + _, err = tenantWriter.CreateTenant(ctx, "test_id_4", "test name 4") + Expect(err).ShouldNot(HaveOccurred()) + + _, err = tenantWriter.CreateTenant(ctx, "test_id_5", "test name 5") + Expect(err).ShouldNot(HaveOccurred()) + + _, err = tenantWriter.CreateTenant(ctx, "test_id_6", "test name 6") + Expect(err).ShouldNot(HaveOccurred()) + + col1, ct1, err := tenantReader.ListTenants(ctx, database.NewPagination(database.Size(3), database.Token(""))) + Expect(err).ShouldNot(HaveOccurred()) + Expect(len(col1)).Should(Equal(3)) + + col2, ct2, err := tenantReader.ListTenants(ctx, database.NewPagination(database.Size(4), database.Token(ct1.String()))) + Expect(err).ShouldNot(HaveOccurred()) + Expect(len(col2)).Should(Equal(4)) + Expect(ct2.String()).Should(Equal("")) + }) + }) +}) diff --git a/internal/storage/postgres/tenantWriter_test.go b/internal/storage/postgres/tenantWriter_test.go new file mode 100644 index 000000000..e3f8af5c7 --- /dev/null +++ b/internal/storage/postgres/tenantWriter_test.go @@ -0,0 +1,74 @@ +package postgres + +import ( + "context" + "os" + + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" + + "github.com/Permify/permify/pkg/database" + PQDatabase "github.com/Permify/permify/pkg/database/postgres" + base "github.com/Permify/permify/pkg/pb/base/v1" +) + +var _ = Describe("TenantWriter", func() { + var db database.Database + var tenantWriter *TenantWriter + + BeforeEach(func() { + version := os.Getenv("POSTGRES_VERSION") + + if version == "" { + version = "14" + } + + db = postgresDB(version) + tenantWriter = NewTenantWriter(db.(*PQDatabase.Postgres)) + }) + + AfterEach(func() { + err := db.Close() + Expect(err).ShouldNot(HaveOccurred()) + }) + + Context("Create Tenant", func() { + It("should create tenant", func() { + ctx := context.Background() + + tenant, err := tenantWriter.CreateTenant(ctx, "test_id_1", "test name 1") + Expect(err).ShouldNot(HaveOccurred()) + + Expect(tenant.Id).Should(Equal("test_id_1")) + Expect(tenant.Name).Should(Equal("test name 1")) + }) + + It("should get unique error", func() { + ctx := context.Background() + + _, err := tenantWriter.CreateTenant(ctx, "test_id_1", "test name 1") + Expect(err).ShouldNot(HaveOccurred()) + + _, err = tenantWriter.CreateTenant(ctx, "test_id_1", "test name 1") + Expect(err.Error()).Should(Equal(base.ErrorCode_ERROR_CODE_UNIQUE_CONSTRAINT.String())) + }) + }) + + Context("Delete Tenant", func() { + It("should delete tenant", func() { + ctx := context.Background() + + tenant, err := tenantWriter.CreateTenant(ctx, "test_id_1", "test name 1") + Expect(err).ShouldNot(HaveOccurred()) + + Expect(tenant.Id).Should(Equal("test_id_1")) + Expect(tenant.Name).Should(Equal("test name 1")) + + tenant, err = tenantWriter.DeleteTenant(ctx, "test_id_1") + Expect(err).ShouldNot(HaveOccurred()) + + Expect(tenant.Id).Should(Equal("test_id_1")) + Expect(tenant.Name).Should(Equal("test name 1")) + }) + }) +}) diff --git a/internal/storage/postgres/tests/dataReader_test.go b/internal/storage/postgres/tests/dataReader_test.go deleted file mode 100644 index f56abbdd6..000000000 --- a/internal/storage/postgres/tests/dataReader_test.go +++ /dev/null @@ -1,100 +0,0 @@ -package tests - -import ( - "context" - "database/sql" - "regexp" - - "github.com/DATA-DOG/go-sqlmock" - "github.com/Masterminds/squirrel" - "github.com/jackc/pgtype" - . "github.com/onsi/ginkgo/v2" - . "github.com/onsi/gomega" - - postgres2 "github.com/Permify/permify/internal/storage/postgres" - "github.com/Permify/permify/internal/storage/postgres/snapshot" - "github.com/Permify/permify/internal/storage/postgres/types" - "github.com/Permify/permify/pkg/database" - "github.com/Permify/permify/pkg/database/postgres" - base "github.com/Permify/permify/pkg/pb/base/v1" -) - -var _ = Describe("RelationshipReader", func() { - var dataReader *postgres2.DataReader - var mock sqlmock.Sqlmock - - BeforeEach(func() { - var db *sql.DB - var err error - - db, mock, err = sqlmock.New() - Expect(err).ShouldNot(HaveOccurred()) - - pg := &postgres.Postgres{ - DB: db, - Builder: squirrel.StatementBuilder.PlaceholderFormat(squirrel.Dollar), - } - - dataReader = postgres2.NewDataReader(pg) - }) - - AfterEach(func() { - err := mock.ExpectationsWereMet() - Expect(err).ShouldNot(HaveOccurred()) - }) - - Context("QueryRelationships", func() { - columns := []string{"entity_type", "entity_id", "relation", "subject_type", "subject_id", "subject_relation"} - - It("should be same queries", func() { - rows := sqlmock.NewRows(columns). - AddRow("organization", "abc", "admin", "user", "jack", ""). - AddRow("organization", "abc", "admin", "user", "john", "") - - mock.ExpectBegin() - mock.ExpectQuery(regexp.QuoteMeta(`SELECT entity_type, entity_id, relation, subject_type, subject_id, subject_relation - FROM relation_tuples WHERE tenant_id = $1 AND entity_id IN ($2) AND entity_type = $3 AND relation = $4 AND (pg_visible_in_snapshot(created_tx_id, - (select snapshot from transactions where id = '4'::xid8)) = true OR created_tx_id = '4'::xid8) AND ((pg_visible_in_snapshot(expired_tx_id, - (select snapshot from transactions where id = '4'::xid8)) = false OR expired_tx_id = '0'::xid8) AND expired_tx_id <> '4'::xid8)`)). - WithArgs("noop", "abc", "organization", "admin"). - WillReturnRows(rows) - mock.ExpectCommit() - - value, err := dataReader.QueryRelationships(context.Background(), "noop", &base.TupleFilter{ - Entity: &base.EntityFilter{ - Type: "organization", - Ids: []string{"abc"}, - }, - Relation: "admin", - }, snapshot.NewToken(types.XID8{Uint: 4, Status: pgtype.Present}).Encode().String()) - - Expect(err).ShouldNot(HaveOccurred()) - Expect(value).Should(Equal(database.NewTupleIterator([]*base.Tuple{ - { - Entity: &base.Entity{ - Type: "organization", - Id: "abc", - }, - Relation: "admin", - Subject: &base.Subject{ - Type: "user", - Id: "jack", - Relation: "", - }, - }, - { - Entity: &base.Entity{ - Type: "organization", - Id: "abc", - }, - Relation: "admin", - Subject: &base.Subject{ - Type: "user", - Id: "john", - Relation: "", - }, - }, - }...))) - }) - }) -}) diff --git a/internal/storage/postgres/tests/dataWriter_test.go b/internal/storage/postgres/tests/dataWriter_test.go deleted file mode 100644 index b1db7dc6d..000000000 --- a/internal/storage/postgres/tests/dataWriter_test.go +++ /dev/null @@ -1,97 +0,0 @@ -//go:build !integration - -package tests - -import ( - "context" - "database/sql" - "regexp" - - "github.com/DATA-DOG/go-sqlmock" - "github.com/Masterminds/squirrel" - . "github.com/onsi/ginkgo/v2" - . "github.com/onsi/gomega" - - postgres2 "github.com/Permify/permify/internal/storage/postgres" - "github.com/Permify/permify/pkg/database" - "github.com/Permify/permify/pkg/database/postgres" - basev1 "github.com/Permify/permify/pkg/pb/base/v1" -) - -var _ = Describe("RelationshipWriter", func() { - var dataWriter *postgres2.DataWriter - var mock sqlmock.Sqlmock - - BeforeEach(func() { - var db *sql.DB - var err error - - db, mock, err = sqlmock.New() - Expect(err).ShouldNot(HaveOccurred()) - - pg := &postgres.Postgres{ - DB: db, - Builder: squirrel.StatementBuilder.PlaceholderFormat(squirrel.Dollar), - } - - dataWriter = postgres2.NewDataWriter(pg) - }) - - AfterEach(func() { - err := mock.ExpectationsWereMet() - Expect(err).ShouldNot(HaveOccurred()) - }) - - Context("Writes Relationships", func() { - columns := []string{"entity_type", "entity_id", "relation", "subject_type", "subject_id", "subject_relation", "tenant_id"} - - It("Insert and throws no error", func() { - mock.ExpectBegin() - mock.ExpectQuery(regexp.QuoteMeta(`INSERT INTO relation_tuples (entity_type, entity_id, relation, subject_type, subject_id, subject_relation, tenant_id) - VALUES ($1,$2,$3,$4,$5,$6,$7) ON CONFLICT DO NOTHING`)). - WithArgs("organization", "abc", "admin", "subject-1", "sub-id", "admin", "noop"). - WillReturnRows( - sqlmock.NewRows(columns).AddRow("organization", "abc", "admin", "subject-1", "sub-id", "admin", "noop"), - ) - mock.ExpectCommit() - tp := &database.TupleCollection{} - tp.Add(&basev1.Tuple{ - Entity: &basev1.Entity{Type: "organization", Id: "abc"}, - Relation: "admin", - Subject: &basev1.Subject{Type: "subject-1", Id: "sub-id", Relation: "admin-sub"}, - }) - _, err := dataWriter.Write(context.Background(), "noop", tp, &database.AttributeCollection{}) - - Expect(err).ShouldNot(HaveOccurred()) - }) - - It("Insert and compares", func() { - mock.ExpectBegin() - mock.ExpectQuery(regexp.QuoteMeta(`INSERT INTO relation_tuples (entity_type, entity_id, relation, subject_type, subject_id, subject_relation, tenant_id) - VALUES ($1,$2,$3,$4,$5,$6,$7)`)). - WithArgs("organization", "abc", "admin", "subject-1", "sub-id", "admin-sub", "noop"). - WillReturnRows( - sqlmock.NewRows(columns).AddRow("organization", "abc", "admin", "subject-1", "sub-id", "admin-sub", "noop"), - ) - mock.ExpectCommit() - tp := &database.TupleCollection{} - tp.Add(&basev1.Tuple{ - Entity: &basev1.Entity{ - Type: "organization", - Id: "abc", - }, - Subject: &basev1.Subject{ - Type: "subject-1", - Id: "sub-id", - Relation: "admin-sub", - }, - Relation: "admin", - }) - _, err := dataWriter.Write(context.Background(), "noop", tp, &database.AttributeCollection{}) - - Expect(err).ShouldNot(HaveOccurred()) - - // TODO: can we write a helper function to fetch the recently inserted record? as we are just creating a mock! any comments? Will think about it! - }) - }) -}) diff --git a/internal/storage/postgres/tests/postgres_test.go b/internal/storage/postgres/tests/postgres_test.go deleted file mode 100644 index ca73ac46c..000000000 --- a/internal/storage/postgres/tests/postgres_test.go +++ /dev/null @@ -1,93 +0,0 @@ -//go:build integration - -package tests - -import ( - "context" - "fmt" - "os" - "testing" - "time" - - "github.com/testcontainers/testcontainers-go" - "github.com/testcontainers/testcontainers-go/wait" - - "github.com/Permify/permify/internal/config" -) - -var ( - postgres testcontainers.Container - postgresDSN string - cfg config.Database -) - -func TestMain(m *testing.M) { - postgresDSN = GetPostgres() - - // Give the PostgreSQL container some time to start up - time.Sleep(5 * time.Second) - - exitCode := m.Run() - - TeardownPostgreSQL() - - os.Exit(exitCode) -} - -func GetPostgres() string { - ctx := context.Background() - req := testcontainers.ContainerRequest{ - Image: "postgres:14-alpine", - ExposedPorts: []string{"5432/tcp"}, - WaitingFor: wait.ForLog("database system is ready to accept connections"), - Env: map[string]string{"POSTGRES_USER": "postgres", "POSTGRES_PASSWORD": "postgres", "POSTGRES_DB": "permify"}, - } - - postgres, err := testcontainers.GenericContainer(ctx, testcontainers.GenericContainerRequest{ - ContainerRequest: req, - Started: true, - }) - if err != nil { - fmt.Println("Error starting PostgreSQL container:", err) - os.Exit(1) - } - - cmd := []string{"sh", "-c", "export PGPASSWORD=postgres" + "; psql -U postgres -d permify -c 'DROP SCHEMA public CASCADE; CREATE SCHEMA public;'"} - - _, _, err = postgres.Exec(context.Background(), cmd) - if err != nil { - fmt.Println("Error resetting PostgreSQL schema:", err) - os.Exit(1) - } - - host, err := postgres.Host(ctx) - if err != nil { - fmt.Println("Error getting PostgreSQL container host:", err) - os.Exit(1) - } - - port, err := postgres.MappedPort(ctx, "5432") - if err != nil { - fmt.Println("Error getting PostgreSQL container port:", err) - os.Exit(1) - } - dbAddr := fmt.Sprintf("%s:%s", host, port.Port()) - postgresDSN = fmt.Sprintf("postgres://%s:%s@%s/%s?sslmode=disable", "postgres", "postgres", dbAddr, "permify") - cfg = config.Database{ - Engine: "postgres", - URI: postgresDSN, - AutoMigrate: true, - MaxOpenConnections: 20, - MaxIdleConnections: 1, - MaxConnectionLifetime: 300, - MaxConnectionIdleTime: 60, - } - - return postgresDSN -} - -func TeardownPostgreSQL() { - if postgres != nil { - postgres.Terminate(context.Background()) - } -} diff --git a/internal/storage/postgres/tests/schemaReader_integration_test.go b/internal/storage/postgres/tests/schemaReader_integration_test.go deleted file mode 100644 index 32ccd9693..000000000 --- a/internal/storage/postgres/tests/schemaReader_integration_test.go +++ /dev/null @@ -1,54 +0,0 @@ -//go:build integration - -package tests - -import ( - "context" - "testing" - - "github.com/rs/xid" - "github.com/stretchr/testify/require" - - "github.com/Permify/permify/internal/storage" - "github.com/Permify/permify/internal/storage/postgres" - "github.com/Permify/permify/pkg/database" - PQDatabase "github.com/Permify/permify/pkg/database/postgres" -) - -func TestSchemaReaderHeadVersion_Integration(t *testing.T) { - ctx := context.Background() - - err := storage.Migrate(cfg) - require.NoError(t, err) - - var db database.Database - db, err = PQDatabase.New(cfg.URI, - PQDatabase.MaxOpenConnections(cfg.MaxOpenConnections), - PQDatabase.MaxIdleConnections(cfg.MaxIdleConnections), - PQDatabase.MaxConnectionIdleTime(cfg.MaxConnectionIdleTime), - PQDatabase.MaxConnectionLifeTime(cfg.MaxConnectionLifetime), - ) - require.NoError(t, err) - - if err != nil { - t.Fatal(err) - } - defer db.Close() - - // Create a TenantWriter instance - schemaWriter := postgres.NewSchemaWriter(db.(*PQDatabase.Postgres)) - schemaReader := postgres.NewSchemaReader(db.(*PQDatabase.Postgres)) - - v := xid.New().String() - schemas := []storage.SchemaDefinition{ - {TenantID: "t1", EntityType: "entity2", SerializedDefinition: []byte("def2"), Version: v}, - } - - // Test the CreateTenant method - err = schemaWriter.WriteSchema(ctx, schemas) - require.NoError(t, err) - - version, err := schemaReader.HeadVersion(ctx, "t1") - require.NoError(t, err) - require.Equal(t, v, version) -} diff --git a/internal/storage/postgres/tests/schemaReader_test.go b/internal/storage/postgres/tests/schemaReader_test.go deleted file mode 100644 index d634dec0e..000000000 --- a/internal/storage/postgres/tests/schemaReader_test.go +++ /dev/null @@ -1,148 +0,0 @@ -package tests - -import ( - "context" - "testing" - - "github.com/DATA-DOG/go-sqlmock" - "github.com/Masterminds/squirrel" - - "github.com/Permify/permify/internal/storage" - "github.com/Permify/permify/internal/storage/postgres" - - "github.com/rs/xid" - "github.com/stretchr/testify/require" - - PQRepository "github.com/Permify/permify/pkg/database/postgres" -) - -var schemaExample = "entity user {}\\n\\nentity organization {\\n\\n // relations\\n relation admin @user\\n relation member @user\\n\\n // actions\\n action create_repository = (admin or member)\\n action delete = admin\\n}\\n\\nentity repository {\\n\\n // relations\\n relation owner @user @organization#member\\n relation parent @organization\\n\\n // actions\\n action push = owner\\n action read = (owner and (parent.admin and not parent.member))\\n \\n // parent.create_repository means user should be\\n // organization admin or organization member\\n action delete = (owner or (parent.create_repository))\\n}" - -func TestHeadVersion_Test(t *testing.T) { - db, mock, err := sqlmock.New() - require.NoError(t, err) - defer db.Close() - - pg := &PQRepository.Postgres{ - DB: db, - Builder: squirrel.StatementBuilder.PlaceholderFormat(squirrel.Dollar), - } - - // Create SchemaWriter - writer := postgres.NewSchemaWriter(pg) - reader := postgres.NewSchemaReader(pg) - - ctx := context.Background() - - version := xid.New().String() - schemas := map[string]storage.SchemaDefinition{ - version: {TenantID: "1", Name: "entity1", SerializedDefinition: []byte("def1"), Version: version}, - } - - writeSchemas := []storage.SchemaDefinition{ - {TenantID: "1", Name: "entity1", SerializedDefinition: []byte("def1"), Version: version}, - } - - query := "INSERT INTO schema_definitions \\(name, serialized_definition, version, tenant_id\\) VALUES \\(\\$1,\\$2,\\$3,\\$4\\)$" - mock.ExpectExec(query). - WithArgs(schemas[version].Name, schemas[version].SerializedDefinition, schemas[version].Version, schemas[version].TenantID). - WillReturnResult(sqlmock.NewResult(0, 2)) - - err = writer.WriteSchema(ctx, writeSchemas) - require.NoError(t, err) - - versionQuery := "SELECT version FROM schema_definitions WHERE tenant_id = \\$1 ORDER BY version DESC LIMIT 1$" - rows := sqlmock.NewRows([]string{"version"}).AddRow(schemas[version].Version) - mock.ExpectQuery(versionQuery).WithArgs(schemas[version].TenantID).WillReturnRows(rows) - - versionActual, err := reader.HeadVersion(ctx, "1") - require.NoError(t, err) - require.Equal(t, version, versionActual) -} - -func TestReadSchema_Test(t *testing.T) { - db, mock, err := sqlmock.New() - require.NoError(t, err) - defer db.Close() - - pg := &PQRepository.Postgres{ - DB: db, - Builder: squirrel.StatementBuilder.PlaceholderFormat(squirrel.Dollar), - } - - // Create SchemaWriter - writer := postgres.NewSchemaWriter(pg) - reader := postgres.NewSchemaReader(pg) - - ctx := context.Background() - - version := xid.New().String() - schemas := map[string]storage.SchemaDefinition{ - version: {TenantID: "1", Name: "user", SerializedDefinition: []byte(schemaExample), Version: version}, - } - - writeSchemas := []storage.SchemaDefinition{ - {TenantID: "1", Name: "user", SerializedDefinition: []byte(schemaExample), Version: version}, - } - - query := "INSERT INTO schema_definitions \\(name, serialized_definition, version, tenant_id\\) VALUES \\(\\$1,\\$2,\\$3,\\$4\\)$" - mock.ExpectExec(query). - WithArgs(schemas[version].Name, schemas[version].SerializedDefinition, schemas[version].Version, schemas[version].TenantID). - WillReturnResult(sqlmock.NewResult(0, 2)) - - err = writer.WriteSchema(ctx, writeSchemas) - require.NoError(t, err) - - expectedQuery := "SELECT name, serialized_definition, version FROM schema_definitions WHERE tenant_id = \\$1 AND version = \\$2" - expectedRows := sqlmock.NewRows([]string{"name", "serialized_definition", "version"}). - AddRow("user", []byte(schemaExample), version) - - mock.ExpectQuery(expectedQuery).WithArgs("1", version).WillReturnRows(expectedRows) - - _, err = reader.ReadSchema(ctx, "1", version) - require.NoError(t, err) -} - -func TestReadSchemaDefinition_Test(t *testing.T) { - db, mock, err := sqlmock.New() - require.NoError(t, err) - defer db.Close() - - pg := &PQRepository.Postgres{ - DB: db, - Builder: squirrel.StatementBuilder.PlaceholderFormat(squirrel.Dollar), - } - - // Create SchemaWriter - writer := postgres.NewSchemaWriter(pg) - reader := postgres.NewSchemaReader(pg) - - ctx := context.Background() - - version := xid.New().String() - schemas := map[string]storage.SchemaDefinition{ - version: {TenantID: "1", Name: "user", SerializedDefinition: []byte(schemaExample), Version: version}, - } - - writeSchemas := []storage.SchemaDefinition{ - {TenantID: "1", Name: "user", SerializedDefinition: []byte(schemaExample), Version: version}, - } - - query := "INSERT INTO schema_definitions \\(name, serialized_definition, version, tenant_id\\) VALUES \\(\\$1,\\$2,\\$3,\\$4\\)$" - mock.ExpectExec(query). - WithArgs(schemas[version].Name, schemas[version].SerializedDefinition, schemas[version].Version, schemas[version].TenantID). - WillReturnResult(sqlmock.NewResult(0, 2)) - - err = writer.WriteSchema(ctx, writeSchemas) - require.NoError(t, err) - - expectedQuery := "SELECT name, serialized_definition, version FROM schema_definitions WHERE name = \\$1 AND tenant_id = \\$2 AND version = \\$3 LIMIT 1" - expectedRows := sqlmock.NewRows([]string{"name", "serialized_definition", "version"}). - AddRow("user", schemaExample, version) - - mock.ExpectQuery(expectedQuery).WithArgs("user", "1", version).WillReturnRows(expectedRows) - - _, v, err := reader.ReadEntityDefinition(ctx, "1", "user", version) - require.NoError(t, err) - require.Equal(t, version, v) -} diff --git a/internal/storage/postgres/tests/schemaWriter_integration_test.go b/internal/storage/postgres/tests/schemaWriter_integration_test.go deleted file mode 100644 index ab4b7695f..000000000 --- a/internal/storage/postgres/tests/schemaWriter_integration_test.go +++ /dev/null @@ -1,47 +0,0 @@ -//go:build integration - -package tests - -import ( - "context" - "testing" - - "github.com/Permify/permify/internal/storage" - "github.com/Permify/permify/internal/storage/postgres" - - "github.com/stretchr/testify/require" - - "github.com/Permify/permify/pkg/database" - PQDatabase "github.com/Permify/permify/pkg/database/postgres" -) - -func TestSchemaWriter_Integration(t *testing.T) { - ctx := context.Background() - - err := storage.Migrate(cfg) - require.NoError(t, err) - - var db database.Database - db, err = PQDatabase.New(cfg.URI, - PQDatabase.MaxOpenConnections(cfg.MaxOpenConnections), - PQDatabase.MaxIdleConnections(cfg.MaxIdleConnections), - PQDatabase.MaxConnectionIdleTime(cfg.MaxConnectionIdleTime), - PQDatabase.MaxConnectionLifeTime(cfg.MaxConnectionLifetime), - ) - require.NoError(t, err) - - if err != nil { - t.Fatal(err) - } - defer db.Close() - - // Create a TenantWriter instance - schemaWriter := postgres.NewSchemaWriter(db.(*PQDatabase.Postgres)) - - schemas := []storage.SchemaDefinition{ - {TenantID: "t1", EntityType: "entity3", SerializedDefinition: []byte("def2"), Version: "v3"}, - } - // Test the CreateTenant method - err = schemaWriter.WriteSchema(ctx, schemas) - require.NoError(t, err) -} diff --git a/internal/storage/postgres/tests/schemaWriter_test.go b/internal/storage/postgres/tests/schemaWriter_test.go deleted file mode 100644 index f892e7ea3..000000000 --- a/internal/storage/postgres/tests/schemaWriter_test.go +++ /dev/null @@ -1,44 +0,0 @@ -package tests - -import ( - "context" - "testing" - - "github.com/DATA-DOG/go-sqlmock" - "github.com/Masterminds/squirrel" - "github.com/stretchr/testify/require" - - "github.com/Permify/permify/internal/storage" - "github.com/Permify/permify/internal/storage/postgres" - PQRepository "github.com/Permify/permify/pkg/database/postgres" -) - -func TestWriteSchema_Test(t *testing.T) { - db, mock, err := sqlmock.New() - require.NoError(t, err) - defer db.Close() - - pg := &PQRepository.Postgres{ - DB: db, - Builder: squirrel.StatementBuilder.PlaceholderFormat(squirrel.Dollar), - } - - // Create SchemaWriter - writer := postgres.NewSchemaWriter(pg) - - ctx := context.Background() - - schemas := []storage.SchemaDefinition{ - {TenantID: "1", Name: "entity1", SerializedDefinition: []byte("def1"), Version: "v1"}, - {TenantID: "2", Name: "entity2", SerializedDefinition: []byte("def2"), Version: "v2"}, - } - - query := "INSERT INTO schema_definitions \\(name, serialized_definition, version, tenant_id\\) VALUES \\(\\$1,\\$2,\\$3,\\$4\\),\\(\\$5,\\$6,\\$7,\\$8\\)$" - mock.ExpectExec(query). - WithArgs(schemas[0].Name, schemas[0].SerializedDefinition, schemas[0].Version, schemas[0].TenantID, - schemas[1].Name, schemas[1].SerializedDefinition, schemas[1].Version, schemas[1].TenantID). - WillReturnResult(sqlmock.NewResult(0, 2)) - - err = writer.WriteSchema(ctx, schemas) - require.NoError(t, err) -} diff --git a/internal/storage/postgres/tests/tenantReader_integration_test.go b/internal/storage/postgres/tests/tenantReader_integration_test.go deleted file mode 100644 index 5542ff0a2..000000000 --- a/internal/storage/postgres/tests/tenantReader_integration_test.go +++ /dev/null @@ -1,58 +0,0 @@ -//go:build integration - -package tests - -import ( - "context" - "testing" - - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" - - "github.com/Permify/permify/internal/storage" - "github.com/Permify/permify/internal/storage/postgres" - "github.com/Permify/permify/pkg/database" - PQDatabase "github.com/Permify/permify/pkg/database/postgres" -) - -func TestTenantReader_Integration(t *testing.T) { - ctx := context.Background() - - err := storage.Migrate(cfg) - require.NoError(t, err) - - var db database.Database - db, err = PQDatabase.New(cfg.URI, - PQDatabase.MaxOpenConnections(cfg.MaxOpenConnections), - PQDatabase.MaxIdleConnections(cfg.MaxIdleConnections), - PQDatabase.MaxConnectionIdleTime(cfg.MaxConnectionIdleTime), - PQDatabase.MaxConnectionLifeTime(cfg.MaxConnectionLifetime), - ) - require.NoError(t, err) - - if err != nil { - t.Fatal(err) - } - defer db.Close() - - // Create a Tenant instances - tenantWriter := postgres.NewTenantWriter(db.(*PQDatabase.Postgres)) - tenantReader := postgres.NewTenantReader(db.(*PQDatabase.Postgres)) - - // Test the CreateTenant method - createdTenant, err := tenantWriter.CreateTenant(ctx, "2", "Test Tenant") - require.NoError(t, err) - assert.Equal(t, "2", createdTenant.Id) - assert.Equal(t, "Test Tenant", createdTenant.Name) - - pagination := database.NewPagination() - - // Test the DeleteTenant method - listTenant, _, err := tenantReader.ListTenants(ctx, pagination) - - require.NoError(t, err) - assert.Equal(t, "t1", listTenant[1].Id) - assert.Equal(t, "example tenant", listTenant[1].Name) - assert.Equal(t, "2", listTenant[0].Id) - assert.Equal(t, "Test Tenant", listTenant[0].Name) -} diff --git a/internal/storage/postgres/tests/tenantReader_test.go b/internal/storage/postgres/tests/tenantReader_test.go deleted file mode 100644 index c6cd81d34..000000000 --- a/internal/storage/postgres/tests/tenantReader_test.go +++ /dev/null @@ -1,44 +0,0 @@ -package tests - -import ( - "context" - "testing" - "time" - - "github.com/Masterminds/squirrel" - - "github.com/Permify/permify/internal/storage/postgres" - pg "github.com/Permify/permify/pkg/database/postgres" - - "github.com/DATA-DOG/go-sqlmock" - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" -) - -func TestTenantReader_ListTenants(t *testing.T) { - db, mock, err := sqlmock.New() - require.NoError(t, err) - defer db.Close() - - pg := &pg.Postgres{ - DB: db, - Builder: squirrel.StatementBuilder.PlaceholderFormat(squirrel.Dollar), - } - - writer := postgres.NewTenantWriter(pg) - // reader := NewTenantReader(pg, log) - ctx := context.Background() - - id := "2" - name := "tenant_1" - createdAt := time.Now() - - mock.ExpectQuery("INSERT INTO tenants \\(id, name\\) VALUES \\(\\$1,\\$2\\) RETURNING created_at").WithArgs(id, name). - WillReturnRows(sqlmock.NewRows([]string{"created_at"}).AddRow(createdAt)) - - tenant, err := writer.CreateTenant(ctx, id, name) - require.NoError(t, err) - assert.NotNil(t, tenant) - assert.Equal(t, id, tenant.Id) - assert.Equal(t, name, tenant.Name) -} diff --git a/internal/storage/postgres/tests/tenantWriter_integration_test.go b/internal/storage/postgres/tests/tenantWriter_integration_test.go deleted file mode 100644 index d829e7c8a..000000000 --- a/internal/storage/postgres/tests/tenantWriter_integration_test.go +++ /dev/null @@ -1,49 +0,0 @@ -//go:build integration - -package tests - -import ( - "context" - "testing" - - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" - - "github.com/Permify/permify/internal/storage" - "github.com/Permify/permify/internal/storage/postgres" - "github.com/Permify/permify/pkg/database" - PQDatabase "github.com/Permify/permify/pkg/database/postgres" -) - -func TestTenantWriter(t *testing.T) { - ctx := context.Background() - - err := storage.Migrate(cfg) - require.NoError(t, err) - - var db database.Database - db, err = PQDatabase.New(cfg.URI, - PQDatabase.MaxOpenConnections(cfg.MaxOpenConnections), - PQDatabase.MaxIdleConnections(cfg.MaxIdleConnections), - PQDatabase.MaxConnectionIdleTime(cfg.MaxConnectionIdleTime), - PQDatabase.MaxConnectionLifeTime(cfg.MaxConnectionLifetime), - ) - require.NoError(t, err) - - defer db.Close() - - // Create a TenantWriter instance - tenantWriter := postgres.NewTenantWriter(db.(*PQDatabase.Postgres)) - - // Test the CreateTenant method - createdTenant, err := tenantWriter.CreateTenant(ctx, "4", "Test Tenant") - require.NoError(t, err) - assert.Equal(t, "4", createdTenant.Id) - assert.Equal(t, "Test Tenant", createdTenant.Name) - - // Test the DeleteTenant method - deletedTenant, err := tenantWriter.DeleteTenant(ctx, "4") - require.NoError(t, err) - assert.Equal(t, "4", deletedTenant.Id) - assert.Equal(t, "Test Tenant", deletedTenant.Name) -} diff --git a/internal/storage/postgres/tests/tenantWriter_test.go b/internal/storage/postgres/tests/tenantWriter_test.go deleted file mode 100644 index 91e781a94..000000000 --- a/internal/storage/postgres/tests/tenantWriter_test.go +++ /dev/null @@ -1,82 +0,0 @@ -package tests - -import ( - "context" - "testing" - "time" - - "github.com/Masterminds/squirrel" - - "github.com/Permify/permify/internal/storage/postgres" - PQRepository "github.com/Permify/permify/pkg/database/postgres" - - "github.com/DATA-DOG/go-sqlmock" - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" -) - -func TestTenantWriter_CreateTenant(t *testing.T) { - db, mock, err := sqlmock.New() - require.NoError(t, err) - defer db.Close() - - pg := &PQRepository.Postgres{ - DB: db, - Builder: squirrel.StatementBuilder.PlaceholderFormat(squirrel.Dollar), - } - - // Create TenantWriter - writer := postgres.NewTenantWriter(pg) - - ctx := context.Background() - - // Create Fields for CreateTenant - id := "2" - name := "tenant_1" - createdAt := time.Now() - - // SQL sorgusunu bekleyen mock.ExpectQuery oluştur - mock.ExpectQuery("INSERT INTO tenants \\(id, name\\) VALUES \\(\\$1,\\$2\\) RETURNING created_at").WithArgs(id, name). - WillReturnRows(sqlmock.NewRows([]string{"created_at"}).AddRow(createdAt)) - - tenant, err := writer.CreateTenant(ctx, id, name) - require.NoError(t, err) - assert.NotNil(t, tenant) - assert.Equal(t, id, tenant.Id) - assert.Equal(t, name, tenant.Name) -} - -func TestTenantWriter_DeleteTenant(t *testing.T) { - db, mock, err := sqlmock.New() - require.NoError(t, err) - defer db.Close() - - pg := &PQRepository.Postgres{ - DB: db, - Builder: squirrel.StatementBuilder.PlaceholderFormat(squirrel.Dollar), - } - - // Create TenantWriter - writer := postgres.NewTenantWriter(pg) - - ctx := context.Background() - - // Create Fields for CreateTenant - id := "2" - name := "tenant_1" - createdAt := time.Now() - - // SQL sorgusunu bekleyen mock.ExpectQuery oluştur - mock.ExpectQuery("INSERT INTO tenants \\(id, name\\) VALUES \\(\\$1,\\$2\\) RETURNING created_at").WithArgs(id, name). - WillReturnRows(sqlmock.NewRows([]string{"created_at"}).AddRow(createdAt)) - - tenant, err := writer.CreateTenant(ctx, id, name) - - mock.ExpectQuery("DELETE FROM tenants WHERE id = \\$1 RETURNING name, created_at").WithArgs(tenant.Id). - WillReturnRows(sqlmock.NewRows([]string{"name", "created_at"}).AddRow(tenant.Name, tenant.CreatedAt.AsTime())) - - deletedTenant, err := writer.DeleteTenant(ctx, tenant.Id) - require.NoError(t, err) - assert.NotNil(t, deletedTenant) - assert.Equal(t, id, deletedTenant.Id) -} From 553c1cc98e7cecea45fcb2e245c839c63a47c5d3 Mon Sep 17 00:00:00 2001 From: Tolga Ozen Date: Wed, 1 Nov 2023 21:30:17 +0300 Subject: [PATCH 2/3] feat(#788): enforce non-nil requirement for exactly one of tuple_filter or attribute_filter --- internal/validation/validation.go | 80 +++++- internal/validation/validation_test.go | 93 ++++++- pkg/pb/base/v1/base.pb.go | 321 ++++++++++++------------- pkg/pb/base/v1/base.pb.validate.go | 22 -- proto/base/v1/base.proto | 6 +- 5 files changed, 329 insertions(+), 193 deletions(-) diff --git a/internal/validation/validation.go b/internal/validation/validation.go index a60eeb811..c08bc95e9 100644 --- a/internal/validation/validation.go +++ b/internal/validation/validation.go @@ -48,13 +48,27 @@ func ValidateTuple(definition *base.EntityDefinition, tup *base.Tuple) (err erro } // ValidateTupleFilter checks if the provided filter conforms to the entity definition -func ValidateTupleFilter(filter *base.TupleFilter) (err error) { - if filter.GetEntity().GetType() == "" { - return errors.New(base.ErrorCode_ERROR_CODE_ENTITY_TYPE_REQUIRED.String()) +func ValidateTupleFilter(tupleFilter *base.TupleFilter) (err error) { + if IsTupleFilterEmpty(tupleFilter) { + return errors.New(base.ErrorCode_ERROR_CODE_VALIDATION.String()) } return nil } +// ValidateFilters checks if both provided filters, tupleFilter and attributeFilter, are empty. +// It returns an error if both filters are empty, as at least one filter should contain criteria for the operation to be valid. +func ValidateFilters(tupleFilter *base.TupleFilter, attributeFilter *base.AttributeFilter) (err error) { + // Check if both tupleFilter and attributeFilter are empty using the respective functions. + // If both are empty, then there is nothing to validate, and an error is returned. + if IsTupleFilterEmpty(tupleFilter) && IsAttributeFilterEmpty(attributeFilter) { + // The error returned indicates a validation error due to both filters being empty. + return errors.New(base.ErrorCode_ERROR_CODE_VALIDATION.String()) + } + + // If at least one of the filters is not empty, then the validation is successful, and no error is returned. + return nil +} + // ValidateAttribute checks whether a given attribute request (reqAttribute) aligns with // the attribute definition in a given entity definition. It verifies if the attribute exists // in the entity definition and if the attribute type matches the type specified in the request. @@ -91,3 +105,63 @@ func ValidateAttribute(definition *base.EntityDefinition, reqAttribute *base.Att // If all checks pass without returning, the attribute is considered valid and the function returns nil. return nil } + +// IsTupleFilterEmpty checks whether any of the fields in a TupleFilter are filled. +// It assumes that a filter is "empty" if all its fields are unset or have zero values. +func IsTupleFilterEmpty(filter *base.TupleFilter) bool { + // If the entity type is set, the filter is not empty. + if filter.GetEntity().GetType() != "" { + return false // Entity type is present, therefore filter is not empty. + } + + // If the entity IDs slice is not empty, the filter is not empty. + if len(filter.GetEntity().GetIds()) > 0 { + return false // Entity IDs are present, therefore filter is not empty. + } + + // If the relation is set, the filter is not empty. + if filter.GetRelation() != "" { + return false // Relation is present, therefore filter is not empty. + } + + // If the subject type is set, the filter is not empty. + if filter.GetSubject().GetType() != "" { + return false // Subject type is present, therefore filter is not empty. + } + + // If the subject IDs slice is not empty, the filter is not empty. + if len(filter.GetSubject().GetIds()) > 0 { + return false // Subject IDs are present, therefore filter is not empty. + } + + // If the subject relation is set, the filter is not empty. + if filter.GetSubject().GetRelation() != "" { + return false // Subject relation is present, therefore filter is not empty. + } + + // If none of the above conditions are met, then all fields are unset or have zero values, + // which means the filter is empty. + return true +} + +// IsAttributeFilterEmpty checks if the provided AttributeFilter object is empty. +// An AttributeFilter is considered empty if none of its fields have been set. +func IsAttributeFilterEmpty(filter *base.AttributeFilter) bool { + // Check if the Entity type field of the filter is set. If it is, the filter is not empty. + if filter.GetEntity().GetType() != "" { + return false // Entity type is specified, hence the filter is not empty. + } + + // Check if the Entity IDs field of the filter has any IDs. If it does, the filter is not empty. + if len(filter.GetEntity().GetIds()) > 0 { + return false // Entity IDs are specified, hence the filter is not empty. + } + + // Check if the Attributes field of the filter has any attributes set. If it does, the filter is not empty. + if len(filter.GetAttributes()) > 0 { + return false // Attributes are specified, hence the filter is not empty. + } + + // If none of the above fields are set, then the filter is considered empty. + return true +} diff --git a/internal/validation/validation_test.go b/internal/validation/validation_test.go index 1a8afe633..9fb778047 100644 --- a/internal/validation/validation_test.go +++ b/internal/validation/validation_test.go @@ -421,12 +421,99 @@ var _ = Describe("validation", func() { err = ValidateTupleFilter(&base.TupleFilter{ Entity: &base.EntityFilter{ Type: "", - Ids: []string{"1"}, + Ids: []string{}, }, - Relation: "admin", + Relation: "", Subject: &base.SubjectFilter{}, }) - Expect(err.Error()).Should(Equal(base.ErrorCode_ERROR_CODE_ENTITY_TYPE_REQUIRED.String())) + Expect(err.Error()).Should(Equal(base.ErrorCode_ERROR_CODE_VALIDATION.String())) + }) + + It("Case 6", func() { + is := IsAttributeFilterEmpty(&base.AttributeFilter{ + Entity: &base.EntityFilter{ + Type: "organization", + Ids: []string{"1"}, + }, + Attributes: []string{"public"}, + }) + Expect(is).Should(BeFalse()) + + is = IsAttributeFilterEmpty(&base.AttributeFilter{ + Entity: &base.EntityFilter{ + Type: "", + Ids: []string{}, + }, + Attributes: []string{}, + }) + Expect(is).Should(BeTrue()) + }) + + It("Case 7", func() { + is := IsAttributeFilterEmpty(&base.AttributeFilter{ + Entity: &base.EntityFilter{ + Type: "organization", + Ids: []string{"1"}, + }, + Attributes: []string{}, + }) + Expect(is).Should(BeFalse()) + + err := ValidateFilters( + &base.TupleFilter{}, &base.AttributeFilter{ + Entity: &base.EntityFilter{ + Type: "", + Ids: []string{}, + }, + Attributes: []string{}, + }) + Expect(err.Error()).Should(Equal(base.ErrorCode_ERROR_CODE_VALIDATION.String())) + }) + + It("Case 8", func() { + is := IsAttributeFilterEmpty(&base.AttributeFilter{ + Entity: &base.EntityFilter{ + Type: "organization", + Ids: []string{"1"}, + }, + Attributes: []string{}, + }) + Expect(is).Should(BeFalse()) + + is = IsAttributeFilterEmpty(&base.AttributeFilter{ + Entity: &base.EntityFilter{ + Type: "organization", + Ids: []string{}, + }, + Attributes: []string{}, + }) + Expect(is).Should(BeFalse()) + + is = IsAttributeFilterEmpty(&base.AttributeFilter{ + Entity: &base.EntityFilter{ + Type: "", + Ids: []string{"1"}, + }, + Attributes: []string{}, + }) + Expect(is).Should(BeFalse()) + + err := ValidateFilters( + &base.TupleFilter{ + Entity: &base.EntityFilter{ + Type: "", + Ids: []string{}, + }, + Relation: "", + Subject: &base.SubjectFilter{}, + }, &base.AttributeFilter{ + Entity: &base.EntityFilter{ + Type: "", + Ids: []string{}, + }, + Attributes: []string{}, + }) + Expect(err.Error()).Should(Equal(base.ErrorCode_ERROR_CODE_VALIDATION.String())) }) }) }) diff --git a/pkg/pb/base/v1/base.pb.go b/pkg/pb/base/v1/base.pb.go index aeb673cb4..e8ae666aa 100644 --- a/pkg/pb/base/v1/base.pb.go +++ b/pkg/pb/base/v1/base.pb.go @@ -3486,171 +3486,170 @@ var file_base_v1_base_proto_rawDesc = []byte{ 0x12, 0x39, 0x0a, 0x08, 0x72, 0x65, 0x6c, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x42, 0x1d, 0xfa, 0x42, 0x1a, 0x72, 0x18, 0x28, 0x40, 0x32, 0x11, 0x5e, 0x5b, 0x61, 0x2d, 0x7a, 0x41, 0x2d, 0x5a, 0x5f, 0x5d, 0x7b, 0x31, 0x2c, 0x36, 0x34, 0x7d, 0x24, 0xd0, 0x01, - 0x01, 0x52, 0x08, 0x72, 0x65, 0x6c, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x22, 0x6a, 0x0a, 0x0f, 0x41, - 0x74, 0x74, 0x72, 0x69, 0x62, 0x75, 0x74, 0x65, 0x46, 0x69, 0x6c, 0x74, 0x65, 0x72, 0x12, 0x37, + 0x01, 0x52, 0x08, 0x72, 0x65, 0x6c, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x22, 0x60, 0x0a, 0x0f, 0x41, + 0x74, 0x74, 0x72, 0x69, 0x62, 0x75, 0x74, 0x65, 0x46, 0x69, 0x6c, 0x74, 0x65, 0x72, 0x12, 0x2d, 0x0a, 0x06, 0x65, 0x6e, 0x74, 0x69, 0x74, 0x79, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x15, 0x2e, 0x62, 0x61, 0x73, 0x65, 0x2e, 0x76, 0x31, 0x2e, 0x45, 0x6e, 0x74, 0x69, 0x74, 0x79, 0x46, - 0x69, 0x6c, 0x74, 0x65, 0x72, 0x42, 0x08, 0xfa, 0x42, 0x05, 0x8a, 0x01, 0x02, 0x10, 0x01, 0x52, - 0x06, 0x65, 0x6e, 0x74, 0x69, 0x74, 0x79, 0x12, 0x1e, 0x0a, 0x0a, 0x61, 0x74, 0x74, 0x72, 0x69, - 0x62, 0x75, 0x74, 0x65, 0x73, 0x18, 0x02, 0x20, 0x03, 0x28, 0x09, 0x52, 0x0a, 0x61, 0x74, 0x74, - 0x72, 0x69, 0x62, 0x75, 0x74, 0x65, 0x73, 0x22, 0xb3, 0x01, 0x0a, 0x0b, 0x54, 0x75, 0x70, 0x6c, - 0x65, 0x46, 0x69, 0x6c, 0x74, 0x65, 0x72, 0x12, 0x37, 0x0a, 0x06, 0x65, 0x6e, 0x74, 0x69, 0x74, - 0x79, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x15, 0x2e, 0x62, 0x61, 0x73, 0x65, 0x2e, 0x76, - 0x31, 0x2e, 0x45, 0x6e, 0x74, 0x69, 0x74, 0x79, 0x46, 0x69, 0x6c, 0x74, 0x65, 0x72, 0x42, 0x08, - 0xfa, 0x42, 0x05, 0x8a, 0x01, 0x02, 0x10, 0x01, 0x52, 0x06, 0x65, 0x6e, 0x74, 0x69, 0x74, 0x79, - 0x12, 0x39, 0x0a, 0x08, 0x72, 0x65, 0x6c, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x18, 0x02, 0x20, 0x01, - 0x28, 0x09, 0x42, 0x1d, 0xfa, 0x42, 0x1a, 0x72, 0x18, 0x28, 0x40, 0x32, 0x11, 0x5e, 0x5b, 0x61, - 0x2d, 0x7a, 0x41, 0x2d, 0x5a, 0x5f, 0x5d, 0x7b, 0x31, 0x2c, 0x36, 0x34, 0x7d, 0x24, 0xd0, 0x01, - 0x01, 0x52, 0x08, 0x72, 0x65, 0x6c, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x12, 0x30, 0x0a, 0x07, 0x73, - 0x75, 0x62, 0x6a, 0x65, 0x63, 0x74, 0x18, 0x03, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x16, 0x2e, 0x62, - 0x61, 0x73, 0x65, 0x2e, 0x76, 0x31, 0x2e, 0x53, 0x75, 0x62, 0x6a, 0x65, 0x63, 0x74, 0x46, 0x69, - 0x6c, 0x74, 0x65, 0x72, 0x52, 0x07, 0x73, 0x75, 0x62, 0x6a, 0x65, 0x63, 0x74, 0x22, 0x34, 0x0a, - 0x0c, 0x45, 0x6e, 0x74, 0x69, 0x74, 0x79, 0x46, 0x69, 0x6c, 0x74, 0x65, 0x72, 0x12, 0x12, 0x0a, - 0x04, 0x74, 0x79, 0x70, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x74, 0x79, 0x70, - 0x65, 0x12, 0x10, 0x0a, 0x03, 0x69, 0x64, 0x73, 0x18, 0x02, 0x20, 0x03, 0x28, 0x09, 0x52, 0x03, - 0x69, 0x64, 0x73, 0x22, 0x70, 0x0a, 0x0d, 0x53, 0x75, 0x62, 0x6a, 0x65, 0x63, 0x74, 0x46, 0x69, - 0x6c, 0x74, 0x65, 0x72, 0x12, 0x12, 0x0a, 0x04, 0x74, 0x79, 0x70, 0x65, 0x18, 0x01, 0x20, 0x01, - 0x28, 0x09, 0x52, 0x04, 0x74, 0x79, 0x70, 0x65, 0x12, 0x10, 0x0a, 0x03, 0x69, 0x64, 0x73, 0x18, - 0x02, 0x20, 0x03, 0x28, 0x09, 0x52, 0x03, 0x69, 0x64, 0x73, 0x12, 0x39, 0x0a, 0x08, 0x72, 0x65, - 0x6c, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x42, 0x1d, 0xfa, 0x42, - 0x1a, 0x72, 0x18, 0x28, 0x40, 0x32, 0x11, 0x5e, 0x5b, 0x61, 0x2d, 0x7a, 0x41, 0x2d, 0x5a, 0x5f, - 0x5d, 0x7b, 0x31, 0x2c, 0x36, 0x34, 0x7d, 0x24, 0xd0, 0x01, 0x01, 0x52, 0x08, 0x72, 0x65, 0x6c, - 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x22, 0xf0, 0x01, 0x0a, 0x0e, 0x45, 0x78, 0x70, 0x61, 0x6e, 0x64, - 0x54, 0x72, 0x65, 0x65, 0x4e, 0x6f, 0x64, 0x65, 0x12, 0x3f, 0x0a, 0x09, 0x6f, 0x70, 0x65, 0x72, - 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0e, 0x32, 0x21, 0x2e, 0x62, 0x61, - 0x73, 0x65, 0x2e, 0x76, 0x31, 0x2e, 0x45, 0x78, 0x70, 0x61, 0x6e, 0x64, 0x54, 0x72, 0x65, 0x65, - 0x4e, 0x6f, 0x64, 0x65, 0x2e, 0x4f, 0x70, 0x65, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x52, 0x09, - 0x6f, 0x70, 0x65, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x12, 0x2b, 0x0a, 0x08, 0x63, 0x68, 0x69, - 0x6c, 0x64, 0x72, 0x65, 0x6e, 0x18, 0x02, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x0f, 0x2e, 0x62, 0x61, - 0x73, 0x65, 0x2e, 0x76, 0x31, 0x2e, 0x45, 0x78, 0x70, 0x61, 0x6e, 0x64, 0x52, 0x08, 0x63, 0x68, - 0x69, 0x6c, 0x64, 0x72, 0x65, 0x6e, 0x22, 0x70, 0x0a, 0x09, 0x4f, 0x70, 0x65, 0x72, 0x61, 0x74, - 0x69, 0x6f, 0x6e, 0x12, 0x19, 0x0a, 0x15, 0x4f, 0x50, 0x45, 0x52, 0x41, 0x54, 0x49, 0x4f, 0x4e, - 0x5f, 0x55, 0x4e, 0x53, 0x50, 0x45, 0x43, 0x49, 0x46, 0x49, 0x45, 0x44, 0x10, 0x00, 0x12, 0x13, - 0x0a, 0x0f, 0x4f, 0x50, 0x45, 0x52, 0x41, 0x54, 0x49, 0x4f, 0x4e, 0x5f, 0x55, 0x4e, 0x49, 0x4f, - 0x4e, 0x10, 0x01, 0x12, 0x1a, 0x0a, 0x16, 0x4f, 0x50, 0x45, 0x52, 0x41, 0x54, 0x49, 0x4f, 0x4e, - 0x5f, 0x49, 0x4e, 0x54, 0x45, 0x52, 0x53, 0x45, 0x43, 0x54, 0x49, 0x4f, 0x4e, 0x10, 0x02, 0x12, - 0x17, 0x0a, 0x13, 0x4f, 0x50, 0x45, 0x52, 0x41, 0x54, 0x49, 0x4f, 0x4e, 0x5f, 0x45, 0x58, 0x43, - 0x4c, 0x55, 0x53, 0x49, 0x4f, 0x4e, 0x10, 0x03, 0x22, 0xe8, 0x01, 0x0a, 0x06, 0x45, 0x78, 0x70, - 0x61, 0x6e, 0x64, 0x12, 0x27, 0x0a, 0x06, 0x65, 0x6e, 0x74, 0x69, 0x74, 0x79, 0x18, 0x01, 0x20, - 0x01, 0x28, 0x0b, 0x32, 0x0f, 0x2e, 0x62, 0x61, 0x73, 0x65, 0x2e, 0x76, 0x31, 0x2e, 0x45, 0x6e, - 0x74, 0x69, 0x74, 0x79, 0x52, 0x06, 0x65, 0x6e, 0x74, 0x69, 0x74, 0x79, 0x12, 0x1e, 0x0a, 0x0a, - 0x70, 0x65, 0x72, 0x6d, 0x69, 0x73, 0x73, 0x69, 0x6f, 0x6e, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, - 0x52, 0x0a, 0x70, 0x65, 0x72, 0x6d, 0x69, 0x73, 0x73, 0x69, 0x6f, 0x6e, 0x12, 0x2f, 0x0a, 0x09, - 0x61, 0x72, 0x67, 0x75, 0x6d, 0x65, 0x6e, 0x74, 0x73, 0x18, 0x03, 0x20, 0x03, 0x28, 0x0b, 0x32, - 0x11, 0x2e, 0x62, 0x61, 0x73, 0x65, 0x2e, 0x76, 0x31, 0x2e, 0x41, 0x72, 0x67, 0x75, 0x6d, 0x65, - 0x6e, 0x74, 0x52, 0x09, 0x61, 0x72, 0x67, 0x75, 0x6d, 0x65, 0x6e, 0x74, 0x73, 0x12, 0x31, 0x0a, - 0x06, 0x65, 0x78, 0x70, 0x61, 0x6e, 0x64, 0x18, 0x04, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x17, 0x2e, - 0x62, 0x61, 0x73, 0x65, 0x2e, 0x76, 0x31, 0x2e, 0x45, 0x78, 0x70, 0x61, 0x6e, 0x64, 0x54, 0x72, - 0x65, 0x65, 0x4e, 0x6f, 0x64, 0x65, 0x48, 0x00, 0x52, 0x06, 0x65, 0x78, 0x70, 0x61, 0x6e, 0x64, - 0x12, 0x29, 0x0a, 0x04, 0x6c, 0x65, 0x61, 0x66, 0x18, 0x05, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x13, - 0x2e, 0x62, 0x61, 0x73, 0x65, 0x2e, 0x76, 0x31, 0x2e, 0x45, 0x78, 0x70, 0x61, 0x6e, 0x64, 0x4c, - 0x65, 0x61, 0x66, 0x48, 0x00, 0x52, 0x04, 0x6c, 0x65, 0x61, 0x66, 0x42, 0x06, 0x0a, 0x04, 0x6e, - 0x6f, 0x64, 0x65, 0x22, 0xa3, 0x01, 0x0a, 0x0a, 0x45, 0x78, 0x70, 0x61, 0x6e, 0x64, 0x4c, 0x65, - 0x61, 0x66, 0x12, 0x2f, 0x0a, 0x08, 0x73, 0x75, 0x62, 0x6a, 0x65, 0x63, 0x74, 0x73, 0x18, 0x01, - 0x20, 0x01, 0x28, 0x0b, 0x32, 0x11, 0x2e, 0x62, 0x61, 0x73, 0x65, 0x2e, 0x76, 0x31, 0x2e, 0x53, - 0x75, 0x62, 0x6a, 0x65, 0x63, 0x74, 0x73, 0x48, 0x00, 0x52, 0x08, 0x73, 0x75, 0x62, 0x6a, 0x65, - 0x63, 0x74, 0x73, 0x12, 0x29, 0x0a, 0x06, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x73, 0x18, 0x02, 0x20, - 0x01, 0x28, 0x0b, 0x32, 0x0f, 0x2e, 0x62, 0x61, 0x73, 0x65, 0x2e, 0x76, 0x31, 0x2e, 0x56, 0x61, - 0x6c, 0x75, 0x65, 0x73, 0x48, 0x00, 0x52, 0x06, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x73, 0x12, 0x2c, - 0x0a, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x18, 0x03, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x14, 0x2e, - 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, - 0x41, 0x6e, 0x79, 0x48, 0x00, 0x52, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x42, 0x0b, 0x0a, 0x04, - 0x74, 0x79, 0x70, 0x65, 0x12, 0x03, 0xf8, 0x42, 0x01, 0x22, 0x8e, 0x01, 0x0a, 0x06, 0x56, 0x61, - 0x6c, 0x75, 0x65, 0x73, 0x12, 0x33, 0x0a, 0x06, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x73, 0x18, 0x01, - 0x20, 0x03, 0x28, 0x0b, 0x32, 0x1b, 0x2e, 0x62, 0x61, 0x73, 0x65, 0x2e, 0x76, 0x31, 0x2e, 0x56, - 0x61, 0x6c, 0x75, 0x65, 0x73, 0x2e, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x73, 0x45, 0x6e, 0x74, 0x72, - 0x79, 0x52, 0x06, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x73, 0x1a, 0x4f, 0x0a, 0x0b, 0x56, 0x61, 0x6c, - 0x75, 0x65, 0x73, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x12, 0x10, 0x0a, 0x03, 0x6b, 0x65, 0x79, 0x18, - 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x03, 0x6b, 0x65, 0x79, 0x12, 0x2a, 0x0a, 0x05, 0x76, 0x61, - 0x6c, 0x75, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x14, 0x2e, 0x67, 0x6f, 0x6f, 0x67, - 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x41, 0x6e, 0x79, 0x52, - 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x3a, 0x02, 0x38, 0x01, 0x22, 0x38, 0x0a, 0x08, 0x53, 0x75, - 0x62, 0x6a, 0x65, 0x63, 0x74, 0x73, 0x12, 0x2c, 0x0a, 0x08, 0x73, 0x75, 0x62, 0x6a, 0x65, 0x63, - 0x74, 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x10, 0x2e, 0x62, 0x61, 0x73, 0x65, 0x2e, - 0x76, 0x31, 0x2e, 0x53, 0x75, 0x62, 0x6a, 0x65, 0x63, 0x74, 0x52, 0x08, 0x73, 0x75, 0x62, 0x6a, - 0x65, 0x63, 0x74, 0x73, 0x22, 0x68, 0x0a, 0x06, 0x54, 0x65, 0x6e, 0x61, 0x6e, 0x74, 0x12, 0x0e, - 0x0a, 0x02, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x02, 0x69, 0x64, 0x12, 0x12, - 0x0a, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x6e, 0x61, - 0x6d, 0x65, 0x12, 0x3a, 0x0a, 0x0a, 0x63, 0x72, 0x65, 0x61, 0x74, 0x65, 0x64, 0x5f, 0x61, 0x74, - 0x18, 0x03, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1a, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, - 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x54, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, - 0x6d, 0x70, 0x52, 0x0a, 0x63, 0x72, 0x65, 0x61, 0x74, 0x65, 0x64, 0x5f, 0x61, 0x74, 0x22, 0x66, - 0x0a, 0x0b, 0x44, 0x61, 0x74, 0x61, 0x43, 0x68, 0x61, 0x6e, 0x67, 0x65, 0x73, 0x12, 0x1e, 0x0a, - 0x0a, 0x73, 0x6e, 0x61, 0x70, 0x5f, 0x74, 0x6f, 0x6b, 0x65, 0x6e, 0x18, 0x01, 0x20, 0x01, 0x28, - 0x09, 0x52, 0x0a, 0x73, 0x6e, 0x61, 0x70, 0x5f, 0x74, 0x6f, 0x6b, 0x65, 0x6e, 0x12, 0x37, 0x0a, - 0x0c, 0x64, 0x61, 0x74, 0x61, 0x5f, 0x63, 0x68, 0x61, 0x6e, 0x67, 0x65, 0x73, 0x18, 0x02, 0x20, - 0x03, 0x28, 0x0b, 0x32, 0x13, 0x2e, 0x62, 0x61, 0x73, 0x65, 0x2e, 0x76, 0x31, 0x2e, 0x44, 0x61, - 0x74, 0x61, 0x43, 0x68, 0x61, 0x6e, 0x67, 0x65, 0x52, 0x0c, 0x64, 0x61, 0x74, 0x61, 0x5f, 0x63, - 0x68, 0x61, 0x6e, 0x67, 0x65, 0x73, 0x22, 0x86, 0x02, 0x0a, 0x0a, 0x44, 0x61, 0x74, 0x61, 0x43, - 0x68, 0x61, 0x6e, 0x67, 0x65, 0x12, 0x3b, 0x0a, 0x09, 0x6f, 0x70, 0x65, 0x72, 0x61, 0x74, 0x69, - 0x6f, 0x6e, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0e, 0x32, 0x1d, 0x2e, 0x62, 0x61, 0x73, 0x65, 0x2e, - 0x76, 0x31, 0x2e, 0x44, 0x61, 0x74, 0x61, 0x43, 0x68, 0x61, 0x6e, 0x67, 0x65, 0x2e, 0x4f, 0x70, - 0x65, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x52, 0x09, 0x6f, 0x70, 0x65, 0x72, 0x61, 0x74, 0x69, - 0x6f, 0x6e, 0x12, 0x26, 0x0a, 0x05, 0x74, 0x75, 0x70, 0x6c, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, - 0x0b, 0x32, 0x0e, 0x2e, 0x62, 0x61, 0x73, 0x65, 0x2e, 0x76, 0x31, 0x2e, 0x54, 0x75, 0x70, 0x6c, - 0x65, 0x48, 0x00, 0x52, 0x05, 0x74, 0x75, 0x70, 0x6c, 0x65, 0x12, 0x32, 0x0a, 0x09, 0x61, 0x74, - 0x74, 0x72, 0x69, 0x62, 0x75, 0x74, 0x65, 0x18, 0x03, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x12, 0x2e, - 0x62, 0x61, 0x73, 0x65, 0x2e, 0x76, 0x31, 0x2e, 0x41, 0x74, 0x74, 0x72, 0x69, 0x62, 0x75, 0x74, - 0x65, 0x48, 0x00, 0x52, 0x09, 0x61, 0x74, 0x74, 0x72, 0x69, 0x62, 0x75, 0x74, 0x65, 0x22, 0x52, - 0x0a, 0x09, 0x4f, 0x70, 0x65, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x12, 0x19, 0x0a, 0x15, 0x4f, - 0x50, 0x45, 0x52, 0x41, 0x54, 0x49, 0x4f, 0x4e, 0x5f, 0x55, 0x4e, 0x53, 0x50, 0x45, 0x43, 0x49, - 0x46, 0x49, 0x45, 0x44, 0x10, 0x00, 0x12, 0x14, 0x0a, 0x10, 0x4f, 0x50, 0x45, 0x52, 0x41, 0x54, - 0x49, 0x4f, 0x4e, 0x5f, 0x43, 0x52, 0x45, 0x41, 0x54, 0x45, 0x10, 0x01, 0x12, 0x14, 0x0a, 0x10, - 0x4f, 0x50, 0x45, 0x52, 0x41, 0x54, 0x49, 0x4f, 0x4e, 0x5f, 0x44, 0x45, 0x4c, 0x45, 0x54, 0x45, - 0x10, 0x02, 0x42, 0x0b, 0x0a, 0x04, 0x74, 0x79, 0x70, 0x65, 0x12, 0x03, 0xf8, 0x42, 0x01, 0x22, - 0x21, 0x0a, 0x0b, 0x53, 0x74, 0x72, 0x69, 0x6e, 0x67, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x12, 0x12, - 0x0a, 0x04, 0x64, 0x61, 0x74, 0x61, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x64, 0x61, - 0x74, 0x61, 0x22, 0x22, 0x0a, 0x0c, 0x49, 0x6e, 0x74, 0x65, 0x67, 0x65, 0x72, 0x56, 0x61, 0x6c, - 0x75, 0x65, 0x12, 0x12, 0x0a, 0x04, 0x64, 0x61, 0x74, 0x61, 0x18, 0x01, 0x20, 0x01, 0x28, 0x05, - 0x52, 0x04, 0x64, 0x61, 0x74, 0x61, 0x22, 0x21, 0x0a, 0x0b, 0x44, 0x6f, 0x75, 0x62, 0x6c, 0x65, + 0x69, 0x6c, 0x74, 0x65, 0x72, 0x52, 0x06, 0x65, 0x6e, 0x74, 0x69, 0x74, 0x79, 0x12, 0x1e, 0x0a, + 0x0a, 0x61, 0x74, 0x74, 0x72, 0x69, 0x62, 0x75, 0x74, 0x65, 0x73, 0x18, 0x02, 0x20, 0x03, 0x28, + 0x09, 0x52, 0x0a, 0x61, 0x74, 0x74, 0x72, 0x69, 0x62, 0x75, 0x74, 0x65, 0x73, 0x22, 0xa9, 0x01, + 0x0a, 0x0b, 0x54, 0x75, 0x70, 0x6c, 0x65, 0x46, 0x69, 0x6c, 0x74, 0x65, 0x72, 0x12, 0x2d, 0x0a, + 0x06, 0x65, 0x6e, 0x74, 0x69, 0x74, 0x79, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x15, 0x2e, + 0x62, 0x61, 0x73, 0x65, 0x2e, 0x76, 0x31, 0x2e, 0x45, 0x6e, 0x74, 0x69, 0x74, 0x79, 0x46, 0x69, + 0x6c, 0x74, 0x65, 0x72, 0x52, 0x06, 0x65, 0x6e, 0x74, 0x69, 0x74, 0x79, 0x12, 0x39, 0x0a, 0x08, + 0x72, 0x65, 0x6c, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x42, 0x1d, + 0xfa, 0x42, 0x1a, 0x72, 0x18, 0x28, 0x40, 0x32, 0x11, 0x5e, 0x5b, 0x61, 0x2d, 0x7a, 0x41, 0x2d, + 0x5a, 0x5f, 0x5d, 0x7b, 0x31, 0x2c, 0x36, 0x34, 0x7d, 0x24, 0xd0, 0x01, 0x01, 0x52, 0x08, 0x72, + 0x65, 0x6c, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x12, 0x30, 0x0a, 0x07, 0x73, 0x75, 0x62, 0x6a, 0x65, + 0x63, 0x74, 0x18, 0x03, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x16, 0x2e, 0x62, 0x61, 0x73, 0x65, 0x2e, + 0x76, 0x31, 0x2e, 0x53, 0x75, 0x62, 0x6a, 0x65, 0x63, 0x74, 0x46, 0x69, 0x6c, 0x74, 0x65, 0x72, + 0x52, 0x07, 0x73, 0x75, 0x62, 0x6a, 0x65, 0x63, 0x74, 0x22, 0x34, 0x0a, 0x0c, 0x45, 0x6e, 0x74, + 0x69, 0x74, 0x79, 0x46, 0x69, 0x6c, 0x74, 0x65, 0x72, 0x12, 0x12, 0x0a, 0x04, 0x74, 0x79, 0x70, + 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x74, 0x79, 0x70, 0x65, 0x12, 0x10, 0x0a, + 0x03, 0x69, 0x64, 0x73, 0x18, 0x02, 0x20, 0x03, 0x28, 0x09, 0x52, 0x03, 0x69, 0x64, 0x73, 0x22, + 0x70, 0x0a, 0x0d, 0x53, 0x75, 0x62, 0x6a, 0x65, 0x63, 0x74, 0x46, 0x69, 0x6c, 0x74, 0x65, 0x72, + 0x12, 0x12, 0x0a, 0x04, 0x74, 0x79, 0x70, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, + 0x74, 0x79, 0x70, 0x65, 0x12, 0x10, 0x0a, 0x03, 0x69, 0x64, 0x73, 0x18, 0x02, 0x20, 0x03, 0x28, + 0x09, 0x52, 0x03, 0x69, 0x64, 0x73, 0x12, 0x39, 0x0a, 0x08, 0x72, 0x65, 0x6c, 0x61, 0x74, 0x69, + 0x6f, 0x6e, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x42, 0x1d, 0xfa, 0x42, 0x1a, 0x72, 0x18, 0x28, + 0x40, 0x32, 0x11, 0x5e, 0x5b, 0x61, 0x2d, 0x7a, 0x41, 0x2d, 0x5a, 0x5f, 0x5d, 0x7b, 0x31, 0x2c, + 0x36, 0x34, 0x7d, 0x24, 0xd0, 0x01, 0x01, 0x52, 0x08, 0x72, 0x65, 0x6c, 0x61, 0x74, 0x69, 0x6f, + 0x6e, 0x22, 0xf0, 0x01, 0x0a, 0x0e, 0x45, 0x78, 0x70, 0x61, 0x6e, 0x64, 0x54, 0x72, 0x65, 0x65, + 0x4e, 0x6f, 0x64, 0x65, 0x12, 0x3f, 0x0a, 0x09, 0x6f, 0x70, 0x65, 0x72, 0x61, 0x74, 0x69, 0x6f, + 0x6e, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0e, 0x32, 0x21, 0x2e, 0x62, 0x61, 0x73, 0x65, 0x2e, 0x76, + 0x31, 0x2e, 0x45, 0x78, 0x70, 0x61, 0x6e, 0x64, 0x54, 0x72, 0x65, 0x65, 0x4e, 0x6f, 0x64, 0x65, + 0x2e, 0x4f, 0x70, 0x65, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x52, 0x09, 0x6f, 0x70, 0x65, 0x72, + 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x12, 0x2b, 0x0a, 0x08, 0x63, 0x68, 0x69, 0x6c, 0x64, 0x72, 0x65, + 0x6e, 0x18, 0x02, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x0f, 0x2e, 0x62, 0x61, 0x73, 0x65, 0x2e, 0x76, + 0x31, 0x2e, 0x45, 0x78, 0x70, 0x61, 0x6e, 0x64, 0x52, 0x08, 0x63, 0x68, 0x69, 0x6c, 0x64, 0x72, + 0x65, 0x6e, 0x22, 0x70, 0x0a, 0x09, 0x4f, 0x70, 0x65, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x12, + 0x19, 0x0a, 0x15, 0x4f, 0x50, 0x45, 0x52, 0x41, 0x54, 0x49, 0x4f, 0x4e, 0x5f, 0x55, 0x4e, 0x53, + 0x50, 0x45, 0x43, 0x49, 0x46, 0x49, 0x45, 0x44, 0x10, 0x00, 0x12, 0x13, 0x0a, 0x0f, 0x4f, 0x50, + 0x45, 0x52, 0x41, 0x54, 0x49, 0x4f, 0x4e, 0x5f, 0x55, 0x4e, 0x49, 0x4f, 0x4e, 0x10, 0x01, 0x12, + 0x1a, 0x0a, 0x16, 0x4f, 0x50, 0x45, 0x52, 0x41, 0x54, 0x49, 0x4f, 0x4e, 0x5f, 0x49, 0x4e, 0x54, + 0x45, 0x52, 0x53, 0x45, 0x43, 0x54, 0x49, 0x4f, 0x4e, 0x10, 0x02, 0x12, 0x17, 0x0a, 0x13, 0x4f, + 0x50, 0x45, 0x52, 0x41, 0x54, 0x49, 0x4f, 0x4e, 0x5f, 0x45, 0x58, 0x43, 0x4c, 0x55, 0x53, 0x49, + 0x4f, 0x4e, 0x10, 0x03, 0x22, 0xe8, 0x01, 0x0a, 0x06, 0x45, 0x78, 0x70, 0x61, 0x6e, 0x64, 0x12, + 0x27, 0x0a, 0x06, 0x65, 0x6e, 0x74, 0x69, 0x74, 0x79, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, + 0x0f, 0x2e, 0x62, 0x61, 0x73, 0x65, 0x2e, 0x76, 0x31, 0x2e, 0x45, 0x6e, 0x74, 0x69, 0x74, 0x79, + 0x52, 0x06, 0x65, 0x6e, 0x74, 0x69, 0x74, 0x79, 0x12, 0x1e, 0x0a, 0x0a, 0x70, 0x65, 0x72, 0x6d, + 0x69, 0x73, 0x73, 0x69, 0x6f, 0x6e, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0a, 0x70, 0x65, + 0x72, 0x6d, 0x69, 0x73, 0x73, 0x69, 0x6f, 0x6e, 0x12, 0x2f, 0x0a, 0x09, 0x61, 0x72, 0x67, 0x75, + 0x6d, 0x65, 0x6e, 0x74, 0x73, 0x18, 0x03, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x11, 0x2e, 0x62, 0x61, + 0x73, 0x65, 0x2e, 0x76, 0x31, 0x2e, 0x41, 0x72, 0x67, 0x75, 0x6d, 0x65, 0x6e, 0x74, 0x52, 0x09, + 0x61, 0x72, 0x67, 0x75, 0x6d, 0x65, 0x6e, 0x74, 0x73, 0x12, 0x31, 0x0a, 0x06, 0x65, 0x78, 0x70, + 0x61, 0x6e, 0x64, 0x18, 0x04, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x17, 0x2e, 0x62, 0x61, 0x73, 0x65, + 0x2e, 0x76, 0x31, 0x2e, 0x45, 0x78, 0x70, 0x61, 0x6e, 0x64, 0x54, 0x72, 0x65, 0x65, 0x4e, 0x6f, + 0x64, 0x65, 0x48, 0x00, 0x52, 0x06, 0x65, 0x78, 0x70, 0x61, 0x6e, 0x64, 0x12, 0x29, 0x0a, 0x04, + 0x6c, 0x65, 0x61, 0x66, 0x18, 0x05, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x13, 0x2e, 0x62, 0x61, 0x73, + 0x65, 0x2e, 0x76, 0x31, 0x2e, 0x45, 0x78, 0x70, 0x61, 0x6e, 0x64, 0x4c, 0x65, 0x61, 0x66, 0x48, + 0x00, 0x52, 0x04, 0x6c, 0x65, 0x61, 0x66, 0x42, 0x06, 0x0a, 0x04, 0x6e, 0x6f, 0x64, 0x65, 0x22, + 0xa3, 0x01, 0x0a, 0x0a, 0x45, 0x78, 0x70, 0x61, 0x6e, 0x64, 0x4c, 0x65, 0x61, 0x66, 0x12, 0x2f, + 0x0a, 0x08, 0x73, 0x75, 0x62, 0x6a, 0x65, 0x63, 0x74, 0x73, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, + 0x32, 0x11, 0x2e, 0x62, 0x61, 0x73, 0x65, 0x2e, 0x76, 0x31, 0x2e, 0x53, 0x75, 0x62, 0x6a, 0x65, + 0x63, 0x74, 0x73, 0x48, 0x00, 0x52, 0x08, 0x73, 0x75, 0x62, 0x6a, 0x65, 0x63, 0x74, 0x73, 0x12, + 0x29, 0x0a, 0x06, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x73, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, + 0x0f, 0x2e, 0x62, 0x61, 0x73, 0x65, 0x2e, 0x76, 0x31, 0x2e, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x73, + 0x48, 0x00, 0x52, 0x06, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x73, 0x12, 0x2c, 0x0a, 0x05, 0x76, 0x61, + 0x6c, 0x75, 0x65, 0x18, 0x03, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x14, 0x2e, 0x67, 0x6f, 0x6f, 0x67, + 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x41, 0x6e, 0x79, 0x48, + 0x00, 0x52, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x42, 0x0b, 0x0a, 0x04, 0x74, 0x79, 0x70, 0x65, + 0x12, 0x03, 0xf8, 0x42, 0x01, 0x22, 0x8e, 0x01, 0x0a, 0x06, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x73, + 0x12, 0x33, 0x0a, 0x06, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, 0x0b, + 0x32, 0x1b, 0x2e, 0x62, 0x61, 0x73, 0x65, 0x2e, 0x76, 0x31, 0x2e, 0x56, 0x61, 0x6c, 0x75, 0x65, + 0x73, 0x2e, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x73, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x52, 0x06, 0x76, + 0x61, 0x6c, 0x75, 0x65, 0x73, 0x1a, 0x4f, 0x0a, 0x0b, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x73, 0x45, + 0x6e, 0x74, 0x72, 0x79, 0x12, 0x10, 0x0a, 0x03, 0x6b, 0x65, 0x79, 0x18, 0x01, 0x20, 0x01, 0x28, + 0x09, 0x52, 0x03, 0x6b, 0x65, 0x79, 0x12, 0x2a, 0x0a, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x18, + 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x14, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, + 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x41, 0x6e, 0x79, 0x52, 0x05, 0x76, 0x61, 0x6c, + 0x75, 0x65, 0x3a, 0x02, 0x38, 0x01, 0x22, 0x38, 0x0a, 0x08, 0x53, 0x75, 0x62, 0x6a, 0x65, 0x63, + 0x74, 0x73, 0x12, 0x2c, 0x0a, 0x08, 0x73, 0x75, 0x62, 0x6a, 0x65, 0x63, 0x74, 0x73, 0x18, 0x01, + 0x20, 0x03, 0x28, 0x0b, 0x32, 0x10, 0x2e, 0x62, 0x61, 0x73, 0x65, 0x2e, 0x76, 0x31, 0x2e, 0x53, + 0x75, 0x62, 0x6a, 0x65, 0x63, 0x74, 0x52, 0x08, 0x73, 0x75, 0x62, 0x6a, 0x65, 0x63, 0x74, 0x73, + 0x22, 0x68, 0x0a, 0x06, 0x54, 0x65, 0x6e, 0x61, 0x6e, 0x74, 0x12, 0x0e, 0x0a, 0x02, 0x69, 0x64, + 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x02, 0x69, 0x64, 0x12, 0x12, 0x0a, 0x04, 0x6e, 0x61, + 0x6d, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x12, 0x3a, + 0x0a, 0x0a, 0x63, 0x72, 0x65, 0x61, 0x74, 0x65, 0x64, 0x5f, 0x61, 0x74, 0x18, 0x03, 0x20, 0x01, + 0x28, 0x0b, 0x32, 0x1a, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, + 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x54, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x52, 0x0a, + 0x63, 0x72, 0x65, 0x61, 0x74, 0x65, 0x64, 0x5f, 0x61, 0x74, 0x22, 0x66, 0x0a, 0x0b, 0x44, 0x61, + 0x74, 0x61, 0x43, 0x68, 0x61, 0x6e, 0x67, 0x65, 0x73, 0x12, 0x1e, 0x0a, 0x0a, 0x73, 0x6e, 0x61, + 0x70, 0x5f, 0x74, 0x6f, 0x6b, 0x65, 0x6e, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0a, 0x73, + 0x6e, 0x61, 0x70, 0x5f, 0x74, 0x6f, 0x6b, 0x65, 0x6e, 0x12, 0x37, 0x0a, 0x0c, 0x64, 0x61, 0x74, + 0x61, 0x5f, 0x63, 0x68, 0x61, 0x6e, 0x67, 0x65, 0x73, 0x18, 0x02, 0x20, 0x03, 0x28, 0x0b, 0x32, + 0x13, 0x2e, 0x62, 0x61, 0x73, 0x65, 0x2e, 0x76, 0x31, 0x2e, 0x44, 0x61, 0x74, 0x61, 0x43, 0x68, + 0x61, 0x6e, 0x67, 0x65, 0x52, 0x0c, 0x64, 0x61, 0x74, 0x61, 0x5f, 0x63, 0x68, 0x61, 0x6e, 0x67, + 0x65, 0x73, 0x22, 0x86, 0x02, 0x0a, 0x0a, 0x44, 0x61, 0x74, 0x61, 0x43, 0x68, 0x61, 0x6e, 0x67, + 0x65, 0x12, 0x3b, 0x0a, 0x09, 0x6f, 0x70, 0x65, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x18, 0x01, + 0x20, 0x01, 0x28, 0x0e, 0x32, 0x1d, 0x2e, 0x62, 0x61, 0x73, 0x65, 0x2e, 0x76, 0x31, 0x2e, 0x44, + 0x61, 0x74, 0x61, 0x43, 0x68, 0x61, 0x6e, 0x67, 0x65, 0x2e, 0x4f, 0x70, 0x65, 0x72, 0x61, 0x74, + 0x69, 0x6f, 0x6e, 0x52, 0x09, 0x6f, 0x70, 0x65, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x12, 0x26, + 0x0a, 0x05, 0x74, 0x75, 0x70, 0x6c, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x0e, 0x2e, + 0x62, 0x61, 0x73, 0x65, 0x2e, 0x76, 0x31, 0x2e, 0x54, 0x75, 0x70, 0x6c, 0x65, 0x48, 0x00, 0x52, + 0x05, 0x74, 0x75, 0x70, 0x6c, 0x65, 0x12, 0x32, 0x0a, 0x09, 0x61, 0x74, 0x74, 0x72, 0x69, 0x62, + 0x75, 0x74, 0x65, 0x18, 0x03, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x12, 0x2e, 0x62, 0x61, 0x73, 0x65, + 0x2e, 0x76, 0x31, 0x2e, 0x41, 0x74, 0x74, 0x72, 0x69, 0x62, 0x75, 0x74, 0x65, 0x48, 0x00, 0x52, + 0x09, 0x61, 0x74, 0x74, 0x72, 0x69, 0x62, 0x75, 0x74, 0x65, 0x22, 0x52, 0x0a, 0x09, 0x4f, 0x70, + 0x65, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x12, 0x19, 0x0a, 0x15, 0x4f, 0x50, 0x45, 0x52, 0x41, + 0x54, 0x49, 0x4f, 0x4e, 0x5f, 0x55, 0x4e, 0x53, 0x50, 0x45, 0x43, 0x49, 0x46, 0x49, 0x45, 0x44, + 0x10, 0x00, 0x12, 0x14, 0x0a, 0x10, 0x4f, 0x50, 0x45, 0x52, 0x41, 0x54, 0x49, 0x4f, 0x4e, 0x5f, + 0x43, 0x52, 0x45, 0x41, 0x54, 0x45, 0x10, 0x01, 0x12, 0x14, 0x0a, 0x10, 0x4f, 0x50, 0x45, 0x52, + 0x41, 0x54, 0x49, 0x4f, 0x4e, 0x5f, 0x44, 0x45, 0x4c, 0x45, 0x54, 0x45, 0x10, 0x02, 0x42, 0x0b, + 0x0a, 0x04, 0x74, 0x79, 0x70, 0x65, 0x12, 0x03, 0xf8, 0x42, 0x01, 0x22, 0x21, 0x0a, 0x0b, 0x53, + 0x74, 0x72, 0x69, 0x6e, 0x67, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x12, 0x12, 0x0a, 0x04, 0x64, 0x61, + 0x74, 0x61, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x64, 0x61, 0x74, 0x61, 0x22, 0x22, + 0x0a, 0x0c, 0x49, 0x6e, 0x74, 0x65, 0x67, 0x65, 0x72, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x12, 0x12, + 0x0a, 0x04, 0x64, 0x61, 0x74, 0x61, 0x18, 0x01, 0x20, 0x01, 0x28, 0x05, 0x52, 0x04, 0x64, 0x61, + 0x74, 0x61, 0x22, 0x21, 0x0a, 0x0b, 0x44, 0x6f, 0x75, 0x62, 0x6c, 0x65, 0x56, 0x61, 0x6c, 0x75, + 0x65, 0x12, 0x12, 0x0a, 0x04, 0x64, 0x61, 0x74, 0x61, 0x18, 0x01, 0x20, 0x01, 0x28, 0x01, 0x52, + 0x04, 0x64, 0x61, 0x74, 0x61, 0x22, 0x22, 0x0a, 0x0c, 0x42, 0x6f, 0x6f, 0x6c, 0x65, 0x61, 0x6e, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x12, 0x12, 0x0a, 0x04, 0x64, 0x61, 0x74, 0x61, 0x18, 0x01, 0x20, - 0x01, 0x28, 0x01, 0x52, 0x04, 0x64, 0x61, 0x74, 0x61, 0x22, 0x22, 0x0a, 0x0c, 0x42, 0x6f, 0x6f, - 0x6c, 0x65, 0x61, 0x6e, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x12, 0x12, 0x0a, 0x04, 0x64, 0x61, 0x74, - 0x61, 0x18, 0x01, 0x20, 0x01, 0x28, 0x08, 0x52, 0x04, 0x64, 0x61, 0x74, 0x61, 0x22, 0x26, 0x0a, - 0x10, 0x53, 0x74, 0x72, 0x69, 0x6e, 0x67, 0x41, 0x72, 0x72, 0x61, 0x79, 0x56, 0x61, 0x6c, 0x75, - 0x65, 0x12, 0x12, 0x0a, 0x04, 0x64, 0x61, 0x74, 0x61, 0x18, 0x01, 0x20, 0x03, 0x28, 0x09, 0x52, - 0x04, 0x64, 0x61, 0x74, 0x61, 0x22, 0x27, 0x0a, 0x11, 0x49, 0x6e, 0x74, 0x65, 0x67, 0x65, 0x72, - 0x41, 0x72, 0x72, 0x61, 0x79, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x12, 0x12, 0x0a, 0x04, 0x64, 0x61, - 0x74, 0x61, 0x18, 0x01, 0x20, 0x03, 0x28, 0x05, 0x52, 0x04, 0x64, 0x61, 0x74, 0x61, 0x22, 0x26, - 0x0a, 0x10, 0x44, 0x6f, 0x75, 0x62, 0x6c, 0x65, 0x41, 0x72, 0x72, 0x61, 0x79, 0x56, 0x61, 0x6c, - 0x75, 0x65, 0x12, 0x12, 0x0a, 0x04, 0x64, 0x61, 0x74, 0x61, 0x18, 0x01, 0x20, 0x03, 0x28, 0x01, - 0x52, 0x04, 0x64, 0x61, 0x74, 0x61, 0x22, 0x27, 0x0a, 0x11, 0x42, 0x6f, 0x6f, 0x6c, 0x65, 0x61, - 0x6e, 0x41, 0x72, 0x72, 0x61, 0x79, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x12, 0x12, 0x0a, 0x04, 0x64, - 0x61, 0x74, 0x61, 0x18, 0x01, 0x20, 0x03, 0x28, 0x08, 0x52, 0x04, 0x64, 0x61, 0x74, 0x61, 0x2a, - 0x5e, 0x0a, 0x0b, 0x43, 0x68, 0x65, 0x63, 0x6b, 0x52, 0x65, 0x73, 0x75, 0x6c, 0x74, 0x12, 0x1c, - 0x0a, 0x18, 0x43, 0x48, 0x45, 0x43, 0x4b, 0x5f, 0x52, 0x45, 0x53, 0x55, 0x4c, 0x54, 0x5f, 0x55, - 0x4e, 0x53, 0x50, 0x45, 0x43, 0x49, 0x46, 0x49, 0x45, 0x44, 0x10, 0x00, 0x12, 0x18, 0x0a, 0x14, - 0x43, 0x48, 0x45, 0x43, 0x4b, 0x5f, 0x52, 0x45, 0x53, 0x55, 0x4c, 0x54, 0x5f, 0x41, 0x4c, 0x4c, - 0x4f, 0x57, 0x45, 0x44, 0x10, 0x01, 0x12, 0x17, 0x0a, 0x13, 0x43, 0x48, 0x45, 0x43, 0x4b, 0x5f, - 0x52, 0x45, 0x53, 0x55, 0x4c, 0x54, 0x5f, 0x44, 0x45, 0x4e, 0x49, 0x45, 0x44, 0x10, 0x02, 0x2a, - 0xa3, 0x02, 0x0a, 0x0d, 0x41, 0x74, 0x74, 0x72, 0x69, 0x62, 0x75, 0x74, 0x65, 0x54, 0x79, 0x70, - 0x65, 0x12, 0x1e, 0x0a, 0x1a, 0x41, 0x54, 0x54, 0x52, 0x49, 0x42, 0x55, 0x54, 0x45, 0x5f, 0x54, - 0x59, 0x50, 0x45, 0x5f, 0x55, 0x4e, 0x53, 0x50, 0x45, 0x43, 0x49, 0x46, 0x49, 0x45, 0x44, 0x10, - 0x00, 0x12, 0x1a, 0x0a, 0x16, 0x41, 0x54, 0x54, 0x52, 0x49, 0x42, 0x55, 0x54, 0x45, 0x5f, 0x54, - 0x59, 0x50, 0x45, 0x5f, 0x42, 0x4f, 0x4f, 0x4c, 0x45, 0x41, 0x4e, 0x10, 0x01, 0x12, 0x20, 0x0a, - 0x1c, 0x41, 0x54, 0x54, 0x52, 0x49, 0x42, 0x55, 0x54, 0x45, 0x5f, 0x54, 0x59, 0x50, 0x45, 0x5f, - 0x42, 0x4f, 0x4f, 0x4c, 0x45, 0x41, 0x4e, 0x5f, 0x41, 0x52, 0x52, 0x41, 0x59, 0x10, 0x02, 0x12, - 0x19, 0x0a, 0x15, 0x41, 0x54, 0x54, 0x52, 0x49, 0x42, 0x55, 0x54, 0x45, 0x5f, 0x54, 0x59, 0x50, - 0x45, 0x5f, 0x53, 0x54, 0x52, 0x49, 0x4e, 0x47, 0x10, 0x03, 0x12, 0x1f, 0x0a, 0x1b, 0x41, 0x54, - 0x54, 0x52, 0x49, 0x42, 0x55, 0x54, 0x45, 0x5f, 0x54, 0x59, 0x50, 0x45, 0x5f, 0x53, 0x54, 0x52, - 0x49, 0x4e, 0x47, 0x5f, 0x41, 0x52, 0x52, 0x41, 0x59, 0x10, 0x04, 0x12, 0x1a, 0x0a, 0x16, 0x41, - 0x54, 0x54, 0x52, 0x49, 0x42, 0x55, 0x54, 0x45, 0x5f, 0x54, 0x59, 0x50, 0x45, 0x5f, 0x49, 0x4e, - 0x54, 0x45, 0x47, 0x45, 0x52, 0x10, 0x05, 0x12, 0x20, 0x0a, 0x1c, 0x41, 0x54, 0x54, 0x52, 0x49, + 0x01, 0x28, 0x08, 0x52, 0x04, 0x64, 0x61, 0x74, 0x61, 0x22, 0x26, 0x0a, 0x10, 0x53, 0x74, 0x72, + 0x69, 0x6e, 0x67, 0x41, 0x72, 0x72, 0x61, 0x79, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x12, 0x12, 0x0a, + 0x04, 0x64, 0x61, 0x74, 0x61, 0x18, 0x01, 0x20, 0x03, 0x28, 0x09, 0x52, 0x04, 0x64, 0x61, 0x74, + 0x61, 0x22, 0x27, 0x0a, 0x11, 0x49, 0x6e, 0x74, 0x65, 0x67, 0x65, 0x72, 0x41, 0x72, 0x72, 0x61, + 0x79, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x12, 0x12, 0x0a, 0x04, 0x64, 0x61, 0x74, 0x61, 0x18, 0x01, + 0x20, 0x03, 0x28, 0x05, 0x52, 0x04, 0x64, 0x61, 0x74, 0x61, 0x22, 0x26, 0x0a, 0x10, 0x44, 0x6f, + 0x75, 0x62, 0x6c, 0x65, 0x41, 0x72, 0x72, 0x61, 0x79, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x12, 0x12, + 0x0a, 0x04, 0x64, 0x61, 0x74, 0x61, 0x18, 0x01, 0x20, 0x03, 0x28, 0x01, 0x52, 0x04, 0x64, 0x61, + 0x74, 0x61, 0x22, 0x27, 0x0a, 0x11, 0x42, 0x6f, 0x6f, 0x6c, 0x65, 0x61, 0x6e, 0x41, 0x72, 0x72, + 0x61, 0x79, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x12, 0x12, 0x0a, 0x04, 0x64, 0x61, 0x74, 0x61, 0x18, + 0x01, 0x20, 0x03, 0x28, 0x08, 0x52, 0x04, 0x64, 0x61, 0x74, 0x61, 0x2a, 0x5e, 0x0a, 0x0b, 0x43, + 0x68, 0x65, 0x63, 0x6b, 0x52, 0x65, 0x73, 0x75, 0x6c, 0x74, 0x12, 0x1c, 0x0a, 0x18, 0x43, 0x48, + 0x45, 0x43, 0x4b, 0x5f, 0x52, 0x45, 0x53, 0x55, 0x4c, 0x54, 0x5f, 0x55, 0x4e, 0x53, 0x50, 0x45, + 0x43, 0x49, 0x46, 0x49, 0x45, 0x44, 0x10, 0x00, 0x12, 0x18, 0x0a, 0x14, 0x43, 0x48, 0x45, 0x43, + 0x4b, 0x5f, 0x52, 0x45, 0x53, 0x55, 0x4c, 0x54, 0x5f, 0x41, 0x4c, 0x4c, 0x4f, 0x57, 0x45, 0x44, + 0x10, 0x01, 0x12, 0x17, 0x0a, 0x13, 0x43, 0x48, 0x45, 0x43, 0x4b, 0x5f, 0x52, 0x45, 0x53, 0x55, + 0x4c, 0x54, 0x5f, 0x44, 0x45, 0x4e, 0x49, 0x45, 0x44, 0x10, 0x02, 0x2a, 0xa3, 0x02, 0x0a, 0x0d, + 0x41, 0x74, 0x74, 0x72, 0x69, 0x62, 0x75, 0x74, 0x65, 0x54, 0x79, 0x70, 0x65, 0x12, 0x1e, 0x0a, + 0x1a, 0x41, 0x54, 0x54, 0x52, 0x49, 0x42, 0x55, 0x54, 0x45, 0x5f, 0x54, 0x59, 0x50, 0x45, 0x5f, + 0x55, 0x4e, 0x53, 0x50, 0x45, 0x43, 0x49, 0x46, 0x49, 0x45, 0x44, 0x10, 0x00, 0x12, 0x1a, 0x0a, + 0x16, 0x41, 0x54, 0x54, 0x52, 0x49, 0x42, 0x55, 0x54, 0x45, 0x5f, 0x54, 0x59, 0x50, 0x45, 0x5f, + 0x42, 0x4f, 0x4f, 0x4c, 0x45, 0x41, 0x4e, 0x10, 0x01, 0x12, 0x20, 0x0a, 0x1c, 0x41, 0x54, 0x54, + 0x52, 0x49, 0x42, 0x55, 0x54, 0x45, 0x5f, 0x54, 0x59, 0x50, 0x45, 0x5f, 0x42, 0x4f, 0x4f, 0x4c, + 0x45, 0x41, 0x4e, 0x5f, 0x41, 0x52, 0x52, 0x41, 0x59, 0x10, 0x02, 0x12, 0x19, 0x0a, 0x15, 0x41, + 0x54, 0x54, 0x52, 0x49, 0x42, 0x55, 0x54, 0x45, 0x5f, 0x54, 0x59, 0x50, 0x45, 0x5f, 0x53, 0x54, + 0x52, 0x49, 0x4e, 0x47, 0x10, 0x03, 0x12, 0x1f, 0x0a, 0x1b, 0x41, 0x54, 0x54, 0x52, 0x49, 0x42, + 0x55, 0x54, 0x45, 0x5f, 0x54, 0x59, 0x50, 0x45, 0x5f, 0x53, 0x54, 0x52, 0x49, 0x4e, 0x47, 0x5f, + 0x41, 0x52, 0x52, 0x41, 0x59, 0x10, 0x04, 0x12, 0x1a, 0x0a, 0x16, 0x41, 0x54, 0x54, 0x52, 0x49, 0x42, 0x55, 0x54, 0x45, 0x5f, 0x54, 0x59, 0x50, 0x45, 0x5f, 0x49, 0x4e, 0x54, 0x45, 0x47, 0x45, - 0x52, 0x5f, 0x41, 0x52, 0x52, 0x41, 0x59, 0x10, 0x06, 0x12, 0x19, 0x0a, 0x15, 0x41, 0x54, 0x54, - 0x52, 0x49, 0x42, 0x55, 0x54, 0x45, 0x5f, 0x54, 0x59, 0x50, 0x45, 0x5f, 0x44, 0x4f, 0x55, 0x42, - 0x4c, 0x45, 0x10, 0x07, 0x12, 0x1f, 0x0a, 0x1b, 0x41, 0x54, 0x54, 0x52, 0x49, 0x42, 0x55, 0x54, - 0x45, 0x5f, 0x54, 0x59, 0x50, 0x45, 0x5f, 0x44, 0x4f, 0x55, 0x42, 0x4c, 0x45, 0x5f, 0x41, 0x52, - 0x52, 0x41, 0x59, 0x10, 0x08, 0x42, 0x87, 0x01, 0x0a, 0x0b, 0x63, 0x6f, 0x6d, 0x2e, 0x62, 0x61, - 0x73, 0x65, 0x2e, 0x76, 0x31, 0x42, 0x09, 0x42, 0x61, 0x73, 0x65, 0x50, 0x72, 0x6f, 0x74, 0x6f, - 0x50, 0x01, 0x5a, 0x30, 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x50, - 0x65, 0x72, 0x6d, 0x69, 0x66, 0x79, 0x2f, 0x70, 0x65, 0x72, 0x6d, 0x69, 0x66, 0x79, 0x2f, 0x70, - 0x6b, 0x67, 0x2f, 0x70, 0x62, 0x2f, 0x62, 0x61, 0x73, 0x65, 0x2f, 0x76, 0x31, 0x3b, 0x62, 0x61, - 0x73, 0x65, 0x76, 0x31, 0xa2, 0x02, 0x03, 0x42, 0x58, 0x58, 0xaa, 0x02, 0x07, 0x42, 0x61, 0x73, - 0x65, 0x2e, 0x56, 0x31, 0xca, 0x02, 0x07, 0x42, 0x61, 0x73, 0x65, 0x5c, 0x56, 0x31, 0xe2, 0x02, - 0x13, 0x42, 0x61, 0x73, 0x65, 0x5c, 0x56, 0x31, 0x5c, 0x47, 0x50, 0x42, 0x4d, 0x65, 0x74, 0x61, - 0x64, 0x61, 0x74, 0x61, 0xea, 0x02, 0x08, 0x42, 0x61, 0x73, 0x65, 0x3a, 0x3a, 0x56, 0x31, 0x62, - 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, + 0x52, 0x10, 0x05, 0x12, 0x20, 0x0a, 0x1c, 0x41, 0x54, 0x54, 0x52, 0x49, 0x42, 0x55, 0x54, 0x45, + 0x5f, 0x54, 0x59, 0x50, 0x45, 0x5f, 0x49, 0x4e, 0x54, 0x45, 0x47, 0x45, 0x52, 0x5f, 0x41, 0x52, + 0x52, 0x41, 0x59, 0x10, 0x06, 0x12, 0x19, 0x0a, 0x15, 0x41, 0x54, 0x54, 0x52, 0x49, 0x42, 0x55, + 0x54, 0x45, 0x5f, 0x54, 0x59, 0x50, 0x45, 0x5f, 0x44, 0x4f, 0x55, 0x42, 0x4c, 0x45, 0x10, 0x07, + 0x12, 0x1f, 0x0a, 0x1b, 0x41, 0x54, 0x54, 0x52, 0x49, 0x42, 0x55, 0x54, 0x45, 0x5f, 0x54, 0x59, + 0x50, 0x45, 0x5f, 0x44, 0x4f, 0x55, 0x42, 0x4c, 0x45, 0x5f, 0x41, 0x52, 0x52, 0x41, 0x59, 0x10, + 0x08, 0x42, 0x87, 0x01, 0x0a, 0x0b, 0x63, 0x6f, 0x6d, 0x2e, 0x62, 0x61, 0x73, 0x65, 0x2e, 0x76, + 0x31, 0x42, 0x09, 0x42, 0x61, 0x73, 0x65, 0x50, 0x72, 0x6f, 0x74, 0x6f, 0x50, 0x01, 0x5a, 0x30, + 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x50, 0x65, 0x72, 0x6d, 0x69, + 0x66, 0x79, 0x2f, 0x70, 0x65, 0x72, 0x6d, 0x69, 0x66, 0x79, 0x2f, 0x70, 0x6b, 0x67, 0x2f, 0x70, + 0x62, 0x2f, 0x62, 0x61, 0x73, 0x65, 0x2f, 0x76, 0x31, 0x3b, 0x62, 0x61, 0x73, 0x65, 0x76, 0x31, + 0xa2, 0x02, 0x03, 0x42, 0x58, 0x58, 0xaa, 0x02, 0x07, 0x42, 0x61, 0x73, 0x65, 0x2e, 0x56, 0x31, + 0xca, 0x02, 0x07, 0x42, 0x61, 0x73, 0x65, 0x5c, 0x56, 0x31, 0xe2, 0x02, 0x13, 0x42, 0x61, 0x73, + 0x65, 0x5c, 0x56, 0x31, 0x5c, 0x47, 0x50, 0x42, 0x4d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, + 0xea, 0x02, 0x08, 0x42, 0x61, 0x73, 0x65, 0x3a, 0x3a, 0x56, 0x31, 0x62, 0x06, 0x70, 0x72, 0x6f, + 0x74, 0x6f, 0x33, } var ( diff --git a/pkg/pb/base/v1/base.pb.validate.go b/pkg/pb/base/v1/base.pb.validate.go index 4ae72b4a9..2fc03edda 100644 --- a/pkg/pb/base/v1/base.pb.validate.go +++ b/pkg/pb/base/v1/base.pb.validate.go @@ -4240,17 +4240,6 @@ func (m *AttributeFilter) validate(all bool) error { var errors []error - if m.GetEntity() == nil { - err := AttributeFilterValidationError{ - field: "Entity", - reason: "value is required", - } - if !all { - return err - } - errors = append(errors, err) - } - if all { switch v := interface{}(m.GetEntity()).(type) { case interface{ ValidateAll() error }: @@ -4380,17 +4369,6 @@ func (m *TupleFilter) validate(all bool) error { var errors []error - if m.GetEntity() == nil { - err := TupleFilterValidationError{ - field: "Entity", - reason: "value is required", - } - if !all { - return err - } - errors = append(errors, err) - } - if all { switch v := interface{}(m.GetEntity()).(type) { case interface{ ValidateAll() error }: diff --git a/proto/base/v1/base.proto b/proto/base/v1/base.proto index a840ef0a2..9870275cb 100644 --- a/proto/base/v1/base.proto +++ b/proto/base/v1/base.proto @@ -394,8 +394,7 @@ message Subject { // AttributeFilter is used to filter attributes based on the entity and attribute names. message AttributeFilter { EntityFilter entity = 1 [ - json_name = "entity", - (validate.rules).message.required = true + json_name = "entity" ]; repeated string attributes = 2 [json_name = "attributes"]; // Names of the attributes to be filtered @@ -404,8 +403,7 @@ message AttributeFilter { // TupleFilter is used to filter tuples based on the entity, relation and the subject. message TupleFilter { EntityFilter entity = 1 [ - json_name = "entity", - (validate.rules).message.required = true + json_name = "entity" ]; string relation = 2 [ From 91d8439146253e665bd9dead9f10cdfc0e1ff86f Mon Sep 17 00:00:00 2001 From: Tolga Ozen Date: Wed, 1 Nov 2023 21:31:11 +0300 Subject: [PATCH 3/3] feat(#787): overwritable data write api --- internal/storage/postgres/dataWriter.go | 243 ++++++++++++++++-------- 1 file changed, 167 insertions(+), 76 deletions(-) diff --git a/internal/storage/postgres/dataWriter.go b/internal/storage/postgres/dataWriter.go index 926fdcb31..199e70199 100644 --- a/internal/storage/postgres/dataWriter.go +++ b/internal/storage/postgres/dataWriter.go @@ -14,6 +14,7 @@ import ( "github.com/Permify/permify/internal/storage/postgres/snapshot" "github.com/Permify/permify/internal/storage/postgres/types" "github.com/Permify/permify/internal/storage/postgres/utils" + "github.com/Permify/permify/internal/validation" "github.com/Permify/permify/pkg/database" db "github.com/Permify/permify/pkg/database/postgres" base "github.com/Permify/permify/pkg/pb/base/v1" @@ -56,9 +57,31 @@ func (w *DataWriter) Write(ctx context.Context, tenantID string, tupleCollection return nil, err } + transaction := w.database.Builder.Insert("transactions"). + Columns("tenant_id"). + Values(tenantID). + Suffix("RETURNING id").RunWith(tx) + if err != nil { + utils.Rollback(tx) + span.RecordError(err) + span.SetStatus(otelCodes.Error, err.Error()) + return nil, errors.New(base.ErrorCode_ERROR_CODE_SQL_BUILDER.String()) + } + + var xid types.XID8 + err = transaction.QueryRowContext(ctx).Scan(&xid) + if err != nil { + utils.Rollback(tx) + span.RecordError(err) + span.SetStatus(otelCodes.Error, err.Error()) + return nil, errors.New(base.ErrorCode_ERROR_CODE_EXECUTION.String()) + } + if len(tupleCollection.GetTuples()) > 0 { - tuplesInsertBuilder := w.database.Builder.Insert(RelationTuplesTable).Columns("entity_type, entity_id, relation, subject_type, subject_id, subject_relation, tenant_id").Suffix("ON CONFLICT DO NOTHING") + tuplesInsertBuilder := w.database.Builder.Insert(RelationTuplesTable).Columns("entity_type, entity_id, relation, subject_type, subject_id, subject_relation, created_tx_id, tenant_id") + + deleteClauses := squirrel.Or{} titer := tupleCollection.CreateTupleIterator() for titer.HasNext() { @@ -67,13 +90,32 @@ func (w *DataWriter) Write(ctx context.Context, tenantID string, tupleCollection if srelation == tuple.ELLIPSIS { srelation = "" } - tuplesInsertBuilder = tuplesInsertBuilder.Values(t.GetEntity().GetType(), t.GetEntity().GetId(), t.GetRelation(), t.GetSubject().GetType(), t.GetSubject().GetId(), srelation, tenantID) + + // Build the condition for this tuple. + condition := squirrel.Eq{ + "entity_type": t.GetEntity().GetType(), + "entity_id": t.GetEntity().GetId(), + "relation": t.GetRelation(), + "subject_type": t.GetSubject().GetType(), + "subject_id": t.GetSubject().GetId(), + "subject_relation": srelation, + } + + // Add the condition to the OR slice. + deleteClauses = append(deleteClauses, condition) + + tuplesInsertBuilder = tuplesInsertBuilder.Values(t.GetEntity().GetType(), t.GetEntity().GetId(), t.GetRelation(), t.GetSubject().GetType(), t.GetSubject().GetId(), srelation, xid, tenantID) } - var tquery string - var targs []interface{} + tDeleteBuilder := w.database.Builder.Update(RelationTuplesTable).Set("expired_tx_id", xid).Where(squirrel.Eq{ + "expired_tx_id": "0", + "tenant_id": tenantID, + }).Where(deleteClauses) + + var tdquery string + var tdargs []interface{} - tquery, targs, err = tuplesInsertBuilder.ToSql() + tdquery, tdargs, err = tDeleteBuilder.ToSql() if err != nil { utils.Rollback(tx) span.RecordError(err) @@ -81,7 +123,7 @@ func (w *DataWriter) Write(ctx context.Context, tenantID string, tupleCollection return nil, errors.New(base.ErrorCode_ERROR_CODE_SQL_BUILDER.String()) } - _, err = tx.ExecContext(ctx, tquery, targs...) + _, err = tx.ExecContext(ctx, tdquery, tdargs...) if err != nil { utils.Rollback(tx) span.RecordError(err) @@ -92,11 +134,34 @@ func (w *DataWriter) Write(ctx context.Context, tenantID string, tupleCollection return nil, errors.New(base.ErrorCode_ERROR_CODE_EXECUTION.String()) } + var tiquery string + var tiargs []interface{} + + tiquery, tiargs, err = tuplesInsertBuilder.ToSql() + if err != nil { + utils.Rollback(tx) + span.RecordError(err) + span.SetStatus(otelCodes.Error, err.Error()) + return nil, errors.New(base.ErrorCode_ERROR_CODE_SQL_BUILDER.String()) + } + + _, err = tx.ExecContext(ctx, tiquery, tiargs...) + if err != nil { + utils.Rollback(tx) + span.RecordError(err) + span.SetStatus(otelCodes.Error, err.Error()) + if strings.Contains(err.Error(), "could not serialize") { + continue + } + return nil, errors.New(base.ErrorCode_ERROR_CODE_EXECUTION.String()) + } } if len(attributeCollection.GetAttributes()) > 0 { - attributesInsertBuilder := w.database.Builder.Insert(AttributesTable).Columns("entity_type, entity_id, attribute, value, tenant_id").Suffix("ON CONFLICT DO NOTHING") + attributesInsertBuilder := w.database.Builder.Insert(AttributesTable).Columns("entity_type, entity_id, attribute, value, created_tx_id, tenant_id") + + deleteClauses := squirrel.Or{} aiter := attributeCollection.CreateAttributeIterator() for aiter.HasNext() { @@ -111,7 +176,44 @@ func (w *DataWriter) Write(ctx context.Context, tenantID string, tupleCollection return nil, errors.New(base.ErrorCode_ERROR_CODE_INVALID_ARGUMENT.String()) } - attributesInsertBuilder = attributesInsertBuilder.Values(a.GetEntity().GetType(), a.GetEntity().GetId(), a.GetAttribute(), jsonStr, tenantID) + // Build the condition for this tuple. + condition := squirrel.Eq{ + "entity_type": a.GetEntity().GetType(), + "entity_id": a.GetEntity().GetId(), + "attribute": a.GetAttribute(), + } + + // Add the condition to the OR slice. + deleteClauses = append(deleteClauses, condition) + + attributesInsertBuilder = attributesInsertBuilder.Values(a.GetEntity().GetType(), a.GetEntity().GetId(), a.GetAttribute(), jsonStr, xid, tenantID) + } + + tDeleteBuilder := w.database.Builder.Update(AttributesTable).Set("expired_tx_id", xid).Where(squirrel.Eq{ + "expired_tx_id": "0", + "tenant_id": tenantID, + }).Where(deleteClauses) + + var adquery string + var adargs []interface{} + + adquery, adargs, err = tDeleteBuilder.ToSql() + if err != nil { + utils.Rollback(tx) + span.RecordError(err) + span.SetStatus(otelCodes.Error, err.Error()) + return nil, errors.New(base.ErrorCode_ERROR_CODE_SQL_BUILDER.String()) + } + + _, err = tx.ExecContext(ctx, adquery, adargs...) + if err != nil { + utils.Rollback(tx) + span.RecordError(err) + span.SetStatus(otelCodes.Error, err.Error()) + if strings.Contains(err.Error(), "could not serialize") { + continue + } + return nil, errors.New(base.ErrorCode_ERROR_CODE_EXECUTION.String()) } var aquery string @@ -135,33 +237,15 @@ func (w *DataWriter) Write(ctx context.Context, tenantID string, tupleCollection } return nil, errors.New(base.ErrorCode_ERROR_CODE_EXECUTION.String()) } - - } - - transaction := w.database.Builder.Insert("transactions"). - Columns("tenant_id"). - Values(tenantID). - Suffix("RETURNING id").RunWith(tx) - if err != nil { - utils.Rollback(tx) - span.RecordError(err) - span.SetStatus(otelCodes.Error, err.Error()) - return nil, errors.New(base.ErrorCode_ERROR_CODE_SQL_BUILDER.String()) - } - - var xid types.XID8 - err = transaction.QueryRowContext(ctx).Scan(&xid) - if err != nil { - utils.Rollback(tx) - span.RecordError(err) - span.SetStatus(otelCodes.Error, err.Error()) - return nil, errors.New(base.ErrorCode_ERROR_CODE_EXECUTION.String()) } if err = tx.Commit(); err != nil { utils.Rollback(tx) span.RecordError(err) span.SetStatus(otelCodes.Error, err.Error()) + if strings.Contains(err.Error(), "could not serialize") { + continue + } return nil, errors.New(base.ErrorCode_ERROR_CODE_EXECUTION.String()) } @@ -184,13 +268,10 @@ func (w *DataWriter) Delete(ctx context.Context, tenantID string, tupleFilter *b return nil, err } - tbuilder := w.database.Builder.Update(RelationTuplesTable).Set("expired_tx_id", squirrel.Expr("pg_current_xact_id()")).Where(squirrel.Eq{"expired_tx_id": "0"}) - tbuilder = utils.TuplesFilterQueryForUpdateBuilder(tbuilder, tupleFilter) - - var tquery string - var targs []interface{} - - tquery, targs, err = tbuilder.ToSql() + transaction := w.database.Builder.Insert("transactions"). + Columns("tenant_id"). + Values(tenantID). + Suffix("RETURNING id").RunWith(tx) if err != nil { utils.Rollback(tx) span.RecordError(err) @@ -198,66 +279,76 @@ func (w *DataWriter) Delete(ctx context.Context, tenantID string, tupleFilter *b return nil, errors.New(base.ErrorCode_ERROR_CODE_SQL_BUILDER.String()) } - _, err = tx.ExecContext(ctx, tquery, targs...) + var xid types.XID8 + err = transaction.QueryRowContext(ctx).Scan(&xid) if err != nil { utils.Rollback(tx) span.RecordError(err) span.SetStatus(otelCodes.Error, err.Error()) - if strings.Contains(err.Error(), "could not serialize") { - continue - } return nil, errors.New(base.ErrorCode_ERROR_CODE_EXECUTION.String()) } - abuilder := w.database.Builder.Update(AttributesTable).Set("expired_tx_id", squirrel.Expr("pg_current_xact_id()")).Where(squirrel.Eq{"expired_tx_id": "0"}) - abuilder = utils.AttributesFilterQueryForUpdateBuilder(abuilder, attributeFilter) + if !validation.IsTupleFilterEmpty(tupleFilter) { + tbuilder := w.database.Builder.Update(RelationTuplesTable).Set("expired_tx_id", xid).Where(squirrel.Eq{"expired_tx_id": "0"}) + tbuilder = utils.TuplesFilterQueryForUpdateBuilder(tbuilder, tupleFilter) - var aquery string - var aargs []interface{} + var tquery string + var targs []interface{} - aquery, aargs, err = abuilder.ToSql() - if err != nil { - utils.Rollback(tx) - span.RecordError(err) - span.SetStatus(otelCodes.Error, err.Error()) - return nil, errors.New(base.ErrorCode_ERROR_CODE_SQL_BUILDER.String()) - } + tquery, targs, err = tbuilder.ToSql() + if err != nil { + utils.Rollback(tx) + span.RecordError(err) + span.SetStatus(otelCodes.Error, err.Error()) + return nil, errors.New(base.ErrorCode_ERROR_CODE_SQL_BUILDER.String()) + } - _, err = tx.ExecContext(ctx, aquery, aargs...) - if err != nil { - utils.Rollback(tx) - span.RecordError(err) - span.SetStatus(otelCodes.Error, err.Error()) - if strings.Contains(err.Error(), "could not serialize") { - continue + _, err = tx.ExecContext(ctx, tquery, targs...) + if err != nil { + utils.Rollback(tx) + span.RecordError(err) + span.SetStatus(otelCodes.Error, err.Error()) + if strings.Contains(err.Error(), "could not serialize") { + continue + } + return nil, errors.New(base.ErrorCode_ERROR_CODE_EXECUTION.String()) } - return nil, errors.New(base.ErrorCode_ERROR_CODE_EXECUTION.String()) } - transaction := w.database.Builder.Insert("transactions"). - Columns("tenant_id"). - Values(tenantID). - Suffix("RETURNING id").RunWith(tx) - if err != nil { - utils.Rollback(tx) - span.RecordError(err) - span.SetStatus(otelCodes.Error, err.Error()) - return nil, errors.New(base.ErrorCode_ERROR_CODE_SQL_BUILDER.String()) - } + if !validation.IsAttributeFilterEmpty(attributeFilter) { + abuilder := w.database.Builder.Update(AttributesTable).Set("expired_tx_id", xid).Where(squirrel.Eq{"expired_tx_id": "0"}) + abuilder = utils.AttributesFilterQueryForUpdateBuilder(abuilder, attributeFilter) - var xid types.XID8 - err = transaction.QueryRowContext(ctx).Scan(&xid) - if err != nil { - utils.Rollback(tx) - span.RecordError(err) - span.SetStatus(otelCodes.Error, err.Error()) - return nil, errors.New(base.ErrorCode_ERROR_CODE_EXECUTION.String()) + var aquery string + var aargs []interface{} + + aquery, aargs, err = abuilder.ToSql() + if err != nil { + utils.Rollback(tx) + span.RecordError(err) + span.SetStatus(otelCodes.Error, err.Error()) + return nil, errors.New(base.ErrorCode_ERROR_CODE_SQL_BUILDER.String()) + } + + _, err = tx.ExecContext(ctx, aquery, aargs...) + if err != nil { + utils.Rollback(tx) + span.RecordError(err) + span.SetStatus(otelCodes.Error, err.Error()) + if strings.Contains(err.Error(), "could not serialize") { + continue + } + return nil, errors.New(base.ErrorCode_ERROR_CODE_EXECUTION.String()) + } } if err = tx.Commit(); err != nil { utils.Rollback(tx) span.RecordError(err) span.SetStatus(otelCodes.Error, err.Error()) + if strings.Contains(err.Error(), "could not serialize") { + continue + } return nil, err }