1
0
Fork 0
mirror of https://github.com/anyproto/anytype-heart.git synced 2025-06-08 05:47:07 +09:00

GO-5717: Rever sub-objects migration

This commit is contained in:
Sergey 2025-05-27 17:46:37 +02:00
parent 0123ea0600
commit 5811b39a86
No known key found for this signature in database
GPG key ID: 3B6BEF79160221C6
4 changed files with 306 additions and 0 deletions

View file

@ -360,6 +360,9 @@ func (sb *smartBlock) Init(ctx *InitContext) (err error) {
}
}
ctx.State.AddBundledRelationLinks(relKeys...)
if ctx.IsNewObject && ctx.State != nil {
source.NewSubObjectsAndProfileLinksMigration(sb.Type(), sb.space, sb.currentParticipantId, sb.spaceIndex).Migrate(ctx.State)
}
if err = sb.injectLocalDetails(ctx.State); err != nil {
return
@ -848,6 +851,7 @@ func (sb *smartBlock) Apply(s *state.State, flags ...ApplyFlag) (err error) {
}
func (sb *smartBlock) ResetToVersion(s *state.State) (err error) {
source.NewSubObjectsAndProfileLinksMigration(sb.Type(), sb.space, sb.currentParticipantId, sb.spaceIndex).Migrate(s)
s.SetParent(sb.Doc.(*state.State))
sb.storeFileKeys(s)
sb.injectLocalDetails(s)

View file

@ -284,6 +284,16 @@ func (s *treeSource) buildState() (doc state.Doc, err error) {
}
st.BlocksInit(st)
// This is temporary migration. We will move it to persistent migration later after several releases.
// The reason is to minimize the number of glitches for users of both old and new versions of Anytype.
// For example, if we persist this migration for Dataview block now, user will see "No query selected"
// error in the old version of Anytype. We want to avoid this as much as possible by making this migration
// temporary, though the applying change to this Dataview block will persist this migration, breaking backward
// compatibility. But in many cases we expect that users update object not so often as they just view them.
// TODO: we can skip migration for non-personal spaces
migration := source.NewSubObjectsAndProfileLinksMigration(s.smartblockType, s.space, s.accountService.MyParticipantId(s.spaceID), s.objectStore)
migration.Migrate(st)
// we need to have required internal relations for all objects, including system
st.AddBundledRelationLinks(bundle.RequiredInternalRelations...)
if s.Type() == smartblock.SmartBlockTypePage || s.Type() == smartblock.SmartBlockTypeProfilePage {

View file

@ -0,0 +1,237 @@
package source
import (
"context"
"fmt"
"strings"
"github.com/globalsign/mgo/bson"
"github.com/gogo/protobuf/types"
"github.com/anyproto/anytype-heart/core/block/editor/state"
"github.com/anyproto/anytype-heart/core/block/simple"
dataview2 "github.com/anyproto/anytype-heart/core/block/simple/dataview"
"github.com/anyproto/anytype-heart/core/domain"
"github.com/anyproto/anytype-heart/pkg/lib/bundle"
"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/spaceindex"
"github.com/anyproto/anytype-heart/pkg/lib/logging"
"github.com/anyproto/anytype-heart/pkg/lib/pb/model"
"github.com/anyproto/anytype-heart/util/pbtypes"
)
var log = logging.Logger("anytype-mw-source-migration")
// Migrate old relation (rel-name, etc.) and object type (ot-page, etc.) IDs to new ones (just ordinary object IDs)
// Those old ids are ids of sub-objects, legacy system for storing types and relations inside workspace object
type subObjectsAndProfileLinksMigration struct {
profileID string
identityObjectID string
sbType smartblock.SmartBlockType
space Space
objectStore spaceindex.Store
}
func NewSubObjectsAndProfileLinksMigration(sbType smartblock.SmartBlockType, space Space, identityObjectID string, objectStore spaceindex.Store) *subObjectsAndProfileLinksMigration {
return &subObjectsAndProfileLinksMigration{
space: space,
identityObjectID: identityObjectID,
sbType: sbType,
objectStore: objectStore,
}
}
func (m *subObjectsAndProfileLinksMigration) replaceLinksInDetails(s *state.State) {
for _, rel := range s.GetRelationLinks() {
if rel.Key == bundle.RelationKeyFeaturedRelations.String() {
continue
}
if rel.Key == bundle.RelationKeySourceObject.String() {
// migrate broken sourceObject after v0.29.11
// todo: remove this
if s.UniqueKeyInternal() == "" {
continue
}
internalKey := s.UniqueKeyInternal()
switch m.sbType {
case smartblock.SmartBlockTypeRelation:
if bundle.HasRelation(domain.RelationKey(internalKey)) {
s.SetDetail(bundle.RelationKeySourceObject, domain.String(domain.RelationKey(internalKey).BundledURL()))
}
case smartblock.SmartBlockTypeObjectType:
if bundle.HasObjectTypeByKey(domain.TypeKey(internalKey)) {
s.SetDetail(bundle.RelationKeySourceObject, domain.String(domain.TypeKey(internalKey).BundledURL()))
}
}
continue
}
if m.canRelationContainObjectValues(rel.Format) {
rawValue := s.Details().Get(domain.RelationKey(rel.Key))
if oldId := rawValue.String(); oldId != "" {
newId := m.migrateId(oldId)
if oldId != newId {
s.SetDetail(domain.RelationKey(rel.Key), domain.String(newId))
}
} else if ids := rawValue.StringList(); len(ids) > 0 {
changed := false
for i, oldId := range ids {
newId := m.migrateId(oldId)
if oldId != newId {
ids[i] = newId
changed = true
}
}
if changed {
s.SetDetail(domain.RelationKey(rel.Key), domain.StringList(ids))
}
}
}
}
}
// Migrate works only in personal space
func (m *subObjectsAndProfileLinksMigration) Migrate(s *state.State) {
if !m.space.IsPersonal() {
return
}
uk, err := domain.NewUniqueKey(smartblock.SmartBlockTypeProfilePage, "")
if err != nil {
log.Errorf("migration: failed to create unique key for profile: %s", err)
} else {
// this way we will get incorrect profileID for non-personal spaces, but we are not migrating them
id, err := m.space.DeriveObjectID(context.Background(), uk)
if err != nil {
log.Errorf("migration: failed to derive id for profile: %s", err)
} else {
m.profileID = id
}
}
m.replaceLinksInDetails(s)
s.Iterate(func(block simple.Block) bool {
if block.Model().GetDataview() != nil {
// Mark block as mutable
dv := s.Get(block.Model().Id).(dataview2.Block)
m.migrateFilters(dv)
}
if _, ok := block.(simple.ObjectLinkReplacer); ok {
// Mark block as mutable
b := s.Get(block.Model().Id)
replacer := b.(simple.ObjectLinkReplacer)
replacer.ReplaceLinkIds(m.migrateId)
}
return true
})
}
func (m *subObjectsAndProfileLinksMigration) migrateId(oldId string) (newId string) {
if m.profileID != "" && m.identityObjectID != "" {
// we substitute all links to profile object with space member object
if oldId == m.profileID ||
strings.HasPrefix(oldId, "_id_") { // we don't need to check the exact accountID here, because we only have links to our own identity
return m.identityObjectID
}
}
uniqueKey, valid := subObjectIdToUniqueKey(oldId)
if !valid {
return oldId
}
newId, err := m.space.DeriveObjectID(context.Background(), uniqueKey)
if err != nil {
log.With("uniqueKey", uniqueKey.Marshal()).Errorf("failed to derive id: %s", err)
return oldId
}
return newId
}
// subObjectIdToUniqueKey converts legacy sub-object id to uniqueKey
// if id is not supported subObjectId, it will return nil, false
// suppose to be used only for migration and almost free to use
func subObjectIdToUniqueKey(id string) (uniqueKey domain.UniqueKey, valid bool) {
// historically, we don't have the prefix for the options,
// so we need to handled it this ugly way
if bson.IsObjectIdHex(id) {
return domain.MustUniqueKey(smartblock.SmartBlockTypeRelationOption, id), true
}
// special case: we don't support bundled relations/types in uniqueKeys (GO-2394). So in case we got it, we need to replace the prefix
if strings.HasPrefix(id, addr.BundledObjectTypeURLPrefix) {
id = addr.ObjectTypeKeyToIdPrefix + strings.TrimPrefix(id, addr.BundledObjectTypeURLPrefix)
} else if strings.HasPrefix(id, addr.BundledRelationURLPrefix) {
id = addr.RelationKeyToIdPrefix + strings.TrimPrefix(id, addr.BundledRelationURLPrefix)
}
uniqueKey, err := domain.UnmarshalUniqueKey(id)
if err != nil {
return nil, false
}
return uniqueKey, true
}
func (m *subObjectsAndProfileLinksMigration) migrateFilters(dv dataview2.Block) {
for _, view := range dv.Model().GetDataview().GetViews() {
for _, filter := range view.GetFilters() {
err := m.migrateFilter(filter)
if err != nil {
log.Errorf("failed to migrate filter %s: %s", filter.Id, err)
}
}
}
}
func (m *subObjectsAndProfileLinksMigration) migrateFilter(filter *model.BlockContentDataviewFilter) error {
if filter == nil {
return nil
}
if filter.Value == nil || filter.Value.Kind == nil {
log.With("relationKey", filter.RelationKey).Warnf("empty filter value")
return nil
}
relation, err := m.objectStore.GetRelationByKey(filter.RelationKey)
if err != nil {
log.Warnf("migration: failed to get relation by key %s: %s", filter.RelationKey, err)
}
// TODO: check this logic
// here we use objectstore to get relation, but it may be not yet available
// In case it is missing, lets try to migrate any string/stringlist: it should ignore invalid strings
if relation == nil || m.canRelationContainObjectValues(relation.Format) {
switch v := filter.Value.Kind.(type) {
case *types.Value_StringValue:
filter.Value = pbtypes.String(m.migrateId(v.StringValue))
case *types.Value_ListValue:
newIDs := make([]string, 0, len(v.ListValue.Values))
for _, oldID := range v.ListValue.Values {
if id, ok := oldID.Kind.(*types.Value_StringValue); ok {
newIDs = append(newIDs, m.migrateId(id.StringValue))
} else {
return fmt.Errorf("migration: failed to migrate filter: invalid list item value kind %t", oldID.Kind)
}
}
filter.Value = pbtypes.StringList(newIDs)
}
}
return nil
}
func (m *subObjectsAndProfileLinksMigration) canRelationContainObjectValues(format model.RelationFormat) bool {
switch format {
case
model.RelationFormat_status,
model.RelationFormat_tag,
model.RelationFormat_object:
return true
default:
return false
}
}

View file

@ -0,0 +1,55 @@
package source
import (
"testing"
"github.com/anyproto/anytype-heart/core/domain"
)
func TestSubObjectIdToUniqueKey(t *testing.T) {
type args struct {
id string
}
tests := []struct {
name string
args args
wantUk string
wantValid bool
}{
{"relation", args{"rel-id"}, "rel-id", true},
{"type", args{"ot-task"}, "ot-task", true},
{"opt", args{"650832666293ae9ae67e5f9c"}, "opt-650832666293ae9ae67e5f9c", true},
{"invalid-prefix", args{"aa-task"}, "", false},
{"no-key", args{"rel"}, "", false},
{"no-key2", args{"rel-"}, "", false},
{"no-key2", args{"rel---gdfgfd--gfdgfd-"}, "", false},
{"invalid", args{"task"}, "", false},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
gotUk, gotValid := subObjectIdToUniqueKey(tt.args.id)
if gotValid != tt.wantValid {
t.Errorf("SubObjectIdToUniqueKey() gotValid = %v, want %v", gotValid, tt.wantValid)
t.Fail()
}
if !tt.wantValid {
return
}
wantUk, err := domain.UnmarshalUniqueKey(tt.wantUk)
if err != nil {
t.Errorf("SubObjectIdToUniqueKey() error = %v", err)
t.Fail()
}
if wantUk.Marshal() != gotUk.Marshal() {
t.Errorf("SubObjectIdToUniqueKey() gotUk = %v, want %v", gotUk, tt.wantUk)
t.Fail()
}
if wantUk.SmartblockType() != gotUk.SmartblockType() {
t.Errorf("SubObjectIdToUniqueKey() gotSmartblockType = %v, want %v", gotUk.SmartblockType(), wantUk.SmartblockType())
t.Fail()
}
})
}
}