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:
parent
0123ea0600
commit
5811b39a86
4 changed files with 306 additions and 0 deletions
|
@ -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)
|
||||
|
|
|
@ -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 {
|
||||
|
|
237
core/block/source/sub_object_links_migration.go
Normal file
237
core/block/source/sub_object_links_migration.go
Normal 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
|
||||
}
|
||||
}
|
55
core/block/source/sub_object_links_migration_test.go
Normal file
55
core/block/source/sub_object_links_migration_test.go
Normal 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()
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
Loading…
Add table
Add a link
Reference in a new issue