mirror of
https://github.com/anyproto/anytype-heart.git
synced 2025-06-10 01:51:07 +09:00
Merge pull request #1536 from anyproto/go-3813-set-lastused-for-relations
GO-3813 Set lastUsedDate for relations
This commit is contained in:
commit
dcb9b6dc99
24 changed files with 518 additions and 193 deletions
|
@ -40,6 +40,7 @@ import (
|
|||
"github.com/anyproto/anytype-heart/core/block/collection"
|
||||
"github.com/anyproto/anytype-heart/core/block/editor"
|
||||
"github.com/anyproto/anytype-heart/core/block/editor/converter"
|
||||
"github.com/anyproto/anytype-heart/core/block/editor/lastused"
|
||||
"github.com/anyproto/anytype-heart/core/block/export"
|
||||
importer "github.com/anyproto/anytype-heart/core/block/import"
|
||||
"github.com/anyproto/anytype-heart/core/block/object/idresolver"
|
||||
|
@ -301,7 +302,8 @@ func Bootstrap(a *app.App, components ...app.Component) {
|
|||
Register(nameserviceclient.New()).
|
||||
Register(payments.New()).
|
||||
Register(paymentscache.New()).
|
||||
Register(peerstatus.New())
|
||||
Register(peerstatus.New()).
|
||||
Register(lastused.New())
|
||||
}
|
||||
|
||||
func MiddlewareVersion() string {
|
||||
|
|
|
@ -22,13 +22,23 @@ func (s *Service) SetDetails(ctx session.Context, objectId string, details []*mo
|
|||
})
|
||||
}
|
||||
|
||||
func (s *Service) SetDetailsAndUpdateLastUsed(ctx session.Context, objectId string, details []*model.Detail) (err error) {
|
||||
return cache.Do(s, objectId, func(b basic.DetailsSettable) error {
|
||||
return b.SetDetailsAndUpdateLastUsed(ctx, details, true)
|
||||
})
|
||||
}
|
||||
|
||||
func (s *Service) SetDetailsList(ctx session.Context, objectIds []string, details []*model.Detail) (err error) {
|
||||
var (
|
||||
resultError error
|
||||
anySucceed bool
|
||||
)
|
||||
for _, objectId := range objectIds {
|
||||
err := s.SetDetails(ctx, objectId, details)
|
||||
for i, objectId := range objectIds {
|
||||
setDetailsFunc := s.SetDetails
|
||||
if i == 0 {
|
||||
setDetailsFunc = s.SetDetailsAndUpdateLastUsed
|
||||
}
|
||||
err := setDetailsFunc(ctx, objectId, details)
|
||||
if err != nil {
|
||||
resultError = errors.Join(resultError, err)
|
||||
} else {
|
||||
|
@ -51,10 +61,20 @@ func (s *Service) ModifyDetails(objectId string, modifier func(current *types.St
|
|||
})
|
||||
}
|
||||
|
||||
func (s *Service) ModifyDetailsAndUpdateLastUsed(objectId string, modifier func(current *types.Struct) (*types.Struct, error)) (err error) {
|
||||
return cache.Do(s, objectId, func(du basic.DetailsUpdatable) error {
|
||||
return du.UpdateDetailsAndLastUsed(modifier)
|
||||
})
|
||||
}
|
||||
|
||||
func (s *Service) ModifyDetailsList(req *pb.RpcObjectListModifyDetailValuesRequest) (resultError error) {
|
||||
var anySucceed bool
|
||||
for _, objectId := range req.ObjectIds {
|
||||
err := s.ModifyDetails(objectId, func(current *types.Struct) (*types.Struct, error) {
|
||||
for i, objectId := range req.ObjectIds {
|
||||
modifyDetailsFunc := s.ModifyDetails
|
||||
if i == 0 {
|
||||
modifyDetailsFunc = s.ModifyDetailsAndUpdateLastUsed
|
||||
}
|
||||
err := modifyDetailsFunc(objectId, func(current *types.Struct) (*types.Struct, error) {
|
||||
for _, op := range req.Operations {
|
||||
if !pbtypes.IsEmptyValue(op.Set) {
|
||||
// Set operation has higher priority than Add and Remove, because it modifies full value
|
||||
|
|
|
@ -7,6 +7,7 @@ import (
|
|||
"github.com/samber/lo"
|
||||
|
||||
"github.com/anyproto/anytype-heart/core/block/editor/converter"
|
||||
"github.com/anyproto/anytype-heart/core/block/editor/lastused"
|
||||
"github.com/anyproto/anytype-heart/core/block/editor/smartblock"
|
||||
"github.com/anyproto/anytype-heart/core/block/editor/state"
|
||||
"github.com/anyproto/anytype-heart/core/block/editor/table"
|
||||
|
@ -65,10 +66,12 @@ type CommonOperations interface {
|
|||
|
||||
type DetailsSettable interface {
|
||||
SetDetails(ctx session.Context, details []*model.Detail, showEvent bool) (err error)
|
||||
SetDetailsAndUpdateLastUsed(ctx session.Context, details []*model.Detail, showEvent bool) (err error)
|
||||
}
|
||||
|
||||
type DetailsUpdatable interface {
|
||||
UpdateDetails(update func(current *types.Struct) (*types.Struct, error)) (err error)
|
||||
UpdateDetailsAndLastUsed(update func(current *types.Struct) (*types.Struct, error)) (err error)
|
||||
}
|
||||
|
||||
type Restrictionable interface {
|
||||
|
@ -104,12 +107,14 @@ func NewBasic(
|
|||
objectStore objectstore.ObjectStore,
|
||||
layoutConverter converter.LayoutConverter,
|
||||
fileObjectService fileobject.Service,
|
||||
lastUsedUpdater lastused.ObjectUsageUpdater,
|
||||
) AllOperations {
|
||||
return &basic{
|
||||
SmartBlock: sb,
|
||||
objectStore: objectStore,
|
||||
layoutConverter: layoutConverter,
|
||||
fileObjectService: fileObjectService,
|
||||
lastUsedUpdater: lastUsedUpdater,
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -119,6 +124,7 @@ type basic struct {
|
|||
objectStore objectstore.ObjectStore
|
||||
layoutConverter converter.LayoutConverter
|
||||
fileObjectService fileobject.Service
|
||||
lastUsedUpdater lastused.ObjectUsageUpdater
|
||||
}
|
||||
|
||||
func (bs *basic) CreateBlock(s *state.State, req pb.RpcBlockCreateRequest) (id string, err error) {
|
||||
|
|
|
@ -45,7 +45,7 @@ func TestBasic_Create(t *testing.T) {
|
|||
t.Run("generic", func(t *testing.T) {
|
||||
sb := smarttest.New("test")
|
||||
sb.AddBlock(simple.New(&model.Block{Id: "test"}))
|
||||
b := NewBasic(sb, nil, converter.NewLayoutConverter(), nil)
|
||||
b := NewBasic(sb, nil, converter.NewLayoutConverter(), nil, nil)
|
||||
st := sb.NewState()
|
||||
id, err := b.CreateBlock(st, pb.RpcBlockCreateRequest{
|
||||
Block: &model.Block{Content: &model.BlockContentOfText{Text: &model.BlockContentText{Text: "ll"}}},
|
||||
|
@ -59,7 +59,7 @@ func TestBasic_Create(t *testing.T) {
|
|||
sb := smarttest.New("test")
|
||||
sb.AddBlock(simple.New(&model.Block{Id: "test"}))
|
||||
require.NoError(t, smartblock.ObjectApplyTemplate(sb, sb.NewState(), template.WithTitle))
|
||||
b := NewBasic(sb, nil, converter.NewLayoutConverter(), nil)
|
||||
b := NewBasic(sb, nil, converter.NewLayoutConverter(), nil, nil)
|
||||
s := sb.NewState()
|
||||
id, err := b.CreateBlock(s, pb.RpcBlockCreateRequest{
|
||||
TargetId: template.TitleBlockId,
|
||||
|
@ -80,7 +80,7 @@ func TestBasic_Create(t *testing.T) {
|
|||
}
|
||||
sb.AddBlock(simple.New(&model.Block{Id: "test"}))
|
||||
require.NoError(t, smartblock.ObjectApplyTemplate(sb, sb.NewState(), template.WithTitle))
|
||||
b := NewBasic(sb, nil, converter.NewLayoutConverter(), nil)
|
||||
b := NewBasic(sb, nil, converter.NewLayoutConverter(), nil, nil)
|
||||
_, err := b.CreateBlock(sb.NewState(), pb.RpcBlockCreateRequest{})
|
||||
assert.ErrorIs(t, err, restriction.ErrRestricted)
|
||||
})
|
||||
|
@ -94,7 +94,7 @@ func TestBasic_Duplicate(t *testing.T) {
|
|||
AddBlock(simple.New(&model.Block{Id: "3"}))
|
||||
|
||||
st := sb.NewState()
|
||||
newIds, err := NewBasic(sb, nil, converter.NewLayoutConverter(), nil).Duplicate(st, st, "", 0, []string{"2"})
|
||||
newIds, err := NewBasic(sb, nil, converter.NewLayoutConverter(), nil, nil).Duplicate(st, st, "", 0, []string{"2"})
|
||||
require.NoError(t, err)
|
||||
|
||||
err = sb.Apply(st)
|
||||
|
@ -172,7 +172,7 @@ func TestBasic_Duplicate(t *testing.T) {
|
|||
ts.SetDetail(bundle.RelationKeySpaceId.String(), pbtypes.String(tc.spaceIds[1]))
|
||||
|
||||
// when
|
||||
newIds, err := NewBasic(source, nil, nil, tc.fos()).Duplicate(ss, ts, "target", model.Block_Inner, []string{"1", "f1"})
|
||||
newIds, err := NewBasic(source, nil, nil, tc.fos(), nil).Duplicate(ss, ts, "target", model.Block_Inner, []string{"1", "f1"})
|
||||
require.NoError(t, err)
|
||||
require.NoError(t, target.Apply(ts))
|
||||
|
||||
|
@ -206,7 +206,7 @@ func TestBasic_Unlink(t *testing.T) {
|
|||
AddBlock(simple.New(&model.Block{Id: "2", ChildrenIds: []string{"3"}})).
|
||||
AddBlock(simple.New(&model.Block{Id: "3"}))
|
||||
|
||||
b := NewBasic(sb, nil, converter.NewLayoutConverter(), nil)
|
||||
b := NewBasic(sb, nil, converter.NewLayoutConverter(), nil, nil)
|
||||
|
||||
err := b.Unlink(nil, "2")
|
||||
require.NoError(t, err)
|
||||
|
@ -220,7 +220,7 @@ func TestBasic_Unlink(t *testing.T) {
|
|||
AddBlock(simple.New(&model.Block{Id: "2", ChildrenIds: []string{"3"}})).
|
||||
AddBlock(simple.New(&model.Block{Id: "3"}))
|
||||
|
||||
b := NewBasic(sb, nil, converter.NewLayoutConverter(), nil)
|
||||
b := NewBasic(sb, nil, converter.NewLayoutConverter(), nil, nil)
|
||||
|
||||
err := b.Unlink(nil, "2", "3")
|
||||
require.NoError(t, err)
|
||||
|
@ -237,7 +237,7 @@ func TestBasic_Move(t *testing.T) {
|
|||
AddBlock(simple.New(&model.Block{Id: "3"})).
|
||||
AddBlock(simple.New(&model.Block{Id: "4"}))
|
||||
|
||||
b := NewBasic(sb, nil, converter.NewLayoutConverter(), nil)
|
||||
b := NewBasic(sb, nil, converter.NewLayoutConverter(), nil, nil)
|
||||
st := sb.NewState()
|
||||
|
||||
err := b.Move(st, st, "4", model.Block_Inner, []string{"3"})
|
||||
|
@ -251,7 +251,7 @@ func TestBasic_Move(t *testing.T) {
|
|||
sb := smarttest.New("test")
|
||||
sb.AddBlock(simple.New(&model.Block{Id: "test"}))
|
||||
require.NoError(t, smartblock.ObjectApplyTemplate(sb, sb.NewState(), template.WithTitle))
|
||||
b := NewBasic(sb, nil, converter.NewLayoutConverter(), nil)
|
||||
b := NewBasic(sb, nil, converter.NewLayoutConverter(), nil, nil)
|
||||
s := sb.NewState()
|
||||
id1, err := b.CreateBlock(s, pb.RpcBlockCreateRequest{
|
||||
TargetId: template.HeaderLayoutId,
|
||||
|
@ -300,7 +300,7 @@ func TestBasic_Move(t *testing.T) {
|
|||
},
|
||||
),
|
||||
)
|
||||
basic := NewBasic(testDoc, nil, converter.NewLayoutConverter(), nil)
|
||||
basic := NewBasic(testDoc, nil, converter.NewLayoutConverter(), nil, nil)
|
||||
state := testDoc.NewState()
|
||||
|
||||
// when
|
||||
|
@ -316,7 +316,7 @@ func TestBasic_Move(t *testing.T) {
|
|||
AddBlock(newTextBlock("1", "", nil)).
|
||||
AddBlock(newTextBlock("2", "one", nil))
|
||||
|
||||
b := NewBasic(sb, nil, converter.NewLayoutConverter(), nil)
|
||||
b := NewBasic(sb, nil, converter.NewLayoutConverter(), nil, nil)
|
||||
st := sb.NewState()
|
||||
err := b.Move(st, st, "1", model.Block_InnerFirst, []string{"2"})
|
||||
require.NoError(t, err)
|
||||
|
@ -336,7 +336,7 @@ func TestBasic_Move(t *testing.T) {
|
|||
AddBlock(firstBlock).
|
||||
AddBlock(secondBlock)
|
||||
|
||||
b := NewBasic(sb, nil, converter.NewLayoutConverter(), nil)
|
||||
b := NewBasic(sb, nil, converter.NewLayoutConverter(), nil, nil)
|
||||
st := sb.NewState()
|
||||
err := b.Move(st, st, "1", model.Block_InnerFirst, []string{"2"})
|
||||
require.NoError(t, err)
|
||||
|
@ -350,7 +350,7 @@ func TestBasic_Move(t *testing.T) {
|
|||
AddBlock(newTextBlock("1", "", nil)).
|
||||
AddBlock(newTextBlock("2", "one", nil))
|
||||
|
||||
b := NewBasic(sb, nil, converter.NewLayoutConverter(), nil)
|
||||
b := NewBasic(sb, nil, converter.NewLayoutConverter(), nil, nil)
|
||||
st := sb.NewState()
|
||||
err := b.Move(st, nil, "1", model.Block_Top, []string{"2"})
|
||||
require.NoError(t, err)
|
||||
|
@ -379,7 +379,7 @@ func TestBasic_MoveTableBlocks(t *testing.T) {
|
|||
t.Run("moving non-root table block '"+block+"' leads to error", func(t *testing.T) {
|
||||
// given
|
||||
sb := getSB()
|
||||
b := NewBasic(sb, nil, converter.NewLayoutConverter(), nil)
|
||||
b := NewBasic(sb, nil, converter.NewLayoutConverter(), nil, nil)
|
||||
st := sb.NewState()
|
||||
|
||||
// when
|
||||
|
@ -394,7 +394,7 @@ func TestBasic_MoveTableBlocks(t *testing.T) {
|
|||
t.Run("no error on moving root table block", func(t *testing.T) {
|
||||
// given
|
||||
sb := getSB()
|
||||
b := NewBasic(sb, nil, converter.NewLayoutConverter(), nil)
|
||||
b := NewBasic(sb, nil, converter.NewLayoutConverter(), nil, nil)
|
||||
st := sb.NewState()
|
||||
|
||||
// when
|
||||
|
@ -408,7 +408,7 @@ func TestBasic_MoveTableBlocks(t *testing.T) {
|
|||
t.Run("no error on moving one row between another", func(t *testing.T) {
|
||||
// given
|
||||
sb := getSB()
|
||||
b := NewBasic(sb, nil, converter.NewLayoutConverter(), nil)
|
||||
b := NewBasic(sb, nil, converter.NewLayoutConverter(), nil, nil)
|
||||
st := sb.NewState()
|
||||
|
||||
// when
|
||||
|
@ -422,7 +422,7 @@ func TestBasic_MoveTableBlocks(t *testing.T) {
|
|||
t.Run("moving rows with incorrect position leads to error", func(t *testing.T) {
|
||||
// given
|
||||
sb := getSB()
|
||||
b := NewBasic(sb, nil, converter.NewLayoutConverter(), nil)
|
||||
b := NewBasic(sb, nil, converter.NewLayoutConverter(), nil, nil)
|
||||
st := sb.NewState()
|
||||
|
||||
// when
|
||||
|
@ -435,7 +435,7 @@ func TestBasic_MoveTableBlocks(t *testing.T) {
|
|||
t.Run("moving rows and some other blocks between another leads to error", func(t *testing.T) {
|
||||
// given
|
||||
sb := getSB()
|
||||
b := NewBasic(sb, nil, converter.NewLayoutConverter(), nil)
|
||||
b := NewBasic(sb, nil, converter.NewLayoutConverter(), nil, nil)
|
||||
st := sb.NewState()
|
||||
|
||||
// when
|
||||
|
@ -448,7 +448,7 @@ func TestBasic_MoveTableBlocks(t *testing.T) {
|
|||
t.Run("moving the row between itself leads to error", func(t *testing.T) {
|
||||
// given
|
||||
sb := getSB()
|
||||
b := NewBasic(sb, nil, converter.NewLayoutConverter(), nil)
|
||||
b := NewBasic(sb, nil, converter.NewLayoutConverter(), nil, nil)
|
||||
st := sb.NewState()
|
||||
|
||||
// when
|
||||
|
@ -461,7 +461,7 @@ func TestBasic_MoveTableBlocks(t *testing.T) {
|
|||
t.Run("moving table block from invalid table leads to error", func(t *testing.T) {
|
||||
// given
|
||||
sb := getSB()
|
||||
b := NewBasic(sb, nil, converter.NewLayoutConverter(), nil)
|
||||
b := NewBasic(sb, nil, converter.NewLayoutConverter(), nil, nil)
|
||||
st := sb.NewState()
|
||||
st.Unlink("columns")
|
||||
|
||||
|
@ -477,7 +477,7 @@ func TestBasic_MoveTableBlocks(t *testing.T) {
|
|||
t.Run("moving a block to '"+block+"' block leads to moving it under the table", func(t *testing.T) {
|
||||
// given
|
||||
sb := getSB()
|
||||
b := NewBasic(sb, nil, converter.NewLayoutConverter(), nil)
|
||||
b := NewBasic(sb, nil, converter.NewLayoutConverter(), nil, nil)
|
||||
st := sb.NewState()
|
||||
|
||||
// when
|
||||
|
@ -492,7 +492,7 @@ func TestBasic_MoveTableBlocks(t *testing.T) {
|
|||
t.Run("moving a block to the invalid table leads to moving it under the table", func(t *testing.T) {
|
||||
// given
|
||||
sb := getSB()
|
||||
b := NewBasic(sb, nil, converter.NewLayoutConverter(), nil)
|
||||
b := NewBasic(sb, nil, converter.NewLayoutConverter(), nil, nil)
|
||||
st := sb.NewState()
|
||||
st.Unlink("columns")
|
||||
|
||||
|
@ -516,7 +516,7 @@ func TestBasic_MoveToAnotherObject(t *testing.T) {
|
|||
sb2 := smarttest.New("test2")
|
||||
sb2.AddBlock(simple.New(&model.Block{Id: "test2", ChildrenIds: []string{}}))
|
||||
|
||||
b := NewBasic(sb1, nil, converter.NewLayoutConverter(), nil)
|
||||
b := NewBasic(sb1, nil, converter.NewLayoutConverter(), nil, nil)
|
||||
|
||||
srcState := sb1.NewState()
|
||||
destState := sb2.NewState()
|
||||
|
@ -551,7 +551,7 @@ func TestBasic_Replace(t *testing.T) {
|
|||
sb := smarttest.New("test")
|
||||
sb.AddBlock(simple.New(&model.Block{Id: "test", ChildrenIds: []string{"2"}})).
|
||||
AddBlock(simple.New(&model.Block{Id: "2"}))
|
||||
b := NewBasic(sb, nil, converter.NewLayoutConverter(), nil)
|
||||
b := NewBasic(sb, nil, converter.NewLayoutConverter(), nil, nil)
|
||||
newId, err := b.Replace(nil, "2", &model.Block{Content: &model.BlockContentOfText{Text: &model.BlockContentText{Text: "l"}}})
|
||||
require.NoError(t, err)
|
||||
require.NotEmpty(t, newId)
|
||||
|
@ -561,7 +561,7 @@ func TestBasic_SetFields(t *testing.T) {
|
|||
sb := smarttest.New("test")
|
||||
sb.AddBlock(simple.New(&model.Block{Id: "test", ChildrenIds: []string{"2"}})).
|
||||
AddBlock(simple.New(&model.Block{Id: "2"}))
|
||||
b := NewBasic(sb, nil, converter.NewLayoutConverter(), nil)
|
||||
b := NewBasic(sb, nil, converter.NewLayoutConverter(), nil, nil)
|
||||
|
||||
fields := &types.Struct{
|
||||
Fields: map[string]*types.Value{
|
||||
|
@ -580,7 +580,7 @@ func TestBasic_Update(t *testing.T) {
|
|||
sb := smarttest.New("test")
|
||||
sb.AddBlock(simple.New(&model.Block{Id: "test", ChildrenIds: []string{"2"}})).
|
||||
AddBlock(simple.New(&model.Block{Id: "2"}))
|
||||
b := NewBasic(sb, nil, converter.NewLayoutConverter(), nil)
|
||||
b := NewBasic(sb, nil, converter.NewLayoutConverter(), nil, nil)
|
||||
|
||||
err := b.Update(nil, func(b simple.Block) error {
|
||||
b.Model().BackgroundColor = "test"
|
||||
|
@ -594,7 +594,7 @@ func TestBasic_SetDivStyle(t *testing.T) {
|
|||
sb := smarttest.New("test")
|
||||
sb.AddBlock(simple.New(&model.Block{Id: "test", ChildrenIds: []string{"2"}})).
|
||||
AddBlock(simple.New(&model.Block{Id: "2", Content: &model.BlockContentOfDiv{Div: &model.BlockContentDiv{}}}))
|
||||
b := NewBasic(sb, nil, converter.NewLayoutConverter(), nil)
|
||||
b := NewBasic(sb, nil, converter.NewLayoutConverter(), nil, nil)
|
||||
|
||||
err := b.SetDivStyle(nil, model.BlockContentDiv_Dots, "2")
|
||||
require.NoError(t, err)
|
||||
|
@ -614,7 +614,7 @@ func TestBasic_SetRelationKey(t *testing.T) {
|
|||
t.Run("correct", func(t *testing.T) {
|
||||
sb := smarttest.New("test")
|
||||
fillSb(sb)
|
||||
b := NewBasic(sb, nil, converter.NewLayoutConverter(), nil)
|
||||
b := NewBasic(sb, nil, converter.NewLayoutConverter(), nil, nil)
|
||||
err := b.SetRelationKey(nil, pb.RpcBlockRelationSetKeyRequest{
|
||||
BlockId: "2",
|
||||
Key: "testRelKey",
|
||||
|
@ -636,7 +636,7 @@ func TestBasic_SetRelationKey(t *testing.T) {
|
|||
t.Run("not relation block", func(t *testing.T) {
|
||||
sb := smarttest.New("test")
|
||||
fillSb(sb)
|
||||
b := NewBasic(sb, nil, converter.NewLayoutConverter(), nil)
|
||||
b := NewBasic(sb, nil, converter.NewLayoutConverter(), nil, nil)
|
||||
require.Error(t, b.SetRelationKey(nil, pb.RpcBlockRelationSetKeyRequest{
|
||||
BlockId: "1",
|
||||
Key: "key",
|
||||
|
@ -645,7 +645,7 @@ func TestBasic_SetRelationKey(t *testing.T) {
|
|||
t.Run("relation not found", func(t *testing.T) {
|
||||
sb := smarttest.New("test")
|
||||
fillSb(sb)
|
||||
b := NewBasic(sb, nil, converter.NewLayoutConverter(), nil)
|
||||
b := NewBasic(sb, nil, converter.NewLayoutConverter(), nil, nil)
|
||||
require.Error(t, b.SetRelationKey(nil, pb.RpcBlockRelationSetKeyRequest{
|
||||
BlockId: "2",
|
||||
Key: "not exists",
|
||||
|
@ -661,7 +661,7 @@ func TestBasic_FeaturedRelationAdd(t *testing.T) {
|
|||
s.AddBundledRelationLinks(bundle.RelationKeyDescription)
|
||||
require.NoError(t, sb.Apply(s))
|
||||
|
||||
b := NewBasic(sb, nil, converter.NewLayoutConverter(), nil)
|
||||
b := NewBasic(sb, nil, converter.NewLayoutConverter(), nil, nil)
|
||||
newRel := []string{bundle.RelationKeyDescription.String(), bundle.RelationKeyName.String()}
|
||||
require.NoError(t, b.FeaturedRelationAdd(nil, newRel...))
|
||||
|
||||
|
@ -677,7 +677,7 @@ func TestBasic_FeaturedRelationRemove(t *testing.T) {
|
|||
template.WithDescription(s)
|
||||
require.NoError(t, sb.Apply(s))
|
||||
|
||||
b := NewBasic(sb, nil, converter.NewLayoutConverter(), nil)
|
||||
b := NewBasic(sb, nil, converter.NewLayoutConverter(), nil, nil)
|
||||
require.NoError(t, b.FeaturedRelationRemove(nil, bundle.RelationKeyDescription.String()))
|
||||
|
||||
res := sb.NewState()
|
||||
|
@ -714,7 +714,7 @@ func TestBasic_ReplaceLink(t *testing.T) {
|
|||
}
|
||||
require.NoError(t, sb.Apply(s))
|
||||
|
||||
b := NewBasic(sb, nil, converter.NewLayoutConverter(), nil)
|
||||
b := NewBasic(sb, nil, converter.NewLayoutConverter(), nil, nil)
|
||||
require.NoError(t, b.ReplaceLink(oldId, newId))
|
||||
|
||||
res := sb.NewState()
|
||||
|
|
|
@ -4,11 +4,11 @@ import (
|
|||
"errors"
|
||||
"fmt"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/gogo/protobuf/types"
|
||||
"golang.org/x/exp/maps"
|
||||
|
||||
"github.com/anyproto/anytype-heart/core/block/editor/objecttype"
|
||||
"github.com/anyproto/anytype-heart/core/block/editor/smartblock"
|
||||
"github.com/anyproto/anytype-heart/core/block/editor/state"
|
||||
"github.com/anyproto/anytype-heart/core/block/restriction"
|
||||
|
@ -33,11 +33,29 @@ type detailUpdate struct {
|
|||
}
|
||||
|
||||
func (bs *basic) SetDetails(ctx session.Context, details []*model.Detail, showEvent bool) (err error) {
|
||||
_, err = bs.setDetails(ctx, details, showEvent)
|
||||
return err
|
||||
}
|
||||
|
||||
func (bs *basic) SetDetailsAndUpdateLastUsed(ctx session.Context, details []*model.Detail, showEvent bool) (err error) {
|
||||
var keys []domain.RelationKey
|
||||
keys, err = bs.setDetails(ctx, details, showEvent)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
ts := time.Now().Unix()
|
||||
for _, key := range keys {
|
||||
bs.lastUsedUpdater.UpdateLastUsedDate(bs.SpaceID(), key, ts)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (bs *basic) setDetails(ctx session.Context, details []*model.Detail, showEvent bool) (updatedKeys []domain.RelationKey, err error) {
|
||||
s := bs.NewStateCtx(ctx)
|
||||
|
||||
// Collect updates handling special cases. These cases could update details themselves, so we
|
||||
// have to apply changes later
|
||||
updates := bs.collectDetailUpdates(details, s)
|
||||
updates, updatedKeys := bs.collectDetailUpdates(details, s)
|
||||
newDetails := applyDetailUpdates(s.CombinedDetails(), updates)
|
||||
s.SetDetails(newDetails)
|
||||
|
||||
|
@ -46,43 +64,71 @@ func (bs *basic) SetDetails(ctx session.Context, details []*model.Detail, showEv
|
|||
flags.AddToState(s)
|
||||
|
||||
if err = bs.Apply(s, smartblock.NoRestrictions, smartblock.KeepInternalFlags); err != nil {
|
||||
return
|
||||
return nil, err
|
||||
}
|
||||
|
||||
bs.discardOwnSetDetailsEvent(ctx, showEvent)
|
||||
return nil
|
||||
return updatedKeys, nil
|
||||
}
|
||||
|
||||
func (bs *basic) UpdateDetails(update func(current *types.Struct) (*types.Struct, error)) (err error) {
|
||||
_, _, err = bs.updateDetails(update)
|
||||
return err
|
||||
}
|
||||
|
||||
func (bs *basic) UpdateDetailsAndLastUsed(update func(current *types.Struct) (*types.Struct, error)) (err error) {
|
||||
var oldDetails, newDetails *types.Struct
|
||||
oldDetails, newDetails, err = bs.updateDetails(update)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
diff := pbtypes.StructDiff(oldDetails, newDetails)
|
||||
if diff == nil || diff.Fields == nil {
|
||||
return nil
|
||||
}
|
||||
ts := time.Now().Unix()
|
||||
for key := range diff.Fields {
|
||||
bs.lastUsedUpdater.UpdateLastUsedDate(bs.SpaceID(), domain.RelationKey(key), ts)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (bs *basic) updateDetails(update func(current *types.Struct) (*types.Struct, error)) (oldDetails, newDetails *types.Struct, err error) {
|
||||
if update == nil {
|
||||
return fmt.Errorf("update function is nil")
|
||||
return nil, nil, fmt.Errorf("update function is nil")
|
||||
}
|
||||
s := bs.NewState()
|
||||
|
||||
newDetails, err := update(s.CombinedDetails())
|
||||
oldDetails = s.CombinedDetails()
|
||||
oldDetailsCopy := pbtypes.CopyStruct(oldDetails, true)
|
||||
|
||||
newDetails, err = update(oldDetailsCopy)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
s.SetDetails(newDetails)
|
||||
|
||||
if err = bs.addRelationLinks(s, maps.Keys(newDetails.Fields)...); err != nil {
|
||||
return
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
return bs.Apply(s)
|
||||
return oldDetails, newDetails, bs.Apply(s)
|
||||
}
|
||||
|
||||
func (bs *basic) collectDetailUpdates(details []*model.Detail, s *state.State) []*detailUpdate {
|
||||
func (bs *basic) collectDetailUpdates(details []*model.Detail, s *state.State) ([]*detailUpdate, []domain.RelationKey) {
|
||||
updates := make([]*detailUpdate, 0, len(details))
|
||||
keys := make([]domain.RelationKey, 0, len(details))
|
||||
for _, detail := range details {
|
||||
update, err := bs.createDetailUpdate(s, detail)
|
||||
if err == nil {
|
||||
updates = append(updates, update)
|
||||
keys = append(keys, domain.RelationKey(update.key))
|
||||
} else {
|
||||
log.Errorf("can't set detail %s: %s", detail.Key, err)
|
||||
}
|
||||
}
|
||||
return updates
|
||||
return updates, keys
|
||||
}
|
||||
|
||||
func applyDetailUpdates(oldDetails *types.Struct, updates []*detailUpdate) *types.Struct {
|
||||
|
@ -342,7 +388,7 @@ func (bs *basic) SetObjectTypesInState(s *state.State, objectTypeKeys []domain.T
|
|||
removeInternalFlags(s)
|
||||
|
||||
if pbtypes.GetInt64(bs.CombinedDetails(), bundle.RelationKeyOrigin.String()) == int64(model.ObjectOrigin_none) {
|
||||
objecttype.UpdateLastUsedDate(bs.Space(), bs.objectStore, objectTypeKeys[0])
|
||||
bs.lastUsedUpdater.UpdateLastUsedDate(bs.SpaceID(), objectTypeKeys[0], time.Now().Unix())
|
||||
}
|
||||
|
||||
toLayout, err := bs.getLayoutForType(objectTypeKeys[0])
|
||||
|
|
|
@ -32,7 +32,7 @@ func newDUFixture(t *testing.T) *duFixture {
|
|||
|
||||
store := objectstore.NewStoreFixture(t)
|
||||
|
||||
b := NewBasic(sb, store, converter.NewLayoutConverter(), nil)
|
||||
b := NewBasic(sb, store, converter.NewLayoutConverter(), nil, nil)
|
||||
|
||||
return &duFixture{
|
||||
sb: sb,
|
||||
|
|
|
@ -323,7 +323,7 @@ func TestExtractObjects(t *testing.T) {
|
|||
ObjectTypeUniqueKey: domain.MustUniqueKey(coresb.SmartBlockTypeObjectType, tc.typeKey).Marshal(),
|
||||
}
|
||||
ctx := session.NewContext()
|
||||
linkIds, err := NewBasic(sb, fixture.store, converter.NewLayoutConverter(), nil).ExtractBlocksToObjects(ctx, creator, ts, req)
|
||||
linkIds, err := NewBasic(sb, fixture.store, converter.NewLayoutConverter(), nil, nil).ExtractBlocksToObjects(ctx, creator, ts, req)
|
||||
assert.NoError(t, err)
|
||||
|
||||
gotBlockIds := []string{}
|
||||
|
@ -378,7 +378,7 @@ func TestExtractObjects(t *testing.T) {
|
|||
}},
|
||||
}
|
||||
ctx := session.NewContext()
|
||||
_, err := NewBasic(sb, fixture.store, converter.NewLayoutConverter(), nil).ExtractBlocksToObjects(ctx, creator, ts, req)
|
||||
_, err := NewBasic(sb, fixture.store, converter.NewLayoutConverter(), nil, nil).ExtractBlocksToObjects(ctx, creator, ts, req)
|
||||
assert.NoError(t, err)
|
||||
var block *model.Block
|
||||
for _, block = range sb.Blocks() {
|
||||
|
@ -411,7 +411,7 @@ func TestExtractObjects(t *testing.T) {
|
|||
}},
|
||||
}
|
||||
ctx := session.NewContext()
|
||||
_, err := NewBasic(sb, fixture.store, converter.NewLayoutConverter(), nil).ExtractBlocksToObjects(ctx, creator, ts, req)
|
||||
_, err := NewBasic(sb, fixture.store, converter.NewLayoutConverter(), nil, nil).ExtractBlocksToObjects(ctx, creator, ts, req)
|
||||
assert.NoError(t, err)
|
||||
var addedBlocks []*model.Block
|
||||
for _, message := range sb.Results.Events {
|
||||
|
|
|
@ -33,7 +33,7 @@ type Dashboard struct {
|
|||
func NewDashboard(sb smartblock.SmartBlock, objectStore objectstore.ObjectStore, layoutConverter converter.LayoutConverter) *Dashboard {
|
||||
return &Dashboard{
|
||||
SmartBlock: sb,
|
||||
AllOperations: basic.NewBasic(sb, objectStore, layoutConverter, nil),
|
||||
AllOperations: basic.NewBasic(sb, objectStore, layoutConverter, nil, nil),
|
||||
Collection: collection.NewCollection(sb, objectStore),
|
||||
objectStore: objectStore,
|
||||
}
|
||||
|
|
|
@ -11,6 +11,7 @@ import (
|
|||
"github.com/anyproto/anytype-heart/core/block/editor/bookmark"
|
||||
"github.com/anyproto/anytype-heart/core/block/editor/converter"
|
||||
"github.com/anyproto/anytype-heart/core/block/editor/file"
|
||||
"github.com/anyproto/anytype-heart/core/block/editor/lastused"
|
||||
"github.com/anyproto/anytype-heart/core/block/editor/smartblock"
|
||||
"github.com/anyproto/anytype-heart/core/block/migration"
|
||||
"github.com/anyproto/anytype-heart/core/block/process"
|
||||
|
@ -66,6 +67,7 @@ type ObjectFactory struct {
|
|||
fileReconciler reconciler.Reconciler
|
||||
objectDeleter ObjectDeleter
|
||||
deviceService deviceService
|
||||
lastUsedUpdater lastused.ObjectUsageUpdater
|
||||
}
|
||||
|
||||
func NewObjectFactory() *ObjectFactory {
|
||||
|
@ -94,6 +96,7 @@ func (f *ObjectFactory) Init(a *app.App) (err error) {
|
|||
f.objectDeleter = app.MustComponent[ObjectDeleter](a)
|
||||
f.fileReconciler = app.MustComponent[reconciler.Reconciler](a)
|
||||
f.deviceService = app.MustComponent[deviceService](a)
|
||||
f.lastUsedUpdater = app.MustComponent[lastused.ObjectUsageUpdater](a)
|
||||
return nil
|
||||
}
|
||||
|
||||
|
|
|
@ -25,7 +25,7 @@ var fileRequiredRelations = append(pageRequiredRelations, []domain.RelationKey{
|
|||
}...)
|
||||
|
||||
func (f *ObjectFactory) newFile(sb smartblock.SmartBlock) *File {
|
||||
basicComponent := basic.NewBasic(sb, f.objectStore, f.layoutConverter, f.fileObjectService)
|
||||
basicComponent := basic.NewBasic(sb, f.objectStore, f.layoutConverter, f.fileObjectService, f.lastUsedUpdater)
|
||||
return &File{
|
||||
SmartBlock: sb,
|
||||
ChangeReceiver: sb.(source.ChangeReceiver),
|
||||
|
|
216
core/block/editor/lastused/lastused.go
Normal file
216
core/block/editor/lastused/lastused.go
Normal file
|
@ -0,0 +1,216 @@
|
|||
package lastused
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/anyproto/any-sync/app"
|
||||
"github.com/anyproto/any-sync/app/logger"
|
||||
"github.com/cheggaaa/mb/v3"
|
||||
"github.com/gogo/protobuf/types"
|
||||
"go.uber.org/zap"
|
||||
|
||||
"github.com/anyproto/anytype-heart/core/block/editor/smartblock"
|
||||
"github.com/anyproto/anytype-heart/core/domain"
|
||||
"github.com/anyproto/anytype-heart/pkg/lib/bundle"
|
||||
coresb "github.com/anyproto/anytype-heart/pkg/lib/core/smartblock"
|
||||
"github.com/anyproto/anytype-heart/pkg/lib/localstore/addr"
|
||||
"github.com/anyproto/anytype-heart/pkg/lib/localstore/objectstore"
|
||||
"github.com/anyproto/anytype-heart/space"
|
||||
"github.com/anyproto/anytype-heart/space/clientspace"
|
||||
"github.com/anyproto/anytype-heart/util/pbtypes"
|
||||
)
|
||||
|
||||
const (
|
||||
CName = "object-usage-updater"
|
||||
|
||||
maxInstallationTime = 5 * time.Minute
|
||||
updateInterval = 5 * time.Second
|
||||
)
|
||||
|
||||
type Key interface {
|
||||
URL() string
|
||||
String() string
|
||||
}
|
||||
|
||||
type message struct {
|
||||
spaceId string
|
||||
key Key
|
||||
time int64
|
||||
}
|
||||
|
||||
var log = logger.NewNamed("update-last-used-date")
|
||||
|
||||
type ObjectUsageUpdater interface {
|
||||
app.ComponentRunnable
|
||||
|
||||
UpdateLastUsedDate(spaceId string, key Key, timeStamp int64)
|
||||
}
|
||||
|
||||
func New() ObjectUsageUpdater {
|
||||
return &updater{}
|
||||
}
|
||||
|
||||
type updater struct {
|
||||
store objectstore.ObjectStore
|
||||
spaceService space.Service
|
||||
|
||||
ctx context.Context
|
||||
cancel context.CancelFunc
|
||||
|
||||
msgBatch *mb.MB[message]
|
||||
}
|
||||
|
||||
func (u *updater) Name() string {
|
||||
return CName
|
||||
}
|
||||
|
||||
func (u *updater) Init(a *app.App) error {
|
||||
u.store = app.MustComponent[objectstore.ObjectStore](a)
|
||||
u.spaceService = app.MustComponent[space.Service](a)
|
||||
u.msgBatch = mb.New[message](0)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (u *updater) Run(context.Context) error {
|
||||
u.ctx, u.cancel = context.WithCancel(context.Background())
|
||||
go u.lastUsedUpdateHandler()
|
||||
return nil
|
||||
}
|
||||
|
||||
func (u *updater) Close(context.Context) error {
|
||||
if u.cancel != nil {
|
||||
u.cancel()
|
||||
}
|
||||
if err := u.msgBatch.Close(); err != nil {
|
||||
log.Error("failed to close message batch", zap.Error(err))
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (u *updater) UpdateLastUsedDate(spaceId string, key Key, ts int64) {
|
||||
if err := u.msgBatch.Add(u.ctx, message{spaceId: spaceId, key: key, time: ts}); err != nil {
|
||||
log.Error("failed to add last used date info to message batch", zap.Error(err), zap.String("key", key.String()))
|
||||
}
|
||||
}
|
||||
|
||||
func (u *updater) lastUsedUpdateHandler() {
|
||||
var (
|
||||
accumulator = make(map[string]map[Key]int64)
|
||||
lock sync.Mutex
|
||||
)
|
||||
|
||||
go func() {
|
||||
for {
|
||||
select {
|
||||
case <-u.ctx.Done():
|
||||
return
|
||||
case <-time.After(updateInterval):
|
||||
lock.Lock()
|
||||
if len(accumulator) == 0 {
|
||||
lock.Unlock()
|
||||
continue
|
||||
}
|
||||
for spaceId, keys := range accumulator {
|
||||
log.Debug("updating lastUsedDate for objects in space", zap.Int("objects num", len(keys)), zap.String("spaceId", spaceId))
|
||||
u.updateLastUsedDateForKeysInSpace(spaceId, keys)
|
||||
}
|
||||
accumulator = make(map[string]map[Key]int64)
|
||||
lock.Unlock()
|
||||
}
|
||||
}
|
||||
}()
|
||||
|
||||
for {
|
||||
msgs, err := u.msgBatch.Wait(u.ctx)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
lock.Lock()
|
||||
for _, msg := range msgs {
|
||||
if keys := accumulator[msg.spaceId]; keys != nil {
|
||||
keys[msg.key] = msg.time
|
||||
} else {
|
||||
keys = map[Key]int64{
|
||||
msg.key: msg.time,
|
||||
}
|
||||
accumulator[msg.spaceId] = keys
|
||||
}
|
||||
}
|
||||
lock.Unlock()
|
||||
}
|
||||
}
|
||||
|
||||
func (u *updater) updateLastUsedDateForKeysInSpace(spaceId string, keys map[Key]int64) {
|
||||
spc, err := u.spaceService.Get(u.ctx, spaceId)
|
||||
if err != nil {
|
||||
log.Error("failed to get space", zap.String("spaceId", spaceId), zap.Error(err))
|
||||
return
|
||||
}
|
||||
|
||||
for key, timeStamp := range keys {
|
||||
if err = u.updateLastUsedDate(spc, key, timeStamp); err != nil {
|
||||
log.Error("failed to update last used date", zap.String("spaceId", spaceId), zap.String("key", key.String()), zap.Error(err))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (u *updater) updateLastUsedDate(spc clientspace.Space, key Key, ts int64) error {
|
||||
uk, err := domain.UnmarshalUniqueKey(key.URL())
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to unmarshall key: %w", err)
|
||||
}
|
||||
|
||||
if uk.SmartblockType() != coresb.SmartBlockTypeObjectType && uk.SmartblockType() != coresb.SmartBlockTypeRelation {
|
||||
return fmt.Errorf("cannot update lastUsedDate for object with invalid smartBlock type. Only object types and relations are expected")
|
||||
}
|
||||
|
||||
details, err := u.store.GetObjectByUniqueKey(spc.Id(), uk)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to get details: %w", err)
|
||||
}
|
||||
|
||||
id := pbtypes.GetString(details.Details, bundle.RelationKeyId.String())
|
||||
if id == "" {
|
||||
return fmt.Errorf("failed to get id from details: %w", err)
|
||||
}
|
||||
|
||||
if err = spc.DoCtx(u.ctx, id, func(sb smartblock.SmartBlock) error {
|
||||
st := sb.NewState()
|
||||
st.SetLocalDetail(bundle.RelationKeyLastUsedDate.String(), pbtypes.Int64(ts))
|
||||
return sb.Apply(st)
|
||||
}); err != nil {
|
||||
return fmt.Errorf("failed to set lastUsedDate to object: %w", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func SetLastUsedDateForInitialObjectType(id string, details *types.Struct) {
|
||||
if !strings.HasPrefix(id, addr.BundledObjectTypeURLPrefix) || details == nil || details.Fields == nil {
|
||||
return
|
||||
}
|
||||
|
||||
var priority int64
|
||||
switch id {
|
||||
case bundle.TypeKeyNote.BundledURL():
|
||||
priority = 1
|
||||
case bundle.TypeKeyPage.BundledURL():
|
||||
priority = 2
|
||||
case bundle.TypeKeyTask.BundledURL():
|
||||
priority = 3
|
||||
case bundle.TypeKeySet.BundledURL():
|
||||
priority = 4
|
||||
case bundle.TypeKeyCollection.BundledURL():
|
||||
priority = 5
|
||||
default:
|
||||
priority = 7
|
||||
}
|
||||
|
||||
// we do this trick to order crucial Anytype object types by last date
|
||||
lastUsed := time.Now().Add(time.Duration(-1 * priority * int64(maxInstallationTime))).Unix()
|
||||
details.Fields[bundle.RelationKeyLastUsedDate.String()] = pbtypes.Int64(lastUsed)
|
||||
}
|
139
core/block/editor/lastused/lastused_test.go
Normal file
139
core/block/editor/lastused/lastused_test.go
Normal file
|
@ -0,0 +1,139 @@
|
|||
package lastused
|
||||
|
||||
import (
|
||||
"context"
|
||||
"math/rand"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/gogo/protobuf/types"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/mock"
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
"github.com/anyproto/anytype-heart/core/block/editor/smartblock"
|
||||
"github.com/anyproto/anytype-heart/core/block/editor/smartblock/smarttest"
|
||||
"github.com/anyproto/anytype-heart/core/domain"
|
||||
"github.com/anyproto/anytype-heart/pkg/lib/bundle"
|
||||
"github.com/anyproto/anytype-heart/pkg/lib/localstore/objectstore"
|
||||
"github.com/anyproto/anytype-heart/space/clientspace"
|
||||
"github.com/anyproto/anytype-heart/space/clientspace/mock_clientspace"
|
||||
"github.com/anyproto/anytype-heart/util/pbtypes"
|
||||
)
|
||||
|
||||
func TestSetLastUsedDateForInitialType(t *testing.T) {
|
||||
isLastUsedDateGreater := func(details1, details2 *types.Struct) bool {
|
||||
return pbtypes.GetInt64(details1, bundle.RelationKeyLastUsedDate.String()) > pbtypes.GetInt64(details2, bundle.RelationKeyLastUsedDate.String())
|
||||
}
|
||||
|
||||
t.Run("object types are sorted by lastUsedDate in correct order", func(t *testing.T) {
|
||||
// given
|
||||
ots := []string{
|
||||
bundle.TypeKeySet.BundledURL(),
|
||||
bundle.TypeKeyNote.BundledURL(),
|
||||
bundle.TypeKeyCollection.BundledURL(),
|
||||
bundle.TypeKeyTask.BundledURL(),
|
||||
bundle.TypeKeyPage.BundledURL(),
|
||||
bundle.TypeKeyDiaryEntry.BundledURL(),
|
||||
bundle.TypeKeyAudio.BundledURL(),
|
||||
}
|
||||
rand.Shuffle(len(ots), func(i, j int) {
|
||||
ots[i], ots[j] = ots[j], ots[i]
|
||||
})
|
||||
detailMap := map[string]*types.Struct{}
|
||||
|
||||
// when
|
||||
for _, id := range ots {
|
||||
details := &types.Struct{Fields: make(map[string]*types.Value)}
|
||||
SetLastUsedDateForInitialObjectType(id, details)
|
||||
detailMap[id] = details
|
||||
}
|
||||
|
||||
// then
|
||||
assert.True(t, isLastUsedDateGreater(detailMap[bundle.TypeKeyNote.BundledURL()], detailMap[bundle.TypeKeyPage.BundledURL()]))
|
||||
assert.True(t, isLastUsedDateGreater(detailMap[bundle.TypeKeyPage.BundledURL()], detailMap[bundle.TypeKeyTask.BundledURL()]))
|
||||
assert.True(t, isLastUsedDateGreater(detailMap[bundle.TypeKeyTask.BundledURL()], detailMap[bundle.TypeKeySet.BundledURL()]))
|
||||
assert.True(t, isLastUsedDateGreater(detailMap[bundle.TypeKeySet.BundledURL()], detailMap[bundle.TypeKeyCollection.BundledURL()]))
|
||||
assert.True(t, isLastUsedDateGreater(detailMap[bundle.TypeKeyCollection.BundledURL()], detailMap[bundle.TypeKeyAudio.BundledURL()]))
|
||||
assert.True(t, isLastUsedDateGreater(detailMap[bundle.TypeKeyCollection.BundledURL()], detailMap[bundle.TypeKeyDiaryEntry.BundledURL()]))
|
||||
})
|
||||
}
|
||||
|
||||
func TestUpdateLastUsedDate(t *testing.T) {
|
||||
const spaceId = "space"
|
||||
|
||||
ts := time.Now().Unix()
|
||||
|
||||
isLastUsedDateRecent := func(details *types.Struct, deltaSeconds int64) bool {
|
||||
return pbtypes.GetInt64(details, bundle.RelationKeyLastUsedDate.String())+deltaSeconds > time.Now().Unix()
|
||||
}
|
||||
|
||||
store := objectstore.NewStoreFixture(t)
|
||||
store.AddObjects(t, []objectstore.TestObject{
|
||||
{
|
||||
bundle.RelationKeyId: pbtypes.String(bundle.RelationKeyCamera.URL()),
|
||||
bundle.RelationKeySpaceId: pbtypes.String(spaceId),
|
||||
bundle.RelationKeyUniqueKey: pbtypes.String(bundle.RelationKeyCamera.URL()),
|
||||
},
|
||||
{
|
||||
bundle.RelationKeyId: pbtypes.String(bundle.TypeKeyDiaryEntry.URL()),
|
||||
bundle.RelationKeySpaceId: pbtypes.String(spaceId),
|
||||
bundle.RelationKeyUniqueKey: pbtypes.String(bundle.TypeKeyDiaryEntry.URL()),
|
||||
},
|
||||
{
|
||||
bundle.RelationKeyId: pbtypes.String("rel-custom"),
|
||||
bundle.RelationKeySpaceId: pbtypes.String(spaceId),
|
||||
bundle.RelationKeyUniqueKey: pbtypes.String("rel-custom"),
|
||||
},
|
||||
{
|
||||
bundle.RelationKeyId: pbtypes.String("opt-done"),
|
||||
bundle.RelationKeySpaceId: pbtypes.String(spaceId),
|
||||
bundle.RelationKeyUniqueKey: pbtypes.String("opt-done"),
|
||||
},
|
||||
})
|
||||
|
||||
u := updater{store: store}
|
||||
|
||||
getSpace := func() clientspace.Space {
|
||||
spc := mock_clientspace.NewMockSpace(t)
|
||||
spc.EXPECT().Id().Return(spaceId)
|
||||
spc.EXPECT().DoCtx(mock.Anything, mock.Anything, mock.Anything).RunAndReturn(func(_ context.Context, id string, apply func(smartblock.SmartBlock) error) error {
|
||||
sb := smarttest.New(id)
|
||||
err := apply(sb)
|
||||
require.NoError(t, err)
|
||||
|
||||
assert.True(t, isLastUsedDateRecent(sb.LocalDetails(), 5))
|
||||
return nil
|
||||
})
|
||||
return spc
|
||||
}
|
||||
|
||||
for _, tc := range []struct {
|
||||
name string
|
||||
key Key
|
||||
getSpace func() clientspace.Space
|
||||
isErrorExpected bool
|
||||
}{
|
||||
{"built-in relation", bundle.RelationKeyCamera, getSpace, false},
|
||||
{"built-in type", bundle.TypeKeyDiaryEntry, getSpace, false},
|
||||
{"custom relation", domain.RelationKey("custom"), getSpace, false},
|
||||
{"option", domain.TypeKey("opt-done"), func() clientspace.Space {
|
||||
spc := mock_clientspace.NewMockSpace(t)
|
||||
return spc
|
||||
}, true},
|
||||
{"type that is not in store", bundle.TypeKeyAudio, func() clientspace.Space {
|
||||
spc := mock_clientspace.NewMockSpace(t)
|
||||
spc.EXPECT().Id().Return(spaceId)
|
||||
return spc
|
||||
}, true},
|
||||
} {
|
||||
t.Run("update lastUsedDate of "+tc.name, func(t *testing.T) {
|
||||
err := u.updateLastUsedDate(tc.getSpace(), tc.key, ts)
|
||||
if tc.isErrorExpected {
|
||||
assert.Error(t, err)
|
||||
} else {
|
||||
assert.NoError(t, err)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
|
@ -1,71 +0,0 @@
|
|||
package objecttype
|
||||
|
||||
import (
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/gogo/protobuf/types"
|
||||
|
||||
"github.com/anyproto/anytype-heart/core/block/editor/smartblock"
|
||||
"github.com/anyproto/anytype-heart/core/domain"
|
||||
"github.com/anyproto/anytype-heart/pkg/lib/bundle"
|
||||
"github.com/anyproto/anytype-heart/pkg/lib/localstore/addr"
|
||||
"github.com/anyproto/anytype-heart/pkg/lib/localstore/objectstore"
|
||||
"github.com/anyproto/anytype-heart/pkg/lib/logging"
|
||||
"github.com/anyproto/anytype-heart/util/pbtypes"
|
||||
)
|
||||
|
||||
const maxInstallationTime = 5 * time.Minute
|
||||
|
||||
var log = logging.Logger("update-last-used-date")
|
||||
|
||||
func UpdateLastUsedDate(spc smartblock.Space, store objectstore.ObjectStore, key domain.TypeKey) {
|
||||
uk, err := domain.UnmarshalUniqueKey(key.URL())
|
||||
if err != nil {
|
||||
log.Errorf("failed to unmarshall type key '%s': %w", key.String(), err)
|
||||
return
|
||||
}
|
||||
details, err := store.GetObjectByUniqueKey(spc.Id(), uk)
|
||||
if err != nil {
|
||||
log.Errorf("failed to get details of type object '%s': %w", key.String(), err)
|
||||
return
|
||||
}
|
||||
id := pbtypes.GetString(details.Details, bundle.RelationKeyId.String())
|
||||
if id == "" {
|
||||
log.Errorf("failed to get id from details of type object '%s': %w", key.String(), err)
|
||||
return
|
||||
}
|
||||
if err = spc.Do(id, func(sb smartblock.SmartBlock) error {
|
||||
st := sb.NewState()
|
||||
st.SetLocalDetail(bundle.RelationKeyLastUsedDate.String(), pbtypes.Int64(time.Now().Unix()))
|
||||
return sb.Apply(st)
|
||||
}); err != nil {
|
||||
log.Errorf("failed to set lastUsedDate to type object '%s': %w", key.String(), err)
|
||||
}
|
||||
}
|
||||
|
||||
func SetLastUsedDateForInitialObjectType(id string, details *types.Struct) {
|
||||
if !strings.HasPrefix(id, addr.BundledObjectTypeURLPrefix) || details == nil || details.Fields == nil {
|
||||
return
|
||||
}
|
||||
|
||||
var priority int64
|
||||
switch id {
|
||||
case bundle.TypeKeyNote.BundledURL():
|
||||
priority = 1
|
||||
case bundle.TypeKeyPage.BundledURL():
|
||||
priority = 2
|
||||
case bundle.TypeKeyTask.BundledURL():
|
||||
priority = 3
|
||||
case bundle.TypeKeySet.BundledURL():
|
||||
priority = 4
|
||||
case bundle.TypeKeyCollection.BundledURL():
|
||||
priority = 5
|
||||
default:
|
||||
priority = 7
|
||||
}
|
||||
|
||||
// we do this trick to order crucial Anytype object types by last date
|
||||
lastUsed := time.Now().Add(time.Duration(-1 * priority * int64(maxInstallationTime))).Unix()
|
||||
details.Fields[bundle.RelationKeyLastUsedDate.String()] = pbtypes.Int64(lastUsed)
|
||||
}
|
|
@ -1,50 +0,0 @@
|
|||
package objecttype
|
||||
|
||||
import (
|
||||
"math/rand"
|
||||
"testing"
|
||||
|
||||
"github.com/gogo/protobuf/types"
|
||||
"github.com/stretchr/testify/assert"
|
||||
|
||||
"github.com/anyproto/anytype-heart/pkg/lib/bundle"
|
||||
"github.com/anyproto/anytype-heart/util/pbtypes"
|
||||
)
|
||||
|
||||
func TestSetLastUsedDateForInitialType(t *testing.T) {
|
||||
isLastUsedDateGreater := func(details1, details2 *types.Struct) bool {
|
||||
return pbtypes.GetInt64(details1, bundle.RelationKeyLastUsedDate.String()) > pbtypes.GetInt64(details2, bundle.RelationKeyLastUsedDate.String())
|
||||
}
|
||||
|
||||
t.Run("object types are sorted by lastUsedDate in correct order", func(t *testing.T) {
|
||||
// given
|
||||
ots := []string{
|
||||
bundle.TypeKeySet.BundledURL(),
|
||||
bundle.TypeKeyNote.BundledURL(),
|
||||
bundle.TypeKeyCollection.BundledURL(),
|
||||
bundle.TypeKeyTask.BundledURL(),
|
||||
bundle.TypeKeyPage.BundledURL(),
|
||||
bundle.TypeKeyGoal.BundledURL(),
|
||||
bundle.TypeKeyAudio.BundledURL(),
|
||||
}
|
||||
rand.Shuffle(len(ots), func(i, j int) {
|
||||
ots[i], ots[j] = ots[j], ots[i]
|
||||
})
|
||||
detailMap := map[string]*types.Struct{}
|
||||
|
||||
// when
|
||||
for _, id := range ots {
|
||||
details := &types.Struct{Fields: make(map[string]*types.Value)}
|
||||
SetLastUsedDateForInitialObjectType(id, details)
|
||||
detailMap[id] = details
|
||||
}
|
||||
|
||||
// then
|
||||
assert.True(t, isLastUsedDateGreater(detailMap[bundle.TypeKeyNote.BundledURL()], detailMap[bundle.TypeKeyPage.BundledURL()]))
|
||||
assert.True(t, isLastUsedDateGreater(detailMap[bundle.TypeKeyPage.BundledURL()], detailMap[bundle.TypeKeyTask.BundledURL()]))
|
||||
assert.True(t, isLastUsedDateGreater(detailMap[bundle.TypeKeyTask.BundledURL()], detailMap[bundle.TypeKeySet.BundledURL()]))
|
||||
assert.True(t, isLastUsedDateGreater(detailMap[bundle.TypeKeySet.BundledURL()], detailMap[bundle.TypeKeyCollection.BundledURL()]))
|
||||
assert.True(t, isLastUsedDateGreater(detailMap[bundle.TypeKeyCollection.BundledURL()], detailMap[bundle.TypeKeyAudio.BundledURL()]))
|
||||
assert.True(t, isLastUsedDateGreater(detailMap[bundle.TypeKeyCollection.BundledURL()], detailMap[bundle.TypeKeyGoal.BundledURL()]))
|
||||
})
|
||||
}
|
|
@ -58,7 +58,7 @@ func (f *ObjectFactory) newPage(sb smartblock.SmartBlock) *Page {
|
|||
return &Page{
|
||||
SmartBlock: sb,
|
||||
ChangeReceiver: sb.(source.ChangeReceiver),
|
||||
AllOperations: basic.NewBasic(sb, f.objectStore, f.layoutConverter, f.fileObjectService),
|
||||
AllOperations: basic.NewBasic(sb, f.objectStore, f.layoutConverter, f.fileObjectService, f.lastUsedUpdater),
|
||||
IHistory: basic.NewHistory(sb),
|
||||
Text: stext.NewText(
|
||||
sb,
|
||||
|
|
|
@ -31,7 +31,7 @@ type participant struct {
|
|||
}
|
||||
|
||||
func (f *ObjectFactory) newParticipant(sb smartblock.SmartBlock) *participant {
|
||||
basicComponent := basic.NewBasic(sb, f.objectStore, f.layoutConverter, nil)
|
||||
basicComponent := basic.NewBasic(sb, f.objectStore, f.layoutConverter, nil, f.lastUsedUpdater)
|
||||
return &participant{
|
||||
SmartBlock: sb,
|
||||
DetailsUpdatable: basicComponent,
|
||||
|
|
|
@ -135,7 +135,7 @@ func newStoreFixture(t *testing.T) *objectstore.StoreFixture {
|
|||
func newParticipantTest(t *testing.T) (*participant, error) {
|
||||
sb := smarttest.New("root")
|
||||
store := newStoreFixture(t)
|
||||
basicComponent := basic.NewBasic(sb, store, nil, nil)
|
||||
basicComponent := basic.NewBasic(sb, store, nil, nil, nil)
|
||||
p := &participant{
|
||||
SmartBlock: sb,
|
||||
DetailsUpdatable: basicComponent,
|
||||
|
|
|
@ -40,7 +40,7 @@ func (f *ObjectFactory) newProfile(sb smartblock.SmartBlock) *Profile {
|
|||
fileComponent := file.NewFile(sb, f.fileBlockService, f.picker, f.processService, f.fileUploaderService)
|
||||
return &Profile{
|
||||
SmartBlock: sb,
|
||||
AllOperations: basic.NewBasic(sb, f.objectStore, f.layoutConverter, f.fileObjectService),
|
||||
AllOperations: basic.NewBasic(sb, f.objectStore, f.layoutConverter, f.fileObjectService, f.lastUsedUpdater),
|
||||
IHistory: basic.NewHistory(sb),
|
||||
Text: stext.NewText(
|
||||
sb,
|
||||
|
|
|
@ -30,7 +30,7 @@ func NewWidgetObject(
|
|||
objectStore objectstore.ObjectStore,
|
||||
layoutConverter converter.LayoutConverter,
|
||||
) *WidgetObject {
|
||||
bs := basic.NewBasic(sb, objectStore, layoutConverter, nil)
|
||||
bs := basic.NewBasic(sb, objectStore, layoutConverter, nil, nil)
|
||||
return &WidgetObject{
|
||||
SmartBlock: sb,
|
||||
Movable: bs,
|
||||
|
|
|
@ -35,7 +35,7 @@ type Workspaces struct {
|
|||
func (f *ObjectFactory) newWorkspace(sb smartblock.SmartBlock) *Workspaces {
|
||||
w := &Workspaces{
|
||||
SmartBlock: sb,
|
||||
AllOperations: basic.NewBasic(sb, f.objectStore, f.layoutConverter, f.fileObjectService),
|
||||
AllOperations: basic.NewBasic(sb, f.objectStore, f.layoutConverter, f.fileObjectService, f.lastUsedUpdater),
|
||||
IHistory: basic.NewHistory(sb),
|
||||
Text: stext.NewText(
|
||||
sb,
|
||||
|
|
|
@ -8,6 +8,7 @@ import (
|
|||
"github.com/gogo/protobuf/types"
|
||||
"github.com/pkg/errors"
|
||||
|
||||
"github.com/anyproto/anytype-heart/core/block/editor/lastused"
|
||||
"github.com/anyproto/anytype-heart/core/block/editor/state"
|
||||
"github.com/anyproto/anytype-heart/core/block/restriction"
|
||||
"github.com/anyproto/anytype-heart/core/domain"
|
||||
|
@ -59,6 +60,7 @@ type service struct {
|
|||
bookmarkService bookmarkService
|
||||
spaceService space.Service
|
||||
templateService templateService
|
||||
lastUsedUpdater lastused.ObjectUsageUpdater
|
||||
}
|
||||
|
||||
func NewCreator() Service {
|
||||
|
@ -71,6 +73,7 @@ func (s *service) Init(a *app.App) (err error) {
|
|||
s.collectionService = app.MustComponent[collectionService](a)
|
||||
s.spaceService = app.MustComponent[space.Service](a)
|
||||
s.templateService = app.MustComponent[templateService](a)
|
||||
s.lastUsedUpdater = app.MustComponent[lastused.ObjectUsageUpdater](a)
|
||||
return nil
|
||||
}
|
||||
|
||||
|
|
|
@ -10,7 +10,7 @@ import (
|
|||
"github.com/gogo/protobuf/types"
|
||||
"go.uber.org/zap"
|
||||
|
||||
"github.com/anyproto/anytype-heart/core/block/editor/objecttype"
|
||||
"github.com/anyproto/anytype-heart/core/block/editor/lastused"
|
||||
"github.com/anyproto/anytype-heart/core/block/editor/smartblock"
|
||||
"github.com/anyproto/anytype-heart/core/domain"
|
||||
"github.com/anyproto/anytype-heart/pkg/lib/bundle"
|
||||
|
@ -226,7 +226,7 @@ func (s *service) prepareDetailsForInstallingObject(
|
|||
details.Fields[bundle.RelationKeyIsReadonly.String()] = pbtypes.Bool(false)
|
||||
|
||||
if isNewSpace {
|
||||
objecttype.SetLastUsedDateForInitialObjectType(sourceId, details)
|
||||
lastused.SetLastUsedDateForInitialObjectType(sourceId, details)
|
||||
}
|
||||
|
||||
bundledRelationIds := pbtypes.GetStringList(details, bundle.RelationKeyRecommendedRelations.String())
|
||||
|
|
|
@ -9,7 +9,6 @@ import (
|
|||
"github.com/gogo/protobuf/types"
|
||||
"golang.org/x/exp/slices"
|
||||
|
||||
"github.com/anyproto/anytype-heart/core/block/editor/objecttype"
|
||||
"github.com/anyproto/anytype-heart/core/block/editor/smartblock"
|
||||
"github.com/anyproto/anytype-heart/core/block/editor/state"
|
||||
"github.com/anyproto/anytype-heart/core/block/object/objectcache"
|
||||
|
@ -103,13 +102,7 @@ func (s *service) CreateSmartBlockFromStateInSpaceWithOptions(
|
|||
sb.Unlock()
|
||||
id = sb.Id()
|
||||
|
||||
if sbType == coresb.SmartBlockTypeObjectType && pbtypes.GetInt64(newDetails, bundle.RelationKeyLastUsedDate.String()) == 0 {
|
||||
objecttype.UpdateLastUsedDate(spc, s.objectStore, domain.TypeKey(
|
||||
strings.TrimPrefix(pbtypes.GetString(newDetails, bundle.RelationKeyUniqueKey.String()), addr.ObjectTypeKeyToIdPrefix)),
|
||||
)
|
||||
} else if pbtypes.GetInt64(newDetails, bundle.RelationKeyOrigin.String()) == int64(model.ObjectOrigin_none) {
|
||||
objecttype.UpdateLastUsedDate(spc, s.objectStore, objectTypeKeys[0])
|
||||
}
|
||||
s.updateLastUsedDate(spc.Id(), sbType, newDetails, objectTypeKeys[0])
|
||||
|
||||
ev.SmartblockCreateMs = time.Since(startTime).Milliseconds() - ev.SetDetailsMs - ev.WorkspaceCreateMs - ev.GetWorkspaceBlockWaitMs
|
||||
ev.SmartblockType = int(sbType)
|
||||
|
@ -118,6 +111,24 @@ func (s *service) CreateSmartBlockFromStateInSpaceWithOptions(
|
|||
return id, newDetails, nil
|
||||
}
|
||||
|
||||
func (s *service) updateLastUsedDate(spaceId string, sbType coresb.SmartBlockType, details *types.Struct, typeKey domain.TypeKey) {
|
||||
if pbtypes.GetInt64(details, bundle.RelationKeyLastUsedDate.String()) != 0 {
|
||||
return
|
||||
}
|
||||
uk := pbtypes.GetString(details, bundle.RelationKeyUniqueKey.String())
|
||||
ts := time.Now().Unix()
|
||||
switch sbType {
|
||||
case coresb.SmartBlockTypeObjectType:
|
||||
s.lastUsedUpdater.UpdateLastUsedDate(spaceId, domain.TypeKey(strings.TrimPrefix(uk, addr.ObjectTypeKeyToIdPrefix)), ts)
|
||||
case coresb.SmartBlockTypeRelation:
|
||||
s.lastUsedUpdater.UpdateLastUsedDate(spaceId, domain.RelationKey(strings.TrimPrefix(uk, addr.RelationKeyToIdPrefix)), ts)
|
||||
default:
|
||||
if pbtypes.GetInt64(details, bundle.RelationKeyOrigin.String()) == int64(model.ObjectOrigin_none) {
|
||||
s.lastUsedUpdater.UpdateLastUsedDate(spaceId, typeKey, ts)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func objectTypeKeysToSmartBlockType(typeKeys []domain.TypeKey) coresb.SmartBlockType {
|
||||
// TODO Add validation for types that user can't create
|
||||
|
||||
|
|
|
@ -44,7 +44,7 @@ func (mw *Middleware) ObjectSetDetails(cctx context.Context, req *pb.RpcObjectSe
|
|||
return m
|
||||
}
|
||||
err := mw.doBlockService(func(bs *block.Service) (err error) {
|
||||
return bs.SetDetails(ctx, req.ContextId, req.GetDetails())
|
||||
return bs.SetDetailsAndUpdateLastUsed(ctx, req.ContextId, req.GetDetails())
|
||||
})
|
||||
if err != nil {
|
||||
return response(pb.RpcObjectSetDetailsResponseError_UNKNOWN_ERROR, err)
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue