diff --git a/core/subscription/mock_subscription/mock_Service.go b/core/subscription/mock_subscription/mock_Service.go index 4ac90492d2..785a642b36 100644 --- a/core/subscription/mock_subscription/mock_Service.go +++ b/core/subscription/mock_subscription/mock_Service.go @@ -7,8 +7,6 @@ import ( app "github.com/anyproto/any-sync/app" - domain "github.com/anyproto/anytype-heart/core/domain" - mock "github.com/stretchr/testify/mock" pb "github.com/anyproto/anytype-heart/pb" @@ -328,65 +326,6 @@ func (_c *MockService_SubscribeGroups_Call) RunAndReturn(run func(subscription.S return _c } -// SubscribeIds provides a mock function with given fields: subId, ids -func (_m *MockService) SubscribeIds(subId string, ids []string) ([]*domain.GenericMap[domain.RelationKey], error) { - ret := _m.Called(subId, ids) - - if len(ret) == 0 { - panic("no return value specified for SubscribeIds") - } - - var r0 []*domain.GenericMap[domain.RelationKey] - var r1 error - if rf, ok := ret.Get(0).(func(string, []string) ([]*domain.GenericMap[domain.RelationKey], error)); ok { - return rf(subId, ids) - } - if rf, ok := ret.Get(0).(func(string, []string) []*domain.GenericMap[domain.RelationKey]); ok { - r0 = rf(subId, ids) - } else { - if ret.Get(0) != nil { - r0 = ret.Get(0).([]*domain.GenericMap[domain.RelationKey]) - } - } - - if rf, ok := ret.Get(1).(func(string, []string) error); ok { - r1 = rf(subId, ids) - } else { - r1 = ret.Error(1) - } - - return r0, r1 -} - -// MockService_SubscribeIds_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'SubscribeIds' -type MockService_SubscribeIds_Call struct { - *mock.Call -} - -// SubscribeIds is a helper method to define mock.On call -// - subId string -// - ids []string -func (_e *MockService_Expecter) SubscribeIds(subId interface{}, ids interface{}) *MockService_SubscribeIds_Call { - return &MockService_SubscribeIds_Call{Call: _e.mock.On("SubscribeIds", subId, ids)} -} - -func (_c *MockService_SubscribeIds_Call) Run(run func(subId string, ids []string)) *MockService_SubscribeIds_Call { - _c.Call.Run(func(args mock.Arguments) { - run(args[0].(string), args[1].([]string)) - }) - return _c -} - -func (_c *MockService_SubscribeIds_Call) Return(records []*domain.GenericMap[domain.RelationKey], err error) *MockService_SubscribeIds_Call { - _c.Call.Return(records, err) - return _c -} - -func (_c *MockService_SubscribeIds_Call) RunAndReturn(run func(string, []string) ([]*domain.GenericMap[domain.RelationKey], error)) *MockService_SubscribeIds_Call { - _c.Call.Return(run) - return _c -} - // SubscribeIdsReq provides a mock function with given fields: req func (_m *MockService) SubscribeIdsReq(req pb.RpcObjectSubscribeIdsRequest) (*pb.RpcObjectSubscribeIdsResponse, error) { ret := _m.Called(req) diff --git a/core/subscription/service.go b/core/subscription/service.go index c3555e860c..e77d6a6258 100644 --- a/core/subscription/service.go +++ b/core/subscription/service.go @@ -98,7 +98,6 @@ type SubscribeResponse struct { type Service interface { Search(req SubscribeRequest) (resp *SubscribeResponse, err error) SubscribeIdsReq(req pb.RpcObjectSubscribeIdsRequest) (resp *pb.RpcObjectSubscribeIdsResponse, err error) - SubscribeIds(subId string, ids []string) (records []*domain.Details, err error) SubscribeGroups(req SubscribeGroupsRequest) (*pb.RpcObjectGroupsSubscribeResponse, error) Unsubscribe(subIds ...string) (err error) UnsubscribeAndReturnIds(spaceId string, subId string) ([]string, error) @@ -231,10 +230,6 @@ func (s *service) SubscribeIdsReq(req pb.RpcObjectSubscribeIdsRequest) (resp *pb return spaceSubs.SubscribeIdsReq(req) } -func (s *service) SubscribeIds(subId string, ids []string) (records []*domain.Details, err error) { - return -} - func (s *service) SubscribeGroups(req SubscribeGroupsRequest) (*pb.RpcObjectGroupsSubscribeResponse, error) { // todo: removed temp fix after we will have session-scoped subscriptions // this is to prevent multiple subscriptions with the same id in different spaces @@ -936,8 +931,8 @@ func (s *spaceSubscriptions) onChangeWithinContext(entries []*entry, proc func(c } func (s *spaceSubscriptions) filtersFromSource(sources []string) (database.Filter, error) { - var relTypeFilter database.FiltersOr var ( + relTypeFilter database.FiltersOr relKeys []string typeUniqueKeys []string ) @@ -974,13 +969,73 @@ func (s *spaceSubscriptions) filtersFromSource(sources []string) (database.Filte } for _, relKey := range relKeys { - relTypeFilter = append(relTypeFilter, database.FilterExists{ + existsFilter := database.FilterExists{ Key: domain.RelationKey(relKey), - }) + } + + typeIds, err := s.typesRecommendingRelation(relKey) + if err != nil || len(typeIds) == 0 { + relTypeFilter = append(relTypeFilter, existsFilter) + continue + } + + nestedTypeFilter, err := database.MakeFilter("", database.FilterRequest{ + RelationKey: bundle.RelationKeyType, + Condition: model.BlockContentDataviewFilter_In, + Value: domain.StringList(typeIds), + }, s.objectStore) + if err != nil { + relTypeFilter = append(relTypeFilter, existsFilter) + continue + } + + relTypeFilter = append(relTypeFilter, database.FiltersOr{existsFilter, nestedTypeFilter}) } return relTypeFilter, nil } +func (s *spaceSubscriptions) typesRecommendingRelation(relKey string) ([]string, error) { + uk, err := domain.NewUniqueKey(smartblock.SmartBlockTypeRelation, relKey) + if err != nil { + return nil, fmt.Errorf("create relation unique key: %w", err) + } + relDetails, err := s.objectStore.GetObjectByUniqueKey(uk) + if err != nil { + return nil, fmt.Errorf("get relation object: %w", err) + } + relId := relDetails.GetString(bundle.RelationKeyId) + if relId == "" { + return nil, fmt.Errorf("relation object has no id") + } + + records, err := s.objectStore.QueryRaw(&database.Filters{ + FilterObj: database.FiltersAnd{ + database.FilterEq{ + Key: bundle.RelationKeyResolvedLayout, + Cond: model.BlockContentDataviewFilter_Equal, + Value: domain.Int64(int64(model.ObjectType_objectType)), + }, + database.FiltersOr{ + database.FilterAllIn{Key: bundle.RelationKeyRecommendedRelations, Strings: []string{relId}}, + database.FilterAllIn{Key: bundle.RelationKeyRecommendedFeaturedRelations, Strings: []string{relId}}, + database.FilterAllIn{Key: bundle.RelationKeyRecommendedFileRelations, Strings: []string{relId}}, + database.FilterAllIn{Key: bundle.RelationKeyRecommendedHiddenRelations, Strings: []string{relId}}, + }, + }, + }, 0, 0) + if err != nil { + return nil, fmt.Errorf("query types recommending relation: %w", err) + } + + var typeIds []string + for _, rec := range records { + if typeId := rec.Details.GetString(bundle.RelationKeyId); typeId != "" { + typeIds = append(typeIds, typeId) + } + } + return typeIds, nil +} + func (s *spaceSubscriptions) depIdsFromFilter(filters []database.FilterRequest) (depIds []string) { for _, f := range filters { if s.ds.isRelationObject(f.RelationKey) { diff --git a/core/subscription/service_test.go b/core/subscription/service_test.go index a2205cbb73..d6362e682d 100644 --- a/core/subscription/service_test.go +++ b/core/subscription/service_test.go @@ -1276,6 +1276,88 @@ func TestService_Search(t *testing.T) { assert.Len(t, sub.Dependencies, 0) assert.Len(t, sub.Records, 0) }) + t.Run("relation source includes objects whose type recommends the relation", func(t *testing.T) { + // given + fx := newFixtureWithRealObjectStore(t) + defer fx.a.Close(context.Background()) + defer fx.ctrl.Finish() + + relationKey := "done" + relationUK, err := domain.NewUniqueKey(smartblock.SmartBlockTypeRelation, relationKey) + require.NoError(t, err) + + projectTypeUK, err := domain.NewUniqueKey(smartblock.SmartBlockTypeObjectType, "project") + require.NoError(t, err) + + noteTypeUK, err := domain.NewUniqueKey(smartblock.SmartBlockTypeObjectType, "note") + require.NoError(t, err) + + fx.store.AddObjects(t, testSpaceId, []objectstore.TestObject{ + // Relation object + { + bundle.RelationKeyId: domain.String("rel-done-id"), + bundle.RelationKeyUniqueKey: domain.String(relationUK.Marshal()), + bundle.RelationKeyRelationKey: domain.String(relationKey), + bundle.RelationKeyResolvedLayout: domain.Int64(int64(model.ObjectType_relation)), + }, + // Type "Project" that recommends the "done" relation + { + bundle.RelationKeyId: domain.String("type-project-id"), + bundle.RelationKeyUniqueKey: domain.String(projectTypeUK.Marshal()), + bundle.RelationKeyResolvedLayout: domain.Int64(int64(model.ObjectType_objectType)), + bundle.RelationKeyRecommendedRelations: domain.StringList([]string{"rel-done-id"}), + }, + // Type "Note" that does NOT recommend the "done" relation + { + bundle.RelationKeyId: domain.String("type-note-id"), + bundle.RelationKeyUniqueKey: domain.String(noteTypeUK.Marshal()), + bundle.RelationKeyResolvedLayout: domain.Int64(int64(model.ObjectType_objectType)), + }, + // Project object WITHOUT the "done" key in details (the bug case) + { + bundle.RelationKeyId: domain.String("project1"), + bundle.RelationKeyName: domain.String("Project 1"), + bundle.RelationKeyResolvedLayout: domain.Int64(int64(model.ObjectType_basic)), + bundle.RelationKeyType: domain.String("type-project-id"), + }, + // Project object WITH the "done" key set (backward compat) + { + bundle.RelationKeyId: domain.String("project2"), + bundle.RelationKeyName: domain.String("Project 2"), + bundle.RelationKeyResolvedLayout: domain.Int64(int64(model.ObjectType_basic)), + bundle.RelationKeyType: domain.String("type-project-id"), + domain.RelationKey(relationKey): domain.Bool(true), + }, + // Note object WITHOUT the "done" key (negative case — should not match) + { + bundle.RelationKeyId: domain.String("note1"), + bundle.RelationKeyName: domain.String("Note 1"), + bundle.RelationKeyResolvedLayout: domain.Int64(int64(model.ObjectType_basic)), + bundle.RelationKeyType: domain.String("type-note-id"), + }, + }) + + // when + resp, err := fx.Search(SubscribeRequest{ + SpaceId: testSpaceId, + SubId: "test-rel-source", + Keys: []string{bundle.RelationKeyId.String(), bundle.RelationKeyName.String()}, + Source: []string{relationUK.Marshal()}, + NoDepSubscription: true, + }) + + // then + require.NoError(t, err) + require.Len(t, resp.Records, 2) + + ids := make([]string, len(resp.Records)) + for i, r := range resp.Records { + ids[i] = r.GetString(bundle.RelationKeyId) + } + assert.Contains(t, ids, "project1") + assert.Contains(t, ids, "project2") + assert.NotContains(t, ids, "note1") + }) } func addTestObjects(t *testing.T, source string, relationKey domain.RelationKey, option1, option2, testSpaceId string, fx *fixtureRealStore) error {