diff --git a/.githooks/commit-msg b/.githooks/commit-msg index 3f62723c2..115005abe 100755 --- a/.githooks/commit-msg +++ b/.githooks/commit-msg @@ -1,4 +1,4 @@ -#!/bin/bash +#!/usr/bin/env bash # The script below adds the branch name automatically to # every one of your commit messages. The regular expression # below searches for linear issue key's. The issue key will @@ -24,4 +24,4 @@ fi if [[ "$COMMIT_TEXT" != "GO-"* ]]; then echo "$ISSUE_ID" "$COMMIT_TEXT" > "$COMMIT_MSG_FILE" -fi \ No newline at end of file +fi diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 19676c43a..d7125a92f 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -129,7 +129,6 @@ jobs: make protos-server CC="x86_64-w64-mingw32-gcc" CXX="x86_64-w64-mingw32-g++" gox -cgo -ldflags="$FLAGS -linkmode external -extldflags=-static" -osarch="windows/amd64" --tags="envproduction nographviz nowatchdog nosigar nomutexdeadlockdetector noheic" -output="{{.OS}}-{{.Arch}}" github.com/anyproto/anytype-heart/cmd/grpcserver ls -lha . - if: github.event_name == 'push' - name: run perf tests run: | echo "Running perf tests" diff --git a/Makefile b/Makefile index 6663d3efb..cf84d0f5b 100644 --- a/Makefile +++ b/Makefile @@ -260,7 +260,7 @@ protos-swift: @echo 'Generating swift protobuf files' @protoc -I ./ --swift_opt=FileNaming=DropPath --swift_opt=Visibility=Public --swift_out=./dist/ios/protobuf pb/protos/*.proto pkg/lib/pb/model/protos/*.proto @echo 'Generated swift protobuf files at ./dist/ios/pb' - + protos-swift-local: protos-swift @echo 'Clear proto files' @rm -rf ./dist/ios/protobuf/protos @@ -325,14 +325,14 @@ install-linter: run-linter: ifdef GOLANGCI_LINT_BRANCH @golangci-lint run -v ./... --new-from-rev=$(GOLANGCI_LINT_BRANCH) --timeout 15m --verbose -else +else @golangci-lint run -v ./... --new-from-rev=origin/main --timeout 15m --verbose endif run-linter-fix: ifdef GOLANGCI_LINT_BRANCH @golangci-lint run -v ./... --new-from-rev=$(GOLANGCI_LINT_BRANCH) --timeout 15m --fix -else +else @golangci-lint run -v ./... --new-from-rev=origin/main --timeout 15m --fix endif @@ -390,4 +390,4 @@ download-tantivy-all: download-tantivy download-tantivy-local: remove-libs @mkdir -p $(OUTPUT_DIR) - @cp -r $(TANTIVY_GO_PATH)/libs/ $(OUTPUT_DIR) \ No newline at end of file + @cp -r $(TANTIVY_GO_PATH)/libs/* $(OUTPUT_DIR) diff --git a/README.md b/README.md index f2ec9c3ec..48d35e96c 100644 --- a/README.md +++ b/README.md @@ -9,9 +9,10 @@ Middleware library for Anytype, distributed as part of the Anytype clients. - [Project architecture](docs/Architecture.md) - [Style guide](docs/Codestyle.md) - [Project workflows](docs/Flow.md) +- [Complex filters](docs/ComplexFilters.md) ### CLI tools -- [Usecase generator](docs/UsecaseGenerator.md) +- [Usecase validator](docs/UsecaseValidator.md) - [Export Archive unpacker](docs/ExportArchiveUnpacker.md) ## Contribution diff --git a/core/anytype/bootstrap.go b/core/anytype/bootstrap.go index 02b3c37d2..6d46f1e93 100644 --- a/core/anytype/bootstrap.go +++ b/core/anytype/bootstrap.go @@ -41,6 +41,7 @@ import ( "github.com/anyproto/anytype-heart/core/block/details" "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" @@ -303,7 +304,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 { diff --git a/core/block/details/service.go b/core/block/details/service.go index 5f28eae6b..8b46dffb7 100644 --- a/core/block/details/service.go +++ b/core/block/details/service.go @@ -35,6 +35,7 @@ type Service interface { cache.ObjectGetterComponent SetDetails(ctx session.Context, objectId string, details []*model.Detail) error + SetDetailsAndUpdateLastUsed(ctx session.Context, objectId string, details []*model.Detail) error SetDetailsList(ctx session.Context, objectIds []string, details []*model.Detail) error ModifyDetails(objectId string, modifier func(current *types.Struct) (*types.Struct, error)) error ModifyDetailsList(req *pb.RpcObjectListModifyDetailValuesRequest) error @@ -99,13 +100,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 { @@ -128,10 +139,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 diff --git a/core/block/editor/basic/basic.go b/core/block/editor/basic/basic.go index 37319a413..991e5a848 100644 --- a/core/block/editor/basic/basic.go +++ b/core/block/editor/basic/basic.go @@ -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) { diff --git a/core/block/editor/basic/basic_test.go b/core/block/editor/basic/basic_test.go index 95271597b..5325f7f0f 100644 --- a/core/block/editor/basic/basic_test.go +++ b/core/block/editor/basic/basic_test.go @@ -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() diff --git a/core/block/editor/basic/details.go b/core/block/editor/basic/details.go index 96fee73d8..c0f1e6a2d 100644 --- a/core/block/editor/basic/details.go +++ b/core/block/editor/basic/details.go @@ -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]) diff --git a/core/block/editor/basic/details_test.go b/core/block/editor/basic/details_test.go index fe5ffa7e9..2565eb5c4 100644 --- a/core/block/editor/basic/details_test.go +++ b/core/block/editor/basic/details_test.go @@ -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, diff --git a/core/block/editor/basic/extract_objects_test.go b/core/block/editor/basic/extract_objects_test.go index 353bdf0a5..d32c5eda2 100644 --- a/core/block/editor/basic/extract_objects_test.go +++ b/core/block/editor/basic/extract_objects_test.go @@ -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 { diff --git a/core/block/editor/dashboard.go b/core/block/editor/dashboard.go index 5065e9341..40993410b 100644 --- a/core/block/editor/dashboard.go +++ b/core/block/editor/dashboard.go @@ -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, } diff --git a/core/block/editor/factory.go b/core/block/editor/factory.go index 3696b9f8d..960f8752d 100644 --- a/core/block/editor/factory.go +++ b/core/block/editor/factory.go @@ -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 } diff --git a/core/block/editor/files.go b/core/block/editor/files.go index f3fc30fae..06828d32e 100644 --- a/core/block/editor/files.go +++ b/core/block/editor/files.go @@ -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), diff --git a/core/block/editor/lastused/lastused.go b/core/block/editor/lastused/lastused.go new file mode 100644 index 000000000..b1ea9736e --- /dev/null +++ b/core/block/editor/lastused/lastused.go @@ -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) +} diff --git a/core/block/editor/lastused/lastused_test.go b/core/block/editor/lastused/lastused_test.go new file mode 100644 index 000000000..3e0b434cd --- /dev/null +++ b/core/block/editor/lastused/lastused_test.go @@ -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) + } + }) + } +} diff --git a/core/block/editor/objecttype/lastused.go b/core/block/editor/objecttype/lastused.go deleted file mode 100644 index de7dc9ee7..000000000 --- a/core/block/editor/objecttype/lastused.go +++ /dev/null @@ -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) -} diff --git a/core/block/editor/objecttype/lastused_test.go b/core/block/editor/objecttype/lastused_test.go deleted file mode 100644 index 91d1bd5ed..000000000 --- a/core/block/editor/objecttype/lastused_test.go +++ /dev/null @@ -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()])) - }) -} diff --git a/core/block/editor/page.go b/core/block/editor/page.go index 8ab60d6d2..b4f2578a6 100644 --- a/core/block/editor/page.go +++ b/core/block/editor/page.go @@ -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, diff --git a/core/block/editor/participant.go b/core/block/editor/participant.go index 96b81acbc..c4fe6251e 100644 --- a/core/block/editor/participant.go +++ b/core/block/editor/participant.go @@ -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, diff --git a/core/block/editor/participant_test.go b/core/block/editor/participant_test.go index 77570dd46..b676a84d9 100644 --- a/core/block/editor/participant_test.go +++ b/core/block/editor/participant_test.go @@ -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, diff --git a/core/block/editor/profile.go b/core/block/editor/profile.go index 53a988237..e3c447a0a 100644 --- a/core/block/editor/profile.go +++ b/core/block/editor/profile.go @@ -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, diff --git a/core/block/editor/smartblock/links.go b/core/block/editor/smartblock/links.go index 310d4123d..315ed3493 100644 --- a/core/block/editor/smartblock/links.go +++ b/core/block/editor/smartblock/links.go @@ -37,6 +37,22 @@ func (sb *smartBlock) injectLinksDetails(s *state.State) { s.SetLocalDetail(bundle.RelationKeyLinks.String(), pbtypes.StringList(links)) } +func (sb *smartBlock) injectMentions(s *state.State) { + mentions := objectlink.DependentObjectIDs(s, sb.Space(), objectlink.Flags{ + Blocks: true, + Details: false, + Relations: false, + Types: false, + Collection: false, + DataviewBlockOnlyTarget: true, + NoSystemRelations: true, + NoHiddenBundledRelations: true, + NoImages: true, + }) + mentions = slice.RemoveMut(mentions, sb.Id()) + s.SetDetailAndBundledRelation(bundle.RelationKeyMentions, pbtypes.StringList(mentions)) +} + func isBacklinksChanged(msgs []simple.EventMessage) bool { for _, msg := range msgs { if amend, ok := msg.Msg.Value.(*pb.EventMessageValueOfObjectDetailsAmend); ok { diff --git a/core/block/editor/smartblock/smartblock.go b/core/block/editor/smartblock/smartblock.go index 68007cf2b..2cb436670 100644 --- a/core/block/editor/smartblock/smartblock.go +++ b/core/block/editor/smartblock/smartblock.go @@ -1454,6 +1454,7 @@ func (sb *smartBlock) injectDerivedDetails(s *state.State, spaceID string, sbt s } sb.injectLinksDetails(s) + sb.injectMentions(s) sb.updateBackLinks(s) } diff --git a/core/block/editor/widget.go b/core/block/editor/widget.go index 3280bda6d..7bd25cec8 100644 --- a/core/block/editor/widget.go +++ b/core/block/editor/widget.go @@ -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, diff --git a/core/block/editor/workspaces.go b/core/block/editor/workspaces.go index 9c3b0bdeb..09477cd03 100644 --- a/core/block/editor/workspaces.go +++ b/core/block/editor/workspaces.go @@ -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, diff --git a/core/block/object/objectcreator/creator.go b/core/block/object/objectcreator/creator.go index 5c834dbdb..980db1202 100644 --- a/core/block/object/objectcreator/creator.go +++ b/core/block/object/objectcreator/creator.go @@ -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 } diff --git a/core/block/object/objectcreator/installer.go b/core/block/object/objectcreator/installer.go index 5db6154d8..d2de7d2b5 100644 --- a/core/block/object/objectcreator/installer.go +++ b/core/block/object/objectcreator/installer.go @@ -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()) diff --git a/core/block/object/objectcreator/smartblock.go b/core/block/object/objectcreator/smartblock.go index a24ba9c83..7213d856a 100644 --- a/core/block/object/objectcreator/smartblock.go +++ b/core/block/object/objectcreator/smartblock.go @@ -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 diff --git a/core/details.go b/core/details.go index 1f72ca048..0511d65c2 100644 --- a/core/details.go +++ b/core/details.go @@ -22,7 +22,7 @@ func (mw *Middleware) ObjectSetDetails(cctx context.Context, req *pb.RpcObjectSe } return m } - err := getService[details.Service](mw).SetDetails(ctx, req.ContextId, req.GetDetails()) + err := getService[details.Service](mw).SetDetailsAndUpdateLastUsed(ctx, req.ContextId, req.GetDetails()) if err != nil { return response(pb.RpcObjectSetDetailsResponseError_UNKNOWN_ERROR, err) } diff --git a/docs/ComplexFilters.md b/docs/ComplexFilters.md new file mode 100644 index 000000000..ea94310ce --- /dev/null +++ b/docs/ComplexFilters.md @@ -0,0 +1,62 @@ +## Complex search filters + +The majority of search filters for ObjectSearch-requests have simple format `key-condition-value`: +```json +{ + "RelationKey": "type", + "condition": 2, // is not equal + "value": { + "stringValue": "ot-page" + } +} +``` + +However, this format does not allow to build complex filters, that involve multiple relation keys analysis. +So we introduce complex filters that uses struct-values to build up more sophisticated requests. + +**type** field defined in the struct stands for complex filters types. + +### Two relation values comparison + +**type** = **valueFromRelation** allows to filter out objects by comparing values of two different relations. +First relation is set to root _RelationKey_ field, second - to _relationKey_ field of the struct value. + +For example, if we want to get all objects that were added to space before last modification, we can build up following filter: +```json +{ + "RelationKey": "addedDate", + "condition": 4, // less + "value": { + "structValue" : { + "fields": { + "type": { + "stringValue": "valueFromRelation" + }, + "relationKey": { + "stringValue": "lastModifiedDate" + } + } + } + } +} +``` + +All objects that has similar name and description: +```json +{ + "RelationKey": "name", + "condition": 1, // equals + "value": { + "structValue": { + "fields": { + "type": { + "stringValue": "valueFromRelation" + }, + "relationKey": { + "stringValue": "description" + } + } + } + } +} +``` \ No newline at end of file diff --git a/docs/UsecaseValidator.md b/docs/UsecaseValidator.md index 95935c26a..989be479d 100644 --- a/docs/UsecaseValidator.md +++ b/docs/UsecaseValidator.md @@ -1,6 +1,6 @@ ## Use Case archives validation tool -To use Use Case archives validation tool head to [cmd/usecasevalidator](../cmd/usecasegenerator), build the program +To use Use Case archives validation tool head to [cmd/usecasevalidator](../cmd/usecasevalidator), build the program `go build` and run it using `./usecasevalidator -path ` @@ -90,4 +90,4 @@ To choose the desired action **action** field should be provided to the JSON obj } ``` -More examples could be found in **rules.json** file \ No newline at end of file +More examples could be found in **rules.json** file diff --git a/go.mod b/go.mod index daa65f6ab..057a3abaa 100644 --- a/go.mod +++ b/go.mod @@ -7,7 +7,7 @@ require ( github.com/PuerkitoBio/goquery v1.9.2 github.com/VividCortex/ewma v1.2.0 github.com/adrium/goheif v0.0.0-20230113233934-ca402e77a786 - github.com/anyproto/any-store v0.0.2-0.20240812125400-7f6f6d062ebf + github.com/anyproto/any-store v0.0.4 github.com/anyproto/any-sync v0.4.30 github.com/anyproto/go-naturaldate/v2 v2.0.2-0.20230524105841-9829cfd13438 github.com/anyproto/tantivy-go v0.1.0 @@ -76,7 +76,7 @@ require ( github.com/otiai10/copy v1.14.0 github.com/otiai10/opengraph/v2 v2.1.0 github.com/pkg/errors v0.9.1 - github.com/prometheus/client_golang v1.20.2 + github.com/prometheus/client_golang v1.20.3 github.com/pseudomuto/protoc-gen-doc v1.5.1 github.com/rwcarlsen/goexif v0.0.0-20190401172101-9e8deecbddbd github.com/samber/lo v1.47.0 @@ -93,13 +93,13 @@ require ( go.uber.org/mock v0.4.0 go.uber.org/multierr v1.11.0 go.uber.org/zap v1.27.0 - golang.org/x/exp v0.0.0-20240719175910-8a7402abbf56 - golang.org/x/image v0.19.0 + golang.org/x/exp v0.0.0-20240823005443-9b4947da3948 + golang.org/x/image v0.20.0 golang.org/x/mobile v0.0.0-20240806205939-81131f6468ab - golang.org/x/net v0.28.0 - golang.org/x/oauth2 v0.22.0 - golang.org/x/text v0.17.0 - google.golang.org/grpc v1.66.0 + golang.org/x/net v0.29.0 + golang.org/x/oauth2 v0.23.0 + golang.org/x/text v0.18.0 + google.golang.org/grpc v1.66.1 gopkg.in/Graylog2/go-gelf.v2 v2.0.0-20180125164251-1832d8546a9f gopkg.in/yaml.v3 v3.0.1 storj.io/drpc v0.0.34 @@ -154,7 +154,7 @@ require ( github.com/dsoprea/go-logging v0.0.0-20200710184922-b02d349568dd // indirect github.com/dsoprea/go-photoshop-info-format v0.0.0-20200609050348-3db9b63b202c // indirect github.com/dsoprea/go-utility/v2 v2.0.0-20221003172846-a3e1774ef349 // indirect - github.com/dustin/go-humanize v1.0.0 // indirect + github.com/dustin/go-humanize v1.0.1 // indirect github.com/envoyproxy/protoc-gen-validate v1.0.4 // indirect github.com/fogleman/gg v1.3.0 // indirect github.com/fsnotify/fsnotify v1.6.0 // indirect @@ -174,7 +174,7 @@ require ( github.com/golang/protobuf v1.5.4 // indirect github.com/google/flatbuffers v1.12.1 // indirect github.com/google/go-querystring v1.1.0 // indirect - github.com/google/pprof v0.0.0-20240402174815-29b9bb013b0f // indirect + github.com/google/pprof v0.0.0-20240409012703-83162a5b38cd // indirect github.com/gopherjs/gopherjs v0.0.0-20190430165422-3e4dfb77656c // indirect github.com/gorilla/css v1.0.0 // indirect github.com/gosimple/unidecode v1.0.1 // indirect @@ -221,6 +221,7 @@ require ( github.com/multiformats/go-varint v0.0.7 // indirect github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect github.com/mwitkow/go-proto-validators v0.3.2 // indirect + github.com/ncruces/go-strftime v0.1.9 // indirect github.com/onsi/ginkgo/v2 v2.17.1 // indirect github.com/pelletier/go-toml/v2 v2.0.6 // indirect github.com/petermattis/goid v0.0.0-20240813172612-4fcff4a6cae7 // indirect @@ -232,6 +233,7 @@ require ( github.com/prometheus/procfs v0.15.1 // indirect github.com/pseudomuto/protokit v0.2.1 // indirect github.com/quic-go/quic-go v0.46.0 // indirect + github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect github.com/rs/cors v1.7.0 // indirect github.com/rs/zerolog v1.29.0 // indirect github.com/shoenig/go-m1cpu v0.1.6 // indirect @@ -258,11 +260,11 @@ require ( go.opencensus.io v0.24.0 // indirect go.opentelemetry.io/otel v1.14.0 // indirect go.opentelemetry.io/otel/trace v1.14.0 // indirect - golang.org/x/crypto v0.26.0 // indirect + golang.org/x/crypto v0.27.0 // indirect golang.org/x/mod v0.20.0 // indirect golang.org/x/sync v0.8.0 // indirect - golang.org/x/sys v0.23.0 // indirect - golang.org/x/term v0.23.0 // indirect + golang.org/x/sys v0.25.0 // indirect + golang.org/x/term v0.24.0 // indirect golang.org/x/time v0.5.0 // indirect golang.org/x/tools v0.24.0 // indirect google.golang.org/genproto/googleapis/api v0.0.0-20240604185151-ef581f913117 // indirect @@ -271,7 +273,12 @@ require ( gopkg.in/ini.v1 v1.67.0 // indirect gopkg.in/yaml.v2 v2.4.0 // indirect lukechampine.com/blake3 v1.2.1 // indirect + modernc.org/libc v1.60.1 // indirect + modernc.org/mathutil v1.6.0 // indirect + modernc.org/memory v1.8.0 // indirect + modernc.org/sqlite v1.32.0 // indirect nhooyr.io/websocket v1.8.7 // indirect + zombiezen.com/go/sqlite v1.3.0 // indirect ) replace github.com/dgraph-io/badger/v4 => github.com/anyproto/badger/v4 v4.2.1-0.20240110160636-80743fa3d580 diff --git a/go.sum b/go.sum index fa0f80e51..44e1cea6e 100644 --- a/go.sum +++ b/go.sum @@ -83,8 +83,8 @@ github.com/andybalholm/cascadia v1.2.0/go.mod h1:YCyR8vOZT9aZ1CHEd8ap0gMVm2aFgxB github.com/andybalholm/cascadia v1.3.1/go.mod h1:R4bJ1UQfqADjvDa4P6HZHLh/3OxWWEqc0Sk8XGwHqvA= github.com/andybalholm/cascadia v1.3.2 h1:3Xi6Dw5lHF15JtdcmAHD3i1+T8plmv7BQ/nsViSLyss= github.com/andybalholm/cascadia v1.3.2/go.mod h1:7gtRlve5FxPPgIgX36uWBX58OdBsSS6lUvCFb+h7KvU= -github.com/anyproto/any-store v0.0.2-0.20240812125400-7f6f6d062ebf h1:9VTQc/011fVyucu9bICmjF6+y/T9QdZv6MN7X4gM4S0= -github.com/anyproto/any-store v0.0.2-0.20240812125400-7f6f6d062ebf/go.mod h1:JV7dJ8+xF1VaCQgFQsD8SZSNlxxTGFbDiF8llkB2ILc= +github.com/anyproto/any-store v0.0.4 h1:Iv5XXOi0WM0g0yPNYixFA/MQpAxwHpJ5VtNuRszvE3g= +github.com/anyproto/any-store v0.0.4/go.mod h1:MA3sSqRXIp0h9bC2LnMuDGVEFGB3BkI/UuWNCleGHMs= github.com/anyproto/any-sync v0.4.30 h1:2nnoVQF9WiBIw8pzwuOubwqf7a+xvZPDkXHmykTPi0k= github.com/anyproto/any-sync v0.4.30/go.mod h1:YjwTdTRL6b6GuutJNboJJ1M5D/kkdeSf9qF5xP6wG7Y= github.com/anyproto/badger/v4 v4.2.1-0.20240110160636-80743fa3d580 h1:Ba80IlCCxkZ9H1GF+7vFu/TSpPvbpDCxXJ5ogc4euYc= @@ -297,8 +297,9 @@ github.com/dsoprea/go-utility/v2 v2.0.0-20221003160719-7bc88537c05e/go.mod h1:VZ github.com/dsoprea/go-utility/v2 v2.0.0-20221003172846-a3e1774ef349 h1:DilThiXje0z+3UQ5YjYiSRRzVdtamFpvBQXKwMglWqw= github.com/dsoprea/go-utility/v2 v2.0.0-20221003172846-a3e1774ef349/go.mod h1:4GC5sXji84i/p+irqghpPFZBF8tRN/Q7+700G0/DLe8= github.com/dustin/go-humanize v0.0.0-20171111073723-bb3d318650d4/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk= -github.com/dustin/go-humanize v1.0.0 h1:VSnTsYCnlFHaM2/igO1h6X3HA71jcobQuxemgkq4zYo= github.com/dustin/go-humanize v1.0.0/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk= +github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY= +github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto= github.com/eapache/go-resiliency v1.1.0/go.mod h1:kFI+JgMyC7bLPUVY133qvEBtVayf5mFgVsvEsIPBvNs= github.com/eapache/go-xerial-snappy v0.0.0-20180814174437-776d5712da21/go.mod h1:+020luEh2TKB4/GOp8oxxtq0Daoen/Cii55CzbTV6DU= github.com/eapache/queue v1.1.0/go.mod h1:6eCeP0CKFpHLu8blIFXhExK/dRa7WDZfr6jVFPTqq+I= @@ -489,8 +490,8 @@ github.com/google/pprof v0.0.0-20200708004538-1a94d8640e99/go.mod h1:ZgVRPoUq/hf github.com/google/pprof v0.0.0-20201023163331-3e6fc7fc9c4c/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= github.com/google/pprof v0.0.0-20201203190320-1bf35d6f28c2/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= github.com/google/pprof v0.0.0-20201218002935-b9804c9f04c2/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= -github.com/google/pprof v0.0.0-20240402174815-29b9bb013b0f h1:f00RU+zOX+B3rLAmMMkzHUF2h1z4DeYR9tTCvEq2REY= -github.com/google/pprof v0.0.0-20240402174815-29b9bb013b0f/go.mod h1:kf6iHlnVGwgKolg33glAes7Yg/8iWP8ukqeldJSO7jw= +github.com/google/pprof v0.0.0-20240409012703-83162a5b38cd h1:gbpYu9NMq8jhDVbvlGkMFWCjLFlqqEZjEmObmhUy6Vo= +github.com/google/pprof v0.0.0-20240409012703-83162a5b38cd/go.mod h1:kf6iHlnVGwgKolg33glAes7Yg/8iWP8ukqeldJSO7jw= github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI= github.com/google/uuid v1.0.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/google/uuid v1.1.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= @@ -1106,6 +1107,8 @@ github.com/nats-io/nats.go v1.9.1/go.mod h1:ZjDU1L/7fJ09jvUSRVBR2e7+RnLiiIQyqyzE github.com/nats-io/nkeys v0.1.0/go.mod h1:xpnFELMwJABBLVhffcfd1MZx6VsNRFpEugbxziKVo7w= github.com/nats-io/nkeys v0.1.3/go.mod h1:xpnFELMwJABBLVhffcfd1MZx6VsNRFpEugbxziKVo7w= github.com/nats-io/nuid v1.0.1/go.mod h1:19wcPz3Ph3q0Jbyiqsd0kePYG7A95tJPxeL+1OSON2c= +github.com/ncruces/go-strftime v0.1.9 h1:bY0MQC28UADQmHmaF5dgpLmImcShSi2kHU9XLdhx/f4= +github.com/ncruces/go-strftime v0.1.9/go.mod h1:Fwc5htZGVVkseilnfgOVb9mKy6w1naJmn9CehxcKcls= github.com/nfnt/resize v0.0.0-20160724205520-891127d8d1b5 h1:BvoENQQU+fZ9uukda/RzCAL/191HHwJA5b13R6diVlY= github.com/nfnt/resize v0.0.0-20160724205520-891127d8d1b5/go.mod h1:jpp1/29i3P1S/RLdc7JQKbRpFeM1dOBd8T9ki5s+AY8= github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno= @@ -1217,8 +1220,8 @@ github.com/prometheus/client_golang v0.9.3/go.mod h1:/TN21ttK/J9q6uSwhBd54HahCDf github.com/prometheus/client_golang v1.0.0/go.mod h1:db9x61etRT2tGnBNRi70OPL5FsnadC4Ky3P0J6CfImo= github.com/prometheus/client_golang v1.3.0/go.mod h1:hJaj2vgQTGQmVCsAACORcieXFeDPbaTKGT+JTgUa3og= github.com/prometheus/client_golang v1.7.1/go.mod h1:PY5Wy2awLA44sXw4AOSfFBetzPP4j5+D6mVACh+pe2M= -github.com/prometheus/client_golang v1.20.2 h1:5ctymQzZlyOON1666svgwn3s6IKWgfbjsejTMiXIyjg= -github.com/prometheus/client_golang v1.20.2/go.mod h1:PIEt8X02hGcP8JWbeHyeZ53Y/jReSnHgO035n//V5WE= +github.com/prometheus/client_golang v1.20.3 h1:oPksm4K8B+Vt35tUhw6GbSNSgVlVSBH0qELP/7u83l4= +github.com/prometheus/client_golang v1.20.3/go.mod h1:PIEt8X02hGcP8JWbeHyeZ53Y/jReSnHgO035n//V5WE= github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo= github.com/prometheus/client_model v0.0.0-20190115171406-56726106282f/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo= github.com/prometheus/client_model v0.0.0-20190129233127-fd36f4220a90/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= @@ -1257,6 +1260,8 @@ github.com/quic-go/quic-go v0.46.0/go.mod h1:1dLehS7TIR64+vxGR70GDcatWTOtMX2PUtn github.com/quic-go/webtransport-go v0.8.0 h1:HxSrwun11U+LlmwpgM1kEqIqH90IT4N8auv/cD7QFJg= github.com/quic-go/webtransport-go v0.8.0/go.mod h1:N99tjprW432Ut5ONql/aUhSLT0YVSlwHohQsuac9WaM= github.com/rcrowley/go-metrics v0.0.0-20181016184325-3113b8401b8a/go.mod h1:bCqnVzQkZxMG4s8nGwiZ5l3QUCyqpo9Y+/ZMZ9VjZe4= +github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec h1:W09IVJc94icq4NjY3clb7Lk8O1qJ8BdBEF8z0ibU0rE= +github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo= github.com/rivo/uniseg v0.1.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= github.com/rogpeppe/fastuuid v0.0.0-20150106093220-6724a57986af/go.mod h1:XWv6SoW27p1b0cqNHllgS5HIMJraePCO15w5zCzIWYg= github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= @@ -1513,8 +1518,8 @@ golang.org/x/crypto v0.0.0-20210421170649-83a5a9bb288b/go.mod h1:T9bdIzuCu7OtxOm golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= golang.org/x/crypto v0.0.0-20220722155217-630584e8d5aa/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= golang.org/x/crypto v0.12.0/go.mod h1:NF0Gs7EO5K4qLn+Ylc+fih8BSTeIjAP05siRnAh98yw= -golang.org/x/crypto v0.26.0 h1:RrRspgV4mU+YwB4FYnuBoKsUapNIL5cohGAmSH3azsw= -golang.org/x/crypto v0.26.0/go.mod h1:GY7jblb9wI+FOo5y8/S2oY4zWP07AkOJ4+jxCqdqn54= +golang.org/x/crypto v0.27.0 h1:GXm2NjJrPaiv/h1tb2UH8QfgC/hOf/+z0p6PT8o1w7A= +golang.org/x/crypto v0.27.0/go.mod h1:1Xngt8kV6Dvbssa53Ziq6Eqn0HqbZi5Z6R0ZpwQzt70= golang.org/x/exp v0.0.0-20180321215751-8460e604b9de/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20180807140117-3d87b88a115f/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= @@ -1529,14 +1534,14 @@ golang.org/x/exp v0.0.0-20200119233911-0405dc783f0a/go.mod h1:2RIsYlXP63K8oxa1u0 golang.org/x/exp v0.0.0-20200207192155-f17229e696bd/go.mod h1:J/WKrq2StrnmMY6+EHIKF9dgMWnmCNThgcyBT1FY9mM= golang.org/x/exp v0.0.0-20200224162631-6cc2880d07d6/go.mod h1:3jZMyOhIsHpP37uCMkUooju7aAi5cS1Q23tOzKc+0MU= golang.org/x/exp v0.0.0-20200331195152-e8c3332aa8e5/go.mod h1:4M0jN8W1tt0AVLNr8HDosyJCDCDuyL9N9+3m7wDWgKw= -golang.org/x/exp v0.0.0-20240719175910-8a7402abbf56 h1:2dVuKD2vS7b0QIHQbpyTISPd0LeHDbnYEryqj5Q1ug8= -golang.org/x/exp v0.0.0-20240719175910-8a7402abbf56/go.mod h1:M4RDyNAINzryxdtnbRXRL/OHtkFuWGRjvuhBJpk2IlY= +golang.org/x/exp v0.0.0-20240823005443-9b4947da3948 h1:kx6Ds3MlpiUHKj7syVnbp57++8WpuKPcR5yjLBjvLEA= +golang.org/x/exp v0.0.0-20240823005443-9b4947da3948/go.mod h1:akd2r19cwCdwSwWeIdzYQGa/EZZyqcOdwWiwj5L5eKQ= golang.org/x/image v0.0.0-20180708004352-c73c2afc3b81/go.mod h1:ux5Hcp/YLpHSI86hEcLt0YII63i6oz57MZXIpbrjZUs= golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js= golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= golang.org/x/image v0.0.0-20191009234506-e7c1f5e7dbb8/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= -golang.org/x/image v0.19.0 h1:D9FX4QWkLfkeqaC62SonffIIuYdOk/UE2XKUBgRIBIQ= -golang.org/x/image v0.19.0/go.mod h1:y0zrRqlQRWQ5PXaYCOMLTW2fpsxZ8Qh9I/ohnInJEys= +golang.org/x/image v0.20.0 h1:7cVCUjQwfL18gyBJOmYvptfSHS8Fb3YUDtfLIZ7Nbpw= +golang.org/x/image v0.20.0/go.mod h1:0a88To4CYVBAHp5FXJm8o7QbUl37Vd85ply1vyD8auM= golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU= golang.org/x/lint v0.0.0-20190301231843-5614ed5bae6f/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= @@ -1628,8 +1633,8 @@ golang.org/x/net v0.7.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= golang.org/x/net v0.9.0/go.mod h1:d48xBJpPfHeWQsugry2m+kC02ZBRGRgulfHnEXEuWns= golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg= golang.org/x/net v0.14.0/go.mod h1:PpSgVXXLK0OxS0F31C1/tv6XNguvCrnXIDrFMspZIUI= -golang.org/x/net v0.28.0 h1:a9JDOJc5GMUJ0+UDqmLT86WiEy7iWyIhz8gz8E4e5hE= -golang.org/x/net v0.28.0/go.mod h1:yqtgsTWOOnlGLG9GFRrK3++bGOUEkNBoHZc8MEDWPNg= +golang.org/x/net v0.29.0 h1:5ORfpBpCs4HzDYoodCDBbwHzdR5UrLBZ3sOnUJmFoHo= +golang.org/x/net v0.29.0/go.mod h1:gLkgy8jTGERgjzMic6DS9+SP0ajcu6Xu3Orq/SpETg0= golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= @@ -1639,8 +1644,8 @@ golang.org/x/oauth2 v0.0.0-20200902213428-5d25da1a8d43/go.mod h1:KelEdhl1UZF7XfJ golang.org/x/oauth2 v0.0.0-20201109201403-9fd604954f58/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= golang.org/x/oauth2 v0.0.0-20201208152858-08078c50e5b5/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= golang.org/x/oauth2 v0.0.0-20210218202405-ba52d332ba99/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= -golang.org/x/oauth2 v0.22.0 h1:BzDx2FehcG7jJwgWLELCdmLuxk2i+x9UDpSiss2u0ZA= -golang.org/x/oauth2 v0.22.0/go.mod h1:XYTD2NtWslqkgxebSiOHnXEap4TF09sJSc7H1sXbhtI= +golang.org/x/oauth2 v0.23.0 h1:PbgcYx2W7i4LvjJWEbf0ngHV6qJYr86PkAV3bXdLEbs= +golang.org/x/oauth2 v0.23.0/go.mod h1:XYTD2NtWslqkgxebSiOHnXEap4TF09sJSc7H1sXbhtI= golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= @@ -1749,8 +1754,8 @@ golang.org/x/sys v0.7.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.11.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.19.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= -golang.org/x/sys v0.23.0 h1:YfKFowiIMvtgl1UERQoTPPToxltDeZfbj4H7dVUCwmM= -golang.org/x/sys v0.23.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/sys v0.25.0 h1:r+8e+loiHxRqhXVl6ML1nO3l1+oFoWbnlu2Ehimmi34= +golang.org/x/sys v0.25.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/term v0.0.0-20201117132131-f5c789dd3221/go.mod h1:Nr5EML6q2oocZ2LXRh80K7BxOlk5/8JxuGnuhpl+muw= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= @@ -1758,8 +1763,8 @@ golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= golang.org/x/term v0.7.0/go.mod h1:P32HKFT3hSsZrRxla30E9HqToFYAQPCMs/zFMBUFqPY= golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo= golang.org/x/term v0.11.0/go.mod h1:zC9APTIj3jG3FdV/Ons+XE1riIZXG4aZ4GTHiPZJPIU= -golang.org/x/term v0.23.0 h1:F6D4vR+EHoL9/sWAWgAR1H2DcHr4PareCbAaCo1RpuU= -golang.org/x/term v0.23.0/go.mod h1:DgV24QBUrK6jhZXl+20l6UWznPlwAHm1Q1mGHtydmSk= +golang.org/x/term v0.24.0 h1:Mh5cbb+Zk2hqqXNO7S1iTjEphVL+jb8ZWaqh/g+JWkM= +golang.org/x/term v0.24.0/go.mod h1:lOBK/LVxemqiMij05LGJ0tzNr8xlmwBRJ81PX6wVLH8= golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= @@ -1771,8 +1776,8 @@ golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= golang.org/x/text v0.12.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE= -golang.org/x/text v0.17.0 h1:XtiM5bkSOt+ewxlOE/aE/AKEHibwj/6gvWMl9Rsh0Qc= -golang.org/x/text v0.17.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY= +golang.org/x/text v0.18.0 h1:XvMDiNzPAl0jr17s6W9lcaIhGUfUORdGCNsuLmPG224= +golang.org/x/text v0.18.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY= golang.org/x/time v0.0.0-20180412165947-fbb02b2291d2/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= @@ -1948,8 +1953,8 @@ google.golang.org/grpc v1.32.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM google.golang.org/grpc v1.33.2/go.mod h1:JMHMWHQWaTccqQQlmk3MJZS+GWXOdAesneDmEnv2fbc= google.golang.org/grpc v1.34.0/go.mod h1:WotjhfgOW/POjDeRt8vscBtXq+2VjORFy659qA51WJ8= google.golang.org/grpc v1.35.0/go.mod h1:qjiiYl8FncCW8feJPdyg3v6XW24KsRHe+dy9BAGRRjU= -google.golang.org/grpc v1.66.0 h1:DibZuoBznOxbDQxRINckZcUvnCEvrW9pcWIE2yF9r1c= -google.golang.org/grpc v1.66.0/go.mod h1:s3/l6xSSCURdVfAnL+TqCNMyTDAGN6+lZeVxnZR128Y= +google.golang.org/grpc v1.66.1 h1:hO5qAXR19+/Z44hmvIM4dQFMSYX9XcWsByfoxutBpAM= +google.golang.org/grpc v1.66.1/go.mod h1:s3/l6xSSCURdVfAnL+TqCNMyTDAGN6+lZeVxnZR128Y= google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8= google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0= google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM= @@ -2006,6 +2011,30 @@ honnef.co/go/tools v0.0.1-2020.1.3/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9 honnef.co/go/tools v0.0.1-2020.1.4/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k= lukechampine.com/blake3 v1.2.1 h1:YuqqRuaqsGV71BV/nm9xlI0MKUv4QC54jQnBChWbGnI= lukechampine.com/blake3 v1.2.1/go.mod h1:0OFRp7fBtAylGVCO40o87sbupkyIGgbpv1+M1k1LM6k= +modernc.org/cc/v4 v4.21.4 h1:3Be/Rdo1fpr8GrQ7IVw9OHtplU4gWbb+wNgeoBMmGLQ= +modernc.org/cc/v4 v4.21.4/go.mod h1:HM7VJTZbUCR3rV8EYBi9wxnJ0ZBRiGE5OeGXNA0IsLQ= +modernc.org/ccgo/v4 v4.21.0 h1:kKPI3dF7RIag8YcToh5ZwDcVMIv6VGa0ED5cvh0LMW4= +modernc.org/ccgo/v4 v4.21.0/go.mod h1:h6kt6H/A2+ew/3MW/p6KEoQmrq/i3pr0J/SiwiaF/g0= +modernc.org/fileutil v1.3.0 h1:gQ5SIzK3H9kdfai/5x41oQiKValumqNTDXMvKo62HvE= +modernc.org/fileutil v1.3.0/go.mod h1:XatxS8fZi3pS8/hKG2GH/ArUogfxjpEKs3Ku3aK4JyQ= +modernc.org/gc/v2 v2.5.0 h1:bJ9ChznK1L1mUtAQtxi0wi5AtAs5jQuw4PrPHO5pb6M= +modernc.org/gc/v2 v2.5.0/go.mod h1:wzN5dK1AzVGoH6XOzc3YZ+ey/jPgYHLuVckd62P0GYU= +modernc.org/libc v1.60.1 h1:at373l8IFRTkJIkAU85BIuUoBM4T1b51ds0E1ovPG2s= +modernc.org/libc v1.60.1/go.mod h1:xJuobKuNxKH3RUatS7GjR+suWj+5c2K7bi4m/S5arOY= +modernc.org/mathutil v1.6.0 h1:fRe9+AmYlaej+64JsEEhoWuAYBkOtQiMEU7n/XgfYi4= +modernc.org/mathutil v1.6.0/go.mod h1:Ui5Q9q1TR2gFm0AQRqQUaBWFLAhQpCwNcuhBOSedWPo= +modernc.org/memory v1.8.0 h1:IqGTL6eFMaDZZhEWwcREgeMXYwmW83LYW8cROZYkg+E= +modernc.org/memory v1.8.0/go.mod h1:XPZ936zp5OMKGWPqbD3JShgd/ZoQ7899TUuQqxY+peU= +modernc.org/opt v0.1.3 h1:3XOZf2yznlhC+ibLltsDGzABUGVx8J6pnFMS3E4dcq4= +modernc.org/opt v0.1.3/go.mod h1:WdSiB5evDcignE70guQKxYUl14mgWtbClRi5wmkkTX0= +modernc.org/sortutil v1.2.0 h1:jQiD3PfS2REGJNzNCMMaLSp/wdMNieTbKX920Cqdgqc= +modernc.org/sortutil v1.2.0/go.mod h1:TKU2s7kJMf1AE84OoiGppNHJwvB753OYfNl2WRb++Ss= +modernc.org/sqlite v1.32.0 h1:6BM4uGza7bWypsw4fdLRsLxut6bHe4c58VeqjRgST8s= +modernc.org/sqlite v1.32.0/go.mod h1:UqoylwmTb9F+IqXERT8bW9zzOWN8qwAIcLdzeBZs4hA= +modernc.org/strutil v1.2.0 h1:agBi9dp1I+eOnxXeiZawM8F4LawKv4NzGWSaLfyeNZA= +modernc.org/strutil v1.2.0/go.mod h1:/mdcBmfOibveCTBxUl5B5l6W+TTH1FXPLHZE6bTosX0= +modernc.org/token v1.1.0 h1:Xl7Ap9dKaEs5kLoOQeQmPWevfnk/DM5qcLcYlA8ys6Y= +modernc.org/token v1.1.0/go.mod h1:UGzOrNV1mAFSEB63lOFHIpNRUVMvYTc6yu1SMY/XTDM= nhooyr.io/websocket v1.8.6/go.mod h1:B70DZP8IakI65RVQ51MsWP/8jndNma26DVA/nFSCgW0= nhooyr.io/websocket v1.8.7 h1:usjR2uOr/zjjkVMy0lW+PPohFok7PCow5sDjLgX4P4g= nhooyr.io/websocket v1.8.7/go.mod h1:B70DZP8IakI65RVQ51MsWP/8jndNma26DVA/nFSCgW0= @@ -2017,3 +2046,5 @@ sigs.k8s.io/yaml v1.1.0/go.mod h1:UJmg0vDUVViEyp3mgSv9WPwZCDxu4rQW1olrI1uml+o= sourcegraph.com/sourcegraph/appdash v0.0.0-20190731080439-ebfcffb1b5c0/go.mod h1:hI742Nqp5OhwiqlzhgfbWU4mW4yO10fP+LoT9WOswdU= storj.io/drpc v0.0.34 h1:q9zlQKfJ5A7x8NQNFk8x7eKUF78FMhmAbZLnFK+og7I= storj.io/drpc v0.0.34/go.mod h1:Y9LZaa8esL1PW2IDMqJE7CFSNq7d5bQ3RI7mGPtmKMg= +zombiezen.com/go/sqlite v1.3.0 h1:98g1gnCm+CNz6AuQHu0gqyw7gR2WU3O3PJufDOStpUs= +zombiezen.com/go/sqlite v1.3.0/go.mod h1:yRl27//s/9aXU3RWs8uFQwjkTG9gYNGEls6+6SvrclY= diff --git a/pkg/lib/bundle/relation.gen.go b/pkg/lib/bundle/relation.gen.go index b94e08bad..f0bded267 100644 --- a/pkg/lib/bundle/relation.gen.go +++ b/pkg/lib/bundle/relation.gen.go @@ -9,7 +9,7 @@ import ( "github.com/anyproto/anytype-heart/pkg/lib/pb/model" ) -const RelationChecksum = "031657c702d56fbad494e48152066b3e7e659d9d95dc67e5ef31378f2c0fd81e" +const RelationChecksum = "68778695c5d49dd1ffbab91e13bd9769d80fd89de3a77902cec1c5066e5495a8" const ( RelationKeyTag domain.RelationKey = "tag" RelationKeyCamera domain.RelationKey = "camera" @@ -141,6 +141,7 @@ const ( RelationKeySyncStatus domain.RelationKey = "syncStatus" RelationKeySyncDate domain.RelationKey = "syncDate" RelationKeySyncError domain.RelationKey = "syncError" + RelationKeyMentions domain.RelationKey = "mentions" ) var ( @@ -1082,6 +1083,19 @@ var ( ReadOnlyRelation: true, Scope: model.Relation_type, }, + RelationKeyMentions: { + + DataSource: model.Relation_local, + Description: "Objects that are mentioned in blocks of this object", + Format: model.RelationFormat_object, + Hidden: true, + Id: "_brmentions", + Key: "mentions", + Name: "Mentions", + ReadOnly: true, + ReadOnlyRelation: true, + Scope: model.Relation_type, + }, RelationKeyMood: { DataSource: model.Relation_details, diff --git a/pkg/lib/bundle/relations.json b/pkg/lib/bundle/relations.json index d70306399..b72138981 100644 --- a/pkg/lib/bundle/relations.json +++ b/pkg/lib/bundle/relations.json @@ -1328,5 +1328,15 @@ "name": "Sync error", "readonly": true, "source": "local" + }, + { + "description": "Objects that are mentioned in blocks of this object", + "format": "object", + "hidden": true, + "key": "mentions", + "maxCount": 0, + "name": "Mentions", + "readonly": true, + "source": "local" } ] diff --git a/pkg/lib/bundle/systemRelations.gen.go b/pkg/lib/bundle/systemRelations.gen.go index d64a116bd..ce55e7ee6 100644 --- a/pkg/lib/bundle/systemRelations.gen.go +++ b/pkg/lib/bundle/systemRelations.gen.go @@ -6,7 +6,7 @@ package bundle import domain "github.com/anyproto/anytype-heart/core/domain" -const SystemRelationsChecksum = "71704f9c0a69b84194e54b4db1d4709f863b2eb7cb56e7635afd73d212c8bf0e" +const SystemRelationsChecksum = "86abb054e3ab3db5feb72fee757ed9e3aaa012e3137c5f99e1baba8cfcf3c3f2" // SystemRelations contains relations that have some special biz logic depends on them in some objects // in case EVERY object depend on the relation please add it to RequiredInternalRelations @@ -79,4 +79,5 @@ var SystemRelations = append(RequiredInternalRelations, []domain.RelationKey{ RelationKeyGlobalName, RelationKeyScope, RelationKeyLastUsedDate, + RelationKeyMentions, }...) diff --git a/pkg/lib/bundle/systemRelations.json b/pkg/lib/bundle/systemRelations.json index db5cc98f6..0bcf37d55 100644 --- a/pkg/lib/bundle/systemRelations.json +++ b/pkg/lib/bundle/systemRelations.json @@ -87,5 +87,6 @@ "syncStatus", "syncError", "scope", - "lastUsedDate" + "lastUsedDate", + "mentions" ] diff --git a/pkg/lib/bundle/types.gen.go b/pkg/lib/bundle/types.gen.go index 8c1d48488..d52b8ab6c 100644 --- a/pkg/lib/bundle/types.gen.go +++ b/pkg/lib/bundle/types.gen.go @@ -9,7 +9,7 @@ import ( "github.com/anyproto/anytype-heart/pkg/lib/pb/model" ) -const TypeChecksum = "745e40062786ba9e43273b0009612a02af1eb9920ecaf6fab191f86b5cdd33f3" +const TypeChecksum = "8b79add6b322acbcee123ad178c1aee523a2d9b9f219c391903530b29e554a1e" const ( TypePrefix = "_ot" ) @@ -54,6 +54,7 @@ var ( Readonly: true, RelationLinks: []*model.RelationLink{MustGetRelationLink(RelationKeyArtist), MustGetRelationLink(RelationKeyAudioAlbum), MustGetRelationLink(RelationKeyAudioAlbumTrackNumber), MustGetRelationLink(RelationKeyAudioGenre), MustGetRelationLink(RelationKeyAudioLyrics), MustGetRelationLink(RelationKeyReleasedYear), MustGetRelationLink(RelationKeySizeInBytes), MustGetRelationLink(RelationKeyFileMimeType), MustGetRelationLink(RelationKeyAddedDate), MustGetRelationLink(RelationKeyFileExt), MustGetRelationLink(RelationKeyOrigin)}, RestrictObjectCreation: true, + Revision: 1, Types: []model.SmartBlockType{model.SmartBlockType_File}, Url: TypePrefix + "audio", }, @@ -144,6 +145,7 @@ var ( Readonly: true, RelationLinks: []*model.RelationLink{MustGetRelationLink(RelationKeyFileMimeType), MustGetRelationLink(RelationKeySizeInBytes), MustGetRelationLink(RelationKeyAddedDate), MustGetRelationLink(RelationKeyFileExt), MustGetRelationLink(RelationKeyOrigin)}, RestrictObjectCreation: true, + Revision: 1, Types: []model.SmartBlockType{model.SmartBlockType_File}, Url: TypePrefix + "file", }, @@ -167,6 +169,7 @@ var ( Readonly: true, RelationLinks: []*model.RelationLink{MustGetRelationLink(RelationKeyFileMimeType), MustGetRelationLink(RelationKeyWidthInPixels), MustGetRelationLink(RelationKeyCamera), MustGetRelationLink(RelationKeyHeightInPixels), MustGetRelationLink(RelationKeySizeInBytes), MustGetRelationLink(RelationKeyCameraIso), MustGetRelationLink(RelationKeyAperture), MustGetRelationLink(RelationKeyExposure), MustGetRelationLink(RelationKeyAddedDate), MustGetRelationLink(RelationKeyFocalRatio), MustGetRelationLink(RelationKeyFileExt), MustGetRelationLink(RelationKeyOrigin)}, RestrictObjectCreation: true, + Revision: 1, Types: []model.SmartBlockType{model.SmartBlockType_File}, Url: TypePrefix + "image", }, @@ -351,6 +354,7 @@ var ( Readonly: true, RelationLinks: []*model.RelationLink{MustGetRelationLink(RelationKeySizeInBytes), MustGetRelationLink(RelationKeyFileMimeType), MustGetRelationLink(RelationKeyCamera), MustGetRelationLink(RelationKeyHeightInPixels), MustGetRelationLink(RelationKeyWidthInPixels), MustGetRelationLink(RelationKeyCameraIso), MustGetRelationLink(RelationKeyAperture), MustGetRelationLink(RelationKeyExposure), MustGetRelationLink(RelationKeyAddedDate), MustGetRelationLink(RelationKeyFileExt), MustGetRelationLink(RelationKeyOrigin)}, RestrictObjectCreation: true, + Revision: 1, Types: []model.SmartBlockType{model.SmartBlockType_File}, Url: TypePrefix + "video", }, diff --git a/pkg/lib/bundle/types.json b/pkg/lib/bundle/types.json index 6bb2e2766..8184ae50a 100644 --- a/pkg/lib/bundle/types.json +++ b/pkg/lib/bundle/types.json @@ -156,7 +156,8 @@ "origin" ], "description": "The recording of moving visual images", - "restrictObjectCreation": true + "restrictObjectCreation": true, + "revision": 1 }, { "id": "dashboard", @@ -358,7 +359,8 @@ "origin" ], "description": "A representation of the external form of a person or thing in art", - "restrictObjectCreation": true + "restrictObjectCreation": true, + "revision": 1 }, { "id": "profile", @@ -398,7 +400,8 @@ "origin" ], "description": "Sound when recorded, with ability to reproduce", - "restrictObjectCreation": true + "restrictObjectCreation": true, + "revision": 1 }, { "id": "goal", @@ -435,7 +438,8 @@ "origin" ], "description": "Computer resource for recording data in a computer storage device", - "restrictObjectCreation": true + "restrictObjectCreation": true, + "revision": 1 }, { "id": "project", diff --git a/pkg/lib/database/filter.go b/pkg/lib/database/filter.go index bda65e83d..fb668ba02 100644 --- a/pkg/lib/database/filter.go +++ b/pkg/lib/database/filter.go @@ -1,6 +1,7 @@ package database import ( + "bytes" "errors" "fmt" "regexp" @@ -116,6 +117,15 @@ func makeFilterByCondition(spaceID string, rawFilter *model.BlockContentDataview Value: pbtypes.Bool(true), } } + + if str := rawFilter.Value.GetStructValue(); str != nil { + filter, err := makeComplexFilter(rawFilter, str) + if err == nil { + return filter, nil + } + log.Errorf("failed to build complex filter: %v", err) + } + switch rawFilter.Condition { case model.BlockContentDataviewFilter_Equal, model.BlockContentDataviewFilter_Greater, @@ -203,6 +213,19 @@ func makeFilterByCondition(spaceID string, rawFilter *model.BlockContentDataview } } +func makeComplexFilter(rawFilter *model.BlockContentDataviewFilter, s *types.Struct) (Filter, error) { + filterType := pbtypes.GetString(s, bundle.RelationKeyType.String()) + // TODO: rewrite to switch statement once we have more filter types + if filterType == "valueFromRelation" { + return Filter2ValuesComp{ + Key1: rawFilter.RelationKey, + Key2: pbtypes.GetString(s, bundle.RelationKeyRelationKey.String()), + Cond: rawFilter.Condition, + }, nil + } + return nil, fmt.Errorf("unsupported type of complex filter: %s", filterType) +} + type WithNestedFilter interface { IterateNestedFilters(func(nestedFilter Filter) error) error } @@ -845,3 +868,93 @@ func (i *FilterNestedNotIn) AnystoreFilter() query.Filter { func (i *FilterNestedNotIn) IterateNestedFilters(fn func(nestedFilter Filter) error) error { return fn(i) } + +type Filter2ValuesComp struct { + Key1, Key2 string + Cond model.BlockContentDataviewFilterCondition +} + +func (i Filter2ValuesComp) FilterObject(g *types.Struct) bool { + val1 := pbtypes.Get(g, i.Key1) + val2 := pbtypes.Get(g, i.Key2) + eq := FilterEq{Value: val2, Cond: i.Cond} + return eq.filterObject(val1) +} + +func (i Filter2ValuesComp) AnystoreFilter() query.Filter { + var op query.CompOp + switch i.Cond { + case model.BlockContentDataviewFilter_Equal: + op = query.CompOpEq + case model.BlockContentDataviewFilter_Greater: + op = query.CompOpGt + case model.BlockContentDataviewFilter_GreaterOrEqual: + op = query.CompOpGte + case model.BlockContentDataviewFilter_Less: + op = query.CompOpLt + case model.BlockContentDataviewFilter_LessOrEqual: + op = query.CompOpLte + case model.BlockContentDataviewFilter_NotEqual: + op = query.CompOpNe + } + return &Anystore2ValuesComp{ + RelationKey1: i.Key1, + RelationKey2: i.Key2, + CompOp: op, + } +} + +type Anystore2ValuesComp struct { + RelationKey1, RelationKey2 string + CompOp query.CompOp + buf1, buf2 []byte +} + +func (e *Anystore2ValuesComp) Ok(v *fastjson.Value) bool { + value1 := v.Get(e.RelationKey1) + value2 := v.Get(e.RelationKey2) + e.buf1 = encoding.AppendJSONValue(e.buf1[:0], value1) + e.buf2 = encoding.AppendJSONValue(e.buf2[:0], value2) + comp := bytes.Compare(e.buf1, e.buf2) + switch e.CompOp { + case query.CompOpEq: + return comp == 0 + case query.CompOpGt: + return comp > 0 + case query.CompOpGte: + return comp >= 0 + case query.CompOpLt: + return comp < 0 + case query.CompOpLte: + return comp <= 0 + case query.CompOpNe: + return comp != 0 + default: + panic(fmt.Errorf("unexpected comp op: %v", e.CompOp)) + } +} + +func (e *Anystore2ValuesComp) IndexBounds(_ string, bs query.Bounds) (bounds query.Bounds) { + return bs +} + +func (e *Anystore2ValuesComp) String() string { + var comp string + switch e.CompOp { + case query.CompOpEq: + comp = "$eq" + case query.CompOpGt: + comp = "$gt" + case query.CompOpGte: + comp = "$gte" + case query.CompOpLt: + comp = "$lt" + case query.CompOpLte: + comp = "$lte" + case query.CompOpNe: + comp = "$ne" + default: + panic(fmt.Errorf("unexpected comp op: %v", e.CompOp)) + } + return fmt.Sprintf(`{"$comp_values": {"%s": ["%s", "%s"]}}`, comp, e.RelationKey1, e.RelationKey2) +} diff --git a/pkg/lib/database/filter_test.go b/pkg/lib/database/filter_test.go index 93b3496c5..e3273abfc 100644 --- a/pkg/lib/database/filter_test.go +++ b/pkg/lib/database/filter_test.go @@ -4,7 +4,6 @@ import ( "testing" "time" - "github.com/anyproto/any-store/query" "github.com/gogo/protobuf/types" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/mock" @@ -19,11 +18,10 @@ import ( func assertFilter(t *testing.T, f Filter, obj *types.Struct, expected bool) { assert.Equal(t, expected, f.FilterObject(obj)) anystoreFilter := f.AnystoreFilter() - _, err := query.ParseCondition(anystoreFilter.String()) - require.NoError(t, err, anystoreFilter.String()) arena := &fastjson.Arena{} val := pbtypes.ProtoToJson(arena, obj) - assert.Equal(t, expected, anystoreFilter.Ok(val)) + result := anystoreFilter.Ok(val) + assert.Equal(t, expected, result) } func TestEq_FilterObject(t *testing.T) { @@ -886,3 +884,50 @@ func TestMakeFilters(t *testing.T) { assert.NotNil(t, filters.(FiltersOr)[1].(FilterEq)) }) } + +func TestFilter2ValuesComp_FilterObject(t *testing.T) { + t.Run("equal", func(t *testing.T) { + eq := Filter2ValuesComp{ + Key1: "a", + Key2: "b", + Cond: model.BlockContentDataviewFilter_Equal, + } + obj1 := &types.Struct{Fields: map[string]*types.Value{ + "a": pbtypes.String("x"), + "b": pbtypes.String("x"), + }} + obj2 := &types.Struct{Fields: map[string]*types.Value{ + "a": pbtypes.String("x"), + "b": pbtypes.String("y"), + }} + obj3 := &types.Struct{Fields: map[string]*types.Value{ + "b": pbtypes.String("x"), + }} + assertFilter(t, eq, obj1, true) + assertFilter(t, eq, obj2, false) + assertFilter(t, eq, obj3, false) + }) + + t.Run("greater", func(t *testing.T) { + eq := Filter2ValuesComp{ + Key1: "a", + Key2: "b", + Cond: model.BlockContentDataviewFilter_Greater, + } + obj1 := &types.Struct{Fields: map[string]*types.Value{ + "a": pbtypes.Int64(100), + "b": pbtypes.Int64(200), + }} + obj2 := &types.Struct{Fields: map[string]*types.Value{ + "a": pbtypes.Int64(300), + "b": pbtypes.Int64(-500), + }} + obj3 := &types.Struct{Fields: map[string]*types.Value{ + "a": pbtypes.String("xxx"), + "b": pbtypes.String("ddd"), + }} + assertFilter(t, eq, obj1, false) + assertFilter(t, eq, obj2, true) + assertFilter(t, eq, obj3, true) + }) +} diff --git a/pkg/lib/localstore/objectstore/objects.go b/pkg/lib/localstore/objectstore/objects.go index 708b05ce5..474713ebd 100644 --- a/pkg/lib/localstore/objectstore/objects.go +++ b/pkg/lib/localstore/objectstore/objects.go @@ -246,6 +246,14 @@ func (s *dsObjectStore) runDatabase(ctx context.Context, path string) error { s.anyStore = store objectIndexes := []anystore.IndexInfo{ + { + Name: "uniqueKey", + Fields: []string{bundle.RelationKeyUniqueKey.String()}, + }, + { + Name: "source", + Fields: []string{bundle.RelationKeySource.String()}, + }, { Name: "layout", Fields: []string{bundle.RelationKeyLayout.String()}, @@ -254,33 +262,23 @@ func (s *dsObjectStore) runDatabase(ctx context.Context, path string) error { Name: "type", Fields: []string{bundle.RelationKeyType.String()}, }, - { - Name: "spaceId", - Fields: []string{bundle.RelationKeySpaceId.String()}, - }, { Name: "relationKey", Fields: []string{bundle.RelationKeyRelationKey.String()}, }, - { - Name: "uniqueKey", - Fields: []string{bundle.RelationKeyUniqueKey.String()}, - }, { Name: "lastModifiedDate", Fields: []string{bundle.RelationKeyLastModifiedDate.String()}, }, { - Name: "syncStatus", - Fields: []string{bundle.RelationKeySyncStatus.String()}, + Name: "fileId", + Fields: []string{bundle.RelationKeyFileId.String()}, + Sparse: true, }, { - Name: "isDeleted", - Fields: []string{bundle.RelationKeyIsDeleted.String()}, - }, - { - Name: "isArchived", - Fields: []string{bundle.RelationKeyIsArchived.String()}, + Name: "oldAnytypeID", + Fields: []string{bundle.RelationKeyOldAnytypeID.String()}, + Sparse: true, }, } err = s.addIndexes(ctx, objects, objectIndexes) @@ -315,6 +313,7 @@ func (s *dsObjectStore) runDatabase(ctx context.Context, path string) error { func (s *dsObjectStore) addIndexes(ctx context.Context, coll anystore.Collection, indexes []anystore.IndexInfo) error { gotIndexes := coll.GetIndexes() toCreate := indexes[:0] + var toDrop []string for _, idx := range indexes { if !slices.ContainsFunc(gotIndexes, func(i anystore.Index) bool { return i.Info().Name == idx.Name @@ -322,14 +321,33 @@ func (s *dsObjectStore) addIndexes(ctx context.Context, coll anystore.Collection toCreate = append(toCreate, idx) } } + for _, idx := range gotIndexes { + if !slices.ContainsFunc(indexes, func(i anystore.IndexInfo) bool { + return i.Name == idx.Info().Name + }) { + toDrop = append(toDrop, idx.Info().Name) + } + } + if len(toDrop) > 0 { + for _, indexName := range toDrop { + if err := coll.DropIndex(ctx, indexName); err != nil { + return err + } + } + } return coll.EnsureIndex(ctx, toCreate...) } +func (s *dsObjectStore) WriteTx(ctx context.Context) (anystore.WriteTx, error) { + return s.anyStore.WriteTx(ctx) +} + func (s *dsObjectStore) Close(_ context.Context) (err error) { s.componentCtxCancel() if s.objects != nil { err = errors.Join(err, s.objects.Close()) } + // TODO Close collections if s.anyStore != nil { err = errors.Join(err, s.anyStore.Close()) } diff --git a/space/internal/components/dependencies/spacewithctx.go b/space/internal/components/dependencies/spacewithctx.go index aec59c95f..64eb4f773 100644 --- a/space/internal/components/dependencies/spacewithctx.go +++ b/space/internal/components/dependencies/spacewithctx.go @@ -4,9 +4,11 @@ import ( "context" "github.com/anyproto/anytype-heart/core/block/editor/smartblock" + "github.com/anyproto/anytype-heart/core/domain" ) type SpaceWithCtx interface { DoCtx(ctx context.Context, objectId string, apply func(sb smartblock.SmartBlock) error) error Id() string + DeriveObjectID(ctx context.Context, uniqueKey domain.UniqueKey) (id string, err error) } diff --git a/space/internal/components/migration/systemobjectreviser/systemobjectreviser.go b/space/internal/components/migration/systemobjectreviser/systemobjectreviser.go index 754d6902e..7da55be02 100644 --- a/space/internal/components/migration/systemobjectreviser/systemobjectreviser.go +++ b/space/internal/components/migration/systemobjectreviser/systemobjectreviser.go @@ -4,8 +4,10 @@ import ( "context" "errors" "fmt" + "strings" "github.com/anyproto/any-sync/app/logger" + "github.com/ethereum/go-ethereum/log" "github.com/gogo/protobuf/types" "github.com/samber/lo" "go.uber.org/zap" @@ -20,6 +22,7 @@ import ( "github.com/anyproto/anytype-heart/pkg/lib/pb/model" "github.com/anyproto/anytype-heart/space/internal/components/dependencies" "github.com/anyproto/anytype-heart/util/pbtypes" + "github.com/anyproto/anytype-heart/util/slice" ) type detailsSettable interface { @@ -100,6 +103,16 @@ func reviseSystemObject(ctx context.Context, log logger.CtxLogger, space depende return false, nil } details := buildDiffDetails(marketObject, localObject) + + recRelsDetail, err := checkRecommendedRelations(ctx, space, marketObject, localObject) + if err != nil { + log.Error("failed to check recommended relations", zap.Error(err)) + } + + if recRelsDetail != nil { + details = append(details, recRelsDetail) + } + if len(details) != 0 { log.Debug("updating system object", zap.String("source", source), zap.String("space", space.Id())) if err := space.DoCtx(ctx, pbtypes.GetString(localObject, bundle.RelationKeyId.String()), func(sb smartblock.SmartBlock) error { @@ -150,3 +163,44 @@ func buildDiffDetails(origin, current *types.Struct) (details []*model.Detail) { } return } + +func checkRecommendedRelations(ctx context.Context, space dependencies.SpaceWithCtx, origin, current *types.Struct) (newValue *model.Detail, err error) { + localIds := pbtypes.GetStringList(current, bundle.RelationKeyRecommendedRelations.String()) + bundledIds := pbtypes.GetStringList(origin, bundle.RelationKeyRecommendedRelations.String()) + + newIds := make([]string, 0, len(bundledIds)) + for _, bundledId := range bundledIds { + if !strings.HasPrefix(bundledId, addr.BundledRelationURLPrefix) { + return nil, fmt.Errorf("invalid recommended bundled relation id: %s. %s prefix is expected", + bundledId, addr.BundledRelationURLPrefix) + } + key := strings.TrimPrefix(bundledId, addr.BundledRelationURLPrefix) + uk, err := domain.NewUniqueKey(coresb.SmartBlockTypeRelation, key) + if err != nil { + return nil, err + } + + // we should add only system relations to object types, because non-system could be not installed to space yet + if !lo.Contains(bundle.SystemRelations, domain.RelationKey(uk.InternalKey())) { + log.Debug("recommended relation is not system, so we are not adding it to the type object", zap.String("relation key", key)) + continue + } + + id, err := space.DeriveObjectID(ctx, uk) + if err != nil { + return nil, fmt.Errorf("failed to derive recommended relation with key '%s': %w", key, err) + } + + newIds = append(newIds, id) + } + + _, added := slice.DifferenceRemovedAdded(localIds, newIds) + if len(added) == 0 { + return nil, nil + } + + return &model.Detail{ + Key: bundle.RelationKeyRecommendedRelations.String(), + Value: pbtypes.StringList(append(localIds, added...)), + }, nil +} diff --git a/space/internal/components/migration/systemobjectreviser/systemobjectreviser_test.go b/space/internal/components/migration/systemobjectreviser/systemobjectreviser_test.go index 80d6d795d..3be4e04cb 100644 --- a/space/internal/components/migration/systemobjectreviser/systemobjectreviser_test.go +++ b/space/internal/components/migration/systemobjectreviser/systemobjectreviser_test.go @@ -9,7 +9,9 @@ import ( "github.com/stretchr/testify/assert" "github.com/stretchr/testify/mock" + "github.com/anyproto/anytype-heart/core/domain" "github.com/anyproto/anytype-heart/pkg/lib/bundle" + "github.com/anyproto/anytype-heart/pkg/lib/localstore/addr" mock_space "github.com/anyproto/anytype-heart/space/clientspace/mock_clientspace" "github.com/anyproto/anytype-heart/util/pbtypes" ) @@ -217,4 +219,72 @@ func TestReviseSystemObject(t *testing.T) { assert.NoError(t, err) assert.True(t, toRevise) }) + + t.Run("recommendedRelations list is updated", func(t *testing.T) { + // given + rel := &types.Struct{Fields: map[string]*types.Value{ + bundle.RelationKeyRevision.String(): pbtypes.Int64(1), + bundle.RelationKeySourceObject.String(): pbtypes.String("_otpage"), + bundle.RelationKeyUniqueKey.String(): pbtypes.String("ot-page"), + bundle.RelationKeyRecommendedRelations.String(): pbtypes.StringList([]string{"rel-name"}), + }} + space := mock_space.NewMockSpace(t) + space.EXPECT().DoCtx(mock.Anything, mock.Anything, mock.Anything).Times(1).Return(nil) + space.EXPECT().Id().Times(1).Return("") + space.EXPECT().DeriveObjectID(mock.Anything, mock.Anything).RunAndReturn(func(_ context.Context, key domain.UniqueKey) (string, error) { + return addr.ObjectTypeKeyToIdPrefix + key.InternalKey(), nil + }).Maybe() + + // when + marketObjects["_otpage"].Fields["recommendedRelations"] = pbtypes.StringList([]string{"_brname", "_brorigin"}) + toRevise, err := reviseSystemObject(ctx, log, space, rel, marketObjects) + + // then + assert.NoError(t, err) + assert.True(t, toRevise) + }) + + t.Run("recommendedRelations list is not updated", func(t *testing.T) { + // given + rel := &types.Struct{Fields: map[string]*types.Value{ + bundle.RelationKeyRevision.String(): pbtypes.Int64(2), + bundle.RelationKeySourceObject.String(): pbtypes.String("_otpage"), + bundle.RelationKeyUniqueKey.String(): pbtypes.String("ot-page"), + bundle.RelationKeyRecommendedRelations.String(): pbtypes.StringList([]string{"rel-name", "rel-tag"}), + }} + space := mock_space.NewMockSpace(t) + space.EXPECT().DeriveObjectID(mock.Anything, mock.Anything).RunAndReturn(func(_ context.Context, key domain.UniqueKey) (string, error) { + return addr.ObjectTypeKeyToIdPrefix + key.InternalKey(), nil + }).Maybe() + + // when + marketObjects["_otpage"].Fields["recommendedRelations"] = pbtypes.StringList([]string{"_brname", "_brtag"}) + toRevise, err := reviseSystemObject(ctx, log, space, rel, marketObjects) + + // then + assert.NoError(t, err) + assert.False(t, toRevise) + }) + + t.Run("recommendedRelations list is updated by not system relations", func(t *testing.T) { + // given + rel := &types.Struct{Fields: map[string]*types.Value{ + bundle.RelationKeyRevision.String(): pbtypes.Int64(2), + bundle.RelationKeySourceObject.String(): pbtypes.String("_otpage"), + bundle.RelationKeyUniqueKey.String(): pbtypes.String("ot-page"), + bundle.RelationKeyRecommendedRelations.String(): pbtypes.StringList([]string{"rel-name"}), + }} + space := mock_space.NewMockSpace(t) + space.EXPECT().DeriveObjectID(mock.Anything, mock.Anything).RunAndReturn(func(_ context.Context, key domain.UniqueKey) (string, error) { + return addr.ObjectTypeKeyToIdPrefix + key.InternalKey(), nil + }).Maybe() + + // when + marketObjects["_otpage"].Fields["recommendedRelations"] = pbtypes.StringList([]string{"_brname", "_brtag"}) + toRevise, err := reviseSystemObject(ctx, log, space, rel, marketObjects) + + // then + assert.NoError(t, err) + assert.False(t, toRevise) + }) } diff --git a/util/builtinobjects/data/get_started.zip b/util/builtinobjects/data/get_started.zip index 91a32e38d..e2a2bedd1 100644 Binary files a/util/builtinobjects/data/get_started.zip and b/util/builtinobjects/data/get_started.zip differ