1
0
Fork 0
mirror of https://github.com/anyproto/anytype-heart.git synced 2025-06-10 10:00:46 +09:00

GO-3964 Merge main

This commit is contained in:
kirillston 2024-09-12 18:04:53 +02:00
commit 708b51e273
No known key found for this signature in database
GPG key ID: 88218A7F1109754B
47 changed files with 1048 additions and 269 deletions

View file

@ -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
fi

View file

@ -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"

View file

@ -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)
@cp -r $(TANTIVY_GO_PATH)/libs/* $(OUTPUT_DIR)

View file

@ -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

View file

@ -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 {

View file

@ -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

View file

@ -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) {

View file

@ -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()

View file

@ -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])

View file

@ -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,

View file

@ -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 {

View file

@ -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,
}

View file

@ -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
}

View file

@ -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),

View file

@ -0,0 +1,216 @@
package lastused
import (
"context"
"fmt"
"strings"
"sync"
"time"
"github.com/anyproto/any-sync/app"
"github.com/anyproto/any-sync/app/logger"
"github.com/cheggaaa/mb/v3"
"github.com/gogo/protobuf/types"
"go.uber.org/zap"
"github.com/anyproto/anytype-heart/core/block/editor/smartblock"
"github.com/anyproto/anytype-heart/core/domain"
"github.com/anyproto/anytype-heart/pkg/lib/bundle"
coresb "github.com/anyproto/anytype-heart/pkg/lib/core/smartblock"
"github.com/anyproto/anytype-heart/pkg/lib/localstore/addr"
"github.com/anyproto/anytype-heart/pkg/lib/localstore/objectstore"
"github.com/anyproto/anytype-heart/space"
"github.com/anyproto/anytype-heart/space/clientspace"
"github.com/anyproto/anytype-heart/util/pbtypes"
)
const (
CName = "object-usage-updater"
maxInstallationTime = 5 * time.Minute
updateInterval = 5 * time.Second
)
type Key interface {
URL() string
String() string
}
type message struct {
spaceId string
key Key
time int64
}
var log = logger.NewNamed("update-last-used-date")
type ObjectUsageUpdater interface {
app.ComponentRunnable
UpdateLastUsedDate(spaceId string, key Key, timeStamp int64)
}
func New() ObjectUsageUpdater {
return &updater{}
}
type updater struct {
store objectstore.ObjectStore
spaceService space.Service
ctx context.Context
cancel context.CancelFunc
msgBatch *mb.MB[message]
}
func (u *updater) Name() string {
return CName
}
func (u *updater) Init(a *app.App) error {
u.store = app.MustComponent[objectstore.ObjectStore](a)
u.spaceService = app.MustComponent[space.Service](a)
u.msgBatch = mb.New[message](0)
return nil
}
func (u *updater) Run(context.Context) error {
u.ctx, u.cancel = context.WithCancel(context.Background())
go u.lastUsedUpdateHandler()
return nil
}
func (u *updater) Close(context.Context) error {
if u.cancel != nil {
u.cancel()
}
if err := u.msgBatch.Close(); err != nil {
log.Error("failed to close message batch", zap.Error(err))
}
return nil
}
func (u *updater) UpdateLastUsedDate(spaceId string, key Key, ts int64) {
if err := u.msgBatch.Add(u.ctx, message{spaceId: spaceId, key: key, time: ts}); err != nil {
log.Error("failed to add last used date info to message batch", zap.Error(err), zap.String("key", key.String()))
}
}
func (u *updater) lastUsedUpdateHandler() {
var (
accumulator = make(map[string]map[Key]int64)
lock sync.Mutex
)
go func() {
for {
select {
case <-u.ctx.Done():
return
case <-time.After(updateInterval):
lock.Lock()
if len(accumulator) == 0 {
lock.Unlock()
continue
}
for spaceId, keys := range accumulator {
log.Debug("updating lastUsedDate for objects in space", zap.Int("objects num", len(keys)), zap.String("spaceId", spaceId))
u.updateLastUsedDateForKeysInSpace(spaceId, keys)
}
accumulator = make(map[string]map[Key]int64)
lock.Unlock()
}
}
}()
for {
msgs, err := u.msgBatch.Wait(u.ctx)
if err != nil {
return
}
lock.Lock()
for _, msg := range msgs {
if keys := accumulator[msg.spaceId]; keys != nil {
keys[msg.key] = msg.time
} else {
keys = map[Key]int64{
msg.key: msg.time,
}
accumulator[msg.spaceId] = keys
}
}
lock.Unlock()
}
}
func (u *updater) updateLastUsedDateForKeysInSpace(spaceId string, keys map[Key]int64) {
spc, err := u.spaceService.Get(u.ctx, spaceId)
if err != nil {
log.Error("failed to get space", zap.String("spaceId", spaceId), zap.Error(err))
return
}
for key, timeStamp := range keys {
if err = u.updateLastUsedDate(spc, key, timeStamp); err != nil {
log.Error("failed to update last used date", zap.String("spaceId", spaceId), zap.String("key", key.String()), zap.Error(err))
}
}
}
func (u *updater) updateLastUsedDate(spc clientspace.Space, key Key, ts int64) error {
uk, err := domain.UnmarshalUniqueKey(key.URL())
if err != nil {
return fmt.Errorf("failed to unmarshall key: %w", err)
}
if uk.SmartblockType() != coresb.SmartBlockTypeObjectType && uk.SmartblockType() != coresb.SmartBlockTypeRelation {
return fmt.Errorf("cannot update lastUsedDate for object with invalid smartBlock type. Only object types and relations are expected")
}
details, err := u.store.GetObjectByUniqueKey(spc.Id(), uk)
if err != nil {
return fmt.Errorf("failed to get details: %w", err)
}
id := pbtypes.GetString(details.Details, bundle.RelationKeyId.String())
if id == "" {
return fmt.Errorf("failed to get id from details: %w", err)
}
if err = spc.DoCtx(u.ctx, id, func(sb smartblock.SmartBlock) error {
st := sb.NewState()
st.SetLocalDetail(bundle.RelationKeyLastUsedDate.String(), pbtypes.Int64(ts))
return sb.Apply(st)
}); err != nil {
return fmt.Errorf("failed to set lastUsedDate to object: %w", err)
}
return nil
}
func SetLastUsedDateForInitialObjectType(id string, details *types.Struct) {
if !strings.HasPrefix(id, addr.BundledObjectTypeURLPrefix) || details == nil || details.Fields == nil {
return
}
var priority int64
switch id {
case bundle.TypeKeyNote.BundledURL():
priority = 1
case bundle.TypeKeyPage.BundledURL():
priority = 2
case bundle.TypeKeyTask.BundledURL():
priority = 3
case bundle.TypeKeySet.BundledURL():
priority = 4
case bundle.TypeKeyCollection.BundledURL():
priority = 5
default:
priority = 7
}
// we do this trick to order crucial Anytype object types by last date
lastUsed := time.Now().Add(time.Duration(-1 * priority * int64(maxInstallationTime))).Unix()
details.Fields[bundle.RelationKeyLastUsedDate.String()] = pbtypes.Int64(lastUsed)
}

View file

@ -0,0 +1,139 @@
package lastused
import (
"context"
"math/rand"
"testing"
"time"
"github.com/gogo/protobuf/types"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/mock"
"github.com/stretchr/testify/require"
"github.com/anyproto/anytype-heart/core/block/editor/smartblock"
"github.com/anyproto/anytype-heart/core/block/editor/smartblock/smarttest"
"github.com/anyproto/anytype-heart/core/domain"
"github.com/anyproto/anytype-heart/pkg/lib/bundle"
"github.com/anyproto/anytype-heart/pkg/lib/localstore/objectstore"
"github.com/anyproto/anytype-heart/space/clientspace"
"github.com/anyproto/anytype-heart/space/clientspace/mock_clientspace"
"github.com/anyproto/anytype-heart/util/pbtypes"
)
func TestSetLastUsedDateForInitialType(t *testing.T) {
isLastUsedDateGreater := func(details1, details2 *types.Struct) bool {
return pbtypes.GetInt64(details1, bundle.RelationKeyLastUsedDate.String()) > pbtypes.GetInt64(details2, bundle.RelationKeyLastUsedDate.String())
}
t.Run("object types are sorted by lastUsedDate in correct order", func(t *testing.T) {
// given
ots := []string{
bundle.TypeKeySet.BundledURL(),
bundle.TypeKeyNote.BundledURL(),
bundle.TypeKeyCollection.BundledURL(),
bundle.TypeKeyTask.BundledURL(),
bundle.TypeKeyPage.BundledURL(),
bundle.TypeKeyDiaryEntry.BundledURL(),
bundle.TypeKeyAudio.BundledURL(),
}
rand.Shuffle(len(ots), func(i, j int) {
ots[i], ots[j] = ots[j], ots[i]
})
detailMap := map[string]*types.Struct{}
// when
for _, id := range ots {
details := &types.Struct{Fields: make(map[string]*types.Value)}
SetLastUsedDateForInitialObjectType(id, details)
detailMap[id] = details
}
// then
assert.True(t, isLastUsedDateGreater(detailMap[bundle.TypeKeyNote.BundledURL()], detailMap[bundle.TypeKeyPage.BundledURL()]))
assert.True(t, isLastUsedDateGreater(detailMap[bundle.TypeKeyPage.BundledURL()], detailMap[bundle.TypeKeyTask.BundledURL()]))
assert.True(t, isLastUsedDateGreater(detailMap[bundle.TypeKeyTask.BundledURL()], detailMap[bundle.TypeKeySet.BundledURL()]))
assert.True(t, isLastUsedDateGreater(detailMap[bundle.TypeKeySet.BundledURL()], detailMap[bundle.TypeKeyCollection.BundledURL()]))
assert.True(t, isLastUsedDateGreater(detailMap[bundle.TypeKeyCollection.BundledURL()], detailMap[bundle.TypeKeyAudio.BundledURL()]))
assert.True(t, isLastUsedDateGreater(detailMap[bundle.TypeKeyCollection.BundledURL()], detailMap[bundle.TypeKeyDiaryEntry.BundledURL()]))
})
}
func TestUpdateLastUsedDate(t *testing.T) {
const spaceId = "space"
ts := time.Now().Unix()
isLastUsedDateRecent := func(details *types.Struct, deltaSeconds int64) bool {
return pbtypes.GetInt64(details, bundle.RelationKeyLastUsedDate.String())+deltaSeconds > time.Now().Unix()
}
store := objectstore.NewStoreFixture(t)
store.AddObjects(t, []objectstore.TestObject{
{
bundle.RelationKeyId: pbtypes.String(bundle.RelationKeyCamera.URL()),
bundle.RelationKeySpaceId: pbtypes.String(spaceId),
bundle.RelationKeyUniqueKey: pbtypes.String(bundle.RelationKeyCamera.URL()),
},
{
bundle.RelationKeyId: pbtypes.String(bundle.TypeKeyDiaryEntry.URL()),
bundle.RelationKeySpaceId: pbtypes.String(spaceId),
bundle.RelationKeyUniqueKey: pbtypes.String(bundle.TypeKeyDiaryEntry.URL()),
},
{
bundle.RelationKeyId: pbtypes.String("rel-custom"),
bundle.RelationKeySpaceId: pbtypes.String(spaceId),
bundle.RelationKeyUniqueKey: pbtypes.String("rel-custom"),
},
{
bundle.RelationKeyId: pbtypes.String("opt-done"),
bundle.RelationKeySpaceId: pbtypes.String(spaceId),
bundle.RelationKeyUniqueKey: pbtypes.String("opt-done"),
},
})
u := updater{store: store}
getSpace := func() clientspace.Space {
spc := mock_clientspace.NewMockSpace(t)
spc.EXPECT().Id().Return(spaceId)
spc.EXPECT().DoCtx(mock.Anything, mock.Anything, mock.Anything).RunAndReturn(func(_ context.Context, id string, apply func(smartblock.SmartBlock) error) error {
sb := smarttest.New(id)
err := apply(sb)
require.NoError(t, err)
assert.True(t, isLastUsedDateRecent(sb.LocalDetails(), 5))
return nil
})
return spc
}
for _, tc := range []struct {
name string
key Key
getSpace func() clientspace.Space
isErrorExpected bool
}{
{"built-in relation", bundle.RelationKeyCamera, getSpace, false},
{"built-in type", bundle.TypeKeyDiaryEntry, getSpace, false},
{"custom relation", domain.RelationKey("custom"), getSpace, false},
{"option", domain.TypeKey("opt-done"), func() clientspace.Space {
spc := mock_clientspace.NewMockSpace(t)
return spc
}, true},
{"type that is not in store", bundle.TypeKeyAudio, func() clientspace.Space {
spc := mock_clientspace.NewMockSpace(t)
spc.EXPECT().Id().Return(spaceId)
return spc
}, true},
} {
t.Run("update lastUsedDate of "+tc.name, func(t *testing.T) {
err := u.updateLastUsedDate(tc.getSpace(), tc.key, ts)
if tc.isErrorExpected {
assert.Error(t, err)
} else {
assert.NoError(t, err)
}
})
}
}

View file

@ -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)
}

View file

@ -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()]))
})
}

View file

@ -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,

View file

@ -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,

View file

@ -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,

View file

@ -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,

View file

@ -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 {

View file

@ -1454,6 +1454,7 @@ func (sb *smartBlock) injectDerivedDetails(s *state.State, spaceID string, sbt s
}
sb.injectLinksDetails(s)
sb.injectMentions(s)
sb.updateBackLinks(s)
}

View file

@ -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,

View file

@ -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,

View file

@ -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
}

View file

@ -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())

View file

@ -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

View file

@ -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)
}

62
docs/ComplexFilters.md Normal file
View file

@ -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"
}
}
}
}
}
```

View file

@ -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 <path_to_archive>`
@ -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
More examples could be found in **rules.json** file

33
go.mod
View file

@ -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

81
go.sum
View file

@ -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=

View file

@ -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,

View file

@ -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"
}
]

View file

@ -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,
}...)

View file

@ -87,5 +87,6 @@
"syncStatus",
"syncError",
"scope",
"lastUsedDate"
"lastUsedDate",
"mentions"
]

View file

@ -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",
},

View file

@ -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",

View file

@ -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)
}

View file

@ -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)
})
}

View file

@ -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())
}

View file

@ -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)
}

View file

@ -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
}

View file

@ -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)
})
}