1
0
Fork 0
mirror of https://github.com/anyproto/anytype-heart.git synced 2025-06-11 18:20:33 +09:00

Merge pull request #1769 from anyproto/go-4408-unify-date-object-creation

GO-4408 Unify Date object creation
This commit is contained in:
Kirill Stonozhenko 2024-11-07 11:50:36 +01:00 committed by GitHub
commit 31e066d088
Signed by: github
GPG key ID: B5690EEEBB952194
18 changed files with 398 additions and 287 deletions

View file

@ -17,8 +17,8 @@ import (
"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/database"
"github.com/anyproto/anytype-heart/pkg/lib/localstore/addr"
"github.com/anyproto/anytype-heart/space/spacecore/typeprovider"
"github.com/anyproto/anytype-heart/util/dateutil"
"github.com/anyproto/anytype-heart/util/pbtypes"
"github.com/anyproto/anytype-heart/util/slice"
)
@ -121,7 +121,7 @@ func generateFilter(value *types.Value) func(v *types.Value) bool {
return equalOrHasFilter
}
start, err := dateIDToDayStart(stringValue)
start, err := dateutil.ParseDateId(stringValue)
if err != nil {
log.Error("failed to convert date id to day start", zap.Error(err))
return equalOrHasFilter
@ -139,10 +139,3 @@ func generateFilter(value *types.Value) func(v *types.Value) bool {
return equalOrHasFilter(v)
}
}
func dateIDToDayStart(id string) (time.Time, error) {
if !strings.HasPrefix(id, addr.DatePrefix) {
return time.Time{}, fmt.Errorf("invalid id: date prefix not found")
}
return time.Parse("2006-01-02", strings.TrimPrefix(id, addr.DatePrefix))
}

View file

@ -14,9 +14,9 @@ import (
"github.com/anyproto/anytype-heart/core/block/editor/state"
"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/pb/model"
"github.com/anyproto/anytype-heart/util/dateutil"
"github.com/anyproto/anytype-heart/util/pbtypes"
)
@ -58,7 +58,7 @@ func TestService_ListRelationsWithValue(t *testing.T) {
{
bundle.RelationKeyId: pbtypes.String("obj2"),
bundle.RelationKeySpaceId: pbtypes.String(spaceId),
bundle.RelationKeyName: pbtypes.String(addr.TimeToID(now)),
bundle.RelationKeyName: pbtypes.String(dateutil.TimeToDateId(now)),
bundle.RelationKeyCreatedDate: pbtypes.Int64(now.Add(-24*time.Hour - 5*time.Minute).Unix()),
bundle.RelationKeyAddedDate: pbtypes.Int64(now.Add(-24*time.Hour - 3*time.Minute).Unix()),
bundle.RelationKeyLastModifiedDate: pbtypes.Int64(now.Add(-1 * time.Minute).Unix()),
@ -72,7 +72,7 @@ func TestService_ListRelationsWithValue(t *testing.T) {
bundle.RelationKeyLastModifiedDate: pbtypes.Int64(now.Unix()),
bundle.RelationKeyIsFavorite: pbtypes.Bool(true),
bundle.RelationKeyCoverX: pbtypes.Int64(300),
bundle.RelationKeyMentions: pbtypes.StringList([]string{addr.TimeToID(now), addr.TimeToID(now.Add(-24 * time.Hour))}),
bundle.RelationKeyMentions: pbtypes.StringList([]string{dateutil.TimeToDateId(now), dateutil.TimeToDateId(now.Add(-24 * time.Hour))}),
},
})
@ -86,13 +86,13 @@ func TestService_ListRelationsWithValue(t *testing.T) {
}{
{
"date object - today",
pbtypes.String(addr.TimeToID(now)),
pbtypes.String(dateutil.TimeToDateId(now)),
[]string{bundle.RelationKeyAddedDate.String(), bundle.RelationKeyCreatedDate.String(), bundle.RelationKeyLastModifiedDate.String(), bundle.RelationKeyMentions.String(), bundle.RelationKeyName.String()},
[]int64{1, 2, 3, 1, 1},
},
{
"date object - yesterday",
pbtypes.String(addr.TimeToID(now.Add(-24 * time.Hour))),
pbtypes.String(dateutil.TimeToDateId(now.Add(-24 * time.Hour))),
[]string{bundle.RelationKeyAddedDate.String(), bundle.RelationKeyCreatedDate.String(), bundle.RelationKeyMentions.String()},
[]int64{1, 1, 1},
},

View file

@ -46,6 +46,7 @@ import (
"github.com/anyproto/anytype-heart/pkg/lib/threads"
"github.com/anyproto/anytype-heart/space/spacecore/storage/sqlitestorage"
"github.com/anyproto/anytype-heart/util/anonymize"
"github.com/anyproto/anytype-heart/util/dateutil"
"github.com/anyproto/anytype-heart/util/internalflag"
"github.com/anyproto/anytype-heart/util/pbtypes"
"github.com/anyproto/anytype-heart/util/slice"
@ -461,10 +462,7 @@ func (sb *smartBlock) fetchMeta() (details []*model.ObjectViewDetailsSet, err er
depIds := sb.dependentSmartIds(sb.includeRelationObjectsAsDependents, true, true)
sb.setDependentIDs(depIds)
perSpace, err := sb.partitionIdsBySpace(sb.depIds)
if err != nil {
return nil, fmt.Errorf("partiton by space: %w", err)
}
perSpace := sb.partitionIdsBySpace(sb.depIds)
recordsCh := make(chan *types.Struct, 10)
sb.recordsSub = database.NewSubscription(nil, recordsCh)
@ -513,9 +511,14 @@ func (sb *smartBlock) fetchMeta() (details []*model.ObjectViewDetailsSet, err er
return
}
func (sb *smartBlock) partitionIdsBySpace(ids []string) (map[string][]string, error) {
func (sb *smartBlock) partitionIdsBySpace(ids []string) map[string][]string {
perSpace := map[string][]string{}
for _, id := range ids {
if _, parseErr := dateutil.ParseDateId(id); parseErr == nil {
perSpace[sb.space.Id()] = append(perSpace[sb.space.Id()], id)
continue
}
spaceId, err := sb.spaceIdResolver.ResolveSpaceID(id)
if errors.Is(err, sqlitestorage.ErrObjectNotFound) || errors.Is(err, badger.ErrKeyNotFound) {
perSpace[sb.space.Id()] = append(perSpace[sb.space.Id()], id)
@ -529,7 +532,7 @@ func (sb *smartBlock) partitionIdsBySpace(ids []string) (map[string][]string, er
}
perSpace[spaceId] = append(perSpace[spaceId], id)
}
return perSpace, nil
return perSpace
}
func (sb *smartBlock) Lock() {
@ -861,10 +864,7 @@ func (sb *smartBlock) CheckSubscriptions() (changed bool) {
}
newIDs := sb.recordsSub.Subscribe(sb.depIds)
perSpace, err := sb.partitionIdsBySpace(newIDs)
if err != nil {
log.Errorf("partiton by space error: %v", err)
}
perSpace := sb.partitionIdsBySpace(newIDs)
for spaceId, ids := range perSpace {
spaceIndex := sb.objectStore.SpaceIndex(spaceId)

View file

@ -2,13 +2,12 @@ package block
import (
"strings"
"time"
"github.com/globalsign/mgo/bson"
"github.com/anyproto/anytype-heart/core/block/import/notion/api"
"github.com/anyproto/anytype-heart/pkg/lib/localstore/addr"
"github.com/anyproto/anytype-heart/pkg/lib/pb/model"
"github.com/anyproto/anytype-heart/util/dateutil"
textUtil "github.com/anyproto/anytype-heart/util/text"
)
@ -240,7 +239,7 @@ func (t *TextObject) handleDateMention(rt api.RichText,
if rt.Mention.Date.End != "" {
textDate = rt.Mention.Date.End
}
date, err := time.Parse(DateMentionTimeFormat, textDate)
date, err := dateutil.ParseDateId(textDate)
if err != nil {
return nil
}
@ -253,7 +252,7 @@ func (t *TextObject) handleDateMention(rt api.RichText,
To: int32(to),
},
Type: model.BlockContentTextMark_Mention,
Param: addr.TimeToID(date),
Param: dateutil.TimeToDateId(date),
},
}
}

View file

@ -11,6 +11,7 @@ import (
"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/block/source"
"github.com/anyproto/anytype-heart/core/domain"
"github.com/anyproto/anytype-heart/pb"
"github.com/anyproto/anytype-heart/pkg/lib/bundle"
@ -20,6 +21,7 @@ import (
"github.com/anyproto/anytype-heart/pkg/lib/pb/model"
"github.com/anyproto/anytype-heart/space"
"github.com/anyproto/anytype-heart/space/clientspace"
"github.com/anyproto/anytype-heart/util/dateutil"
"github.com/anyproto/anytype-heart/util/internalflag"
"github.com/anyproto/anytype-heart/util/pbtypes"
)
@ -158,6 +160,8 @@ func (s *service) createObjectInSpace(
if pbtypes.GetString(details, bundle.RelationKeyTargetObjectType.String()) == "" {
return "", nil, fmt.Errorf("cannot create template without target object")
}
case bundle.TypeKeyDate:
return buildDateObject(space, details)
}
return s.createObjectFromTemplate(ctx, space, []domain.TypeKey{req.ObjectTypeKey}, details, req.TemplateId)
@ -176,3 +180,33 @@ func (s *service) createObjectFromTemplate(
}
return s.CreateSmartBlockFromStateInSpace(ctx, space, objectTypeKeys, createState)
}
// buildDateObject does not create real date object. It just builds date object details
func buildDateObject(space clientspace.Space, details *types.Struct) (string, *types.Struct, error) {
name := pbtypes.GetString(details, bundle.RelationKeyName.String())
id, err := dateutil.DateNameToId(name)
if err != nil {
return "", nil, fmt.Errorf("failed to build date object, as its name is invalid: %w", err)
}
typeId, err := space.GetTypeIdByKey(context.Background(), bundle.TypeKeyDate)
if err != nil {
return "", nil, fmt.Errorf("failed to find Date type to build Date object: %w", err)
}
dateSource := source.NewDate(source.DateSourceParams{
Id: domain.FullID{
ObjectID: id,
SpaceID: space.Id(),
},
DateObjectTypeId: typeId,
})
detailsGetter, ok := dateSource.(source.SourceIdEndodedDetails)
if !ok {
return "", nil, fmt.Errorf("date object does not implement DetailsFromId")
}
details, err = detailsGetter.DetailsFromId()
return id, details, err
}

View file

@ -3,6 +3,7 @@ package objectcreator
import (
"context"
"testing"
"time"
"github.com/gogo/protobuf/types"
"github.com/stretchr/testify/assert"
@ -11,10 +12,12 @@ import (
"github.com/anyproto/anytype-heart/core/block/editor/lastused/mock_lastused"
"github.com/anyproto/anytype-heart/core/block/editor/smartblock/smarttest"
"github.com/anyproto/anytype-heart/core/block/editor/state"
"github.com/anyproto/anytype-heart/core/domain"
"github.com/anyproto/anytype-heart/pkg/lib/bundle"
"github.com/anyproto/anytype-heart/space/clientspace"
"github.com/anyproto/anytype-heart/space/clientspace/mock_clientspace"
"github.com/anyproto/anytype-heart/space/mock_space"
"github.com/anyproto/anytype-heart/util/dateutil"
"github.com/anyproto/anytype-heart/util/pbtypes"
)
@ -103,4 +106,50 @@ func TestService_CreateObject(t *testing.T) {
// then
assert.Error(t, err)
})
t.Run("date object creation", func(t *testing.T) {
// given
f := newFixture(t)
f.spaceService.EXPECT().Get(mock.Anything, mock.Anything).Return(f.spc, nil)
f.spc.EXPECT().Id().Return(spaceId)
f.spc.EXPECT().GetTypeIdByKey(mock.Anything, mock.Anything).RunAndReturn(func(ctx context.Context, key domain.TypeKey) (string, error) {
assert.Equal(t, bundle.TypeKeyDate, key)
return bundle.TypeKeyDate.URL(), nil
})
ts := time.Now()
name := dateutil.TimeToDateName(ts)
// when
id, details, err := f.service.CreateObject(context.Background(), spaceId, CreateObjectRequest{
ObjectTypeKey: bundle.TypeKeyDate,
Details: &types.Struct{Fields: map[string]*types.Value{
bundle.RelationKeyName.String(): pbtypes.String(name),
}},
})
// then
assert.NoError(t, err)
assert.Equal(t, dateutil.TimeToDateId(ts), id)
assert.Equal(t, spaceId, pbtypes.GetString(details, bundle.RelationKeySpaceId.String()))
assert.Equal(t, bundle.TypeKeyDate.URL(), pbtypes.GetString(details, bundle.RelationKeyType.String()))
})
t.Run("date object creation - invalid name", func(t *testing.T) {
// given
f := newFixture(t)
f.spaceService.EXPECT().Get(mock.Anything, mock.Anything).Return(f.spc, nil)
ts := time.Now()
name := ts.Format(time.RFC3339)
// when
_, _, err := f.service.CreateObject(context.Background(), spaceId, CreateObjectRequest{
ObjectTypeKey: bundle.TypeKeyDate,
Details: &types.Struct{Fields: map[string]*types.Value{
bundle.RelationKeyName.String(): pbtypes.String(name),
}},
})
// then
assert.Error(t, err)
})
}

View file

@ -13,9 +13,9 @@ import (
"github.com/anyproto/anytype-heart/core/block/simple"
"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/logging"
"github.com/anyproto/anytype-heart/pkg/lib/pb/model"
"github.com/anyproto/anytype-heart/util/dateutil"
"github.com/anyproto/anytype-heart/util/pbtypes"
)
@ -152,7 +152,7 @@ func collectIdsFromDetail(rel *model.RelationLink, det *types.Struct, flags Flag
if relInt > 0 {
t := time.Unix(relInt, 0)
t = t.In(time.Local)
ids = append(ids, addr.TimeToID(t))
ids = append(ids, dateutil.TimeToDateId(t))
}
return
}

View file

@ -3,8 +3,6 @@ package source
import (
"context"
"fmt"
"strings"
"time"
"github.com/gogo/protobuf/types"
@ -16,179 +14,104 @@ import (
"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/pb/model"
"github.com/anyproto/anytype-heart/util/dateutil"
"github.com/anyproto/anytype-heart/util/pbtypes"
)
func NewDate(space Space, id domain.FullID) (s Source) {
type DateSourceParams struct {
Id domain.FullID
DateObjectTypeId string
}
func NewDate(params DateSourceParams) (s Source) {
return &date{
id: id.ObjectID,
spaceId: id.SpaceID,
space: space,
id: params.Id.ObjectID,
spaceId: params.Id.SpaceID,
typeId: params.DateObjectTypeId,
}
}
type date struct {
space Space
id, spaceId string
t time.Time
id, spaceId, typeId string
}
func (v *date) ListIds() ([]string, error) {
func (d *date) ListIds() ([]string, error) {
return []string{}, nil
}
func (v *date) ReadOnly() bool {
func (d *date) ReadOnly() bool {
return true
}
func (v *date) Id() string {
return v.id
func (d *date) Id() string {
return d.id
}
func (v *date) SpaceID() string {
if v.space != nil {
return v.space.Id()
}
if v.spaceId != "" {
return v.spaceId
}
return ""
func (d *date) SpaceID() string {
return d.spaceId
}
func (v *date) Type() smartblock.SmartBlockType {
func (d *date) Type() smartblock.SmartBlockType {
return smartblock.SmartBlockTypeDate
}
func (v *date) getDetails(ctx context.Context) (*types.Struct, error) {
linksRelationId, err := v.space.GetRelationIdByKey(ctx, bundle.RelationKeyLinks)
func (d *date) getDetails() (*types.Struct, error) {
t, err := dateutil.ParseDateId(d.id)
if err != nil {
return nil, fmt.Errorf("get links relation id: %w", err)
}
dateTypeId, err := v.space.GetTypeIdByKey(ctx, bundle.TypeKeyDate)
if err != nil {
return nil, fmt.Errorf("get date type id: %w", err)
return nil, fmt.Errorf("failed to parse date id: %w", err)
}
return &types.Struct{Fields: map[string]*types.Value{
bundle.RelationKeyName.String(): pbtypes.String(v.t.Format("02 Jan 2006")),
bundle.RelationKeyId.String(): pbtypes.String(v.id),
bundle.RelationKeyName.String(): pbtypes.String(dateutil.TimeToDateName(t)),
bundle.RelationKeyId.String(): pbtypes.String(d.id),
bundle.RelationKeyType.String(): pbtypes.String(d.typeId),
bundle.RelationKeyIsReadonly.String(): pbtypes.Bool(true),
bundle.RelationKeyIsArchived.String(): pbtypes.Bool(false),
bundle.RelationKeyIsHidden.String(): pbtypes.Bool(false),
bundle.RelationKeyLayout.String(): pbtypes.Float64(float64(model.ObjectType_date)),
bundle.RelationKeyIconEmoji.String(): pbtypes.String("📅"),
bundle.RelationKeySpaceId.String(): pbtypes.String(v.SpaceID()),
bundle.RelationKeySetOf.String(): pbtypes.StringList([]string{linksRelationId}),
bundle.RelationKeyType.String(): pbtypes.String(dateTypeId),
bundle.RelationKeySpaceId.String(): pbtypes.String(d.SpaceID()),
bundle.RelationKeyTimestamp.String(): pbtypes.Int64(t.Unix()),
}}, nil
}
// TODO Fix?
func (v *date) DetailsFromId() (*types.Struct, error) {
if err := v.parseId(); err != nil {
return nil, err
}
return &types.Struct{Fields: map[string]*types.Value{
bundle.RelationKeyName.String(): pbtypes.String(v.t.Format("02 Jan 2006")),
bundle.RelationKeyId.String(): pbtypes.String(v.id),
bundle.RelationKeyIsReadonly.String(): pbtypes.Bool(true),
bundle.RelationKeyIsArchived.String(): pbtypes.Bool(false),
bundle.RelationKeyIsHidden.String(): pbtypes.Bool(false),
bundle.RelationKeyLayout.String(): pbtypes.Float64(float64(model.ObjectType_date)),
bundle.RelationKeyIconEmoji.String(): pbtypes.String("📅"),
bundle.RelationKeySpaceId.String(): pbtypes.String(v.SpaceID()),
}}, nil
func (d *date) DetailsFromId() (*types.Struct, error) {
return d.getDetails()
}
func (v *date) parseId() error {
t, err := time.Parse("2006-01-02", strings.TrimPrefix(v.id, addr.DatePrefix))
if err != nil {
return err
}
v.t = t
return nil
}
func (v *date) ReadDoc(ctx context.Context, receiver ChangeReceiver, empty bool) (doc state.Doc, err error) {
if err = v.parseId(); err != nil {
return
}
s := state.NewDoc(v.id, nil).(*state.State)
d, err := v.getDetails(ctx)
func (d *date) ReadDoc(context.Context, ChangeReceiver, bool) (doc state.Doc, err error) {
details, err := d.getDetails()
if err != nil {
return
}
dataview := &model.BlockContentOfDataview{
Dataview: &model.BlockContentDataview{
RelationLinks: []*model.RelationLink{
{
Key: bundle.RelationKeyName.String(),
Format: model.RelationFormat_shorttext,
},
{
Key: bundle.RelationKeyLastModifiedDate.String(),
Format: model.RelationFormat_date,
},
},
Views: []*model.BlockContentDataviewView{
{
Id: "1",
Type: model.BlockContentDataviewView_Table,
Name: "Date backlinks",
Sorts: []*model.BlockContentDataviewSort{
{
RelationKey: bundle.RelationKeyLastModifiedDate.String(),
Type: model.BlockContentDataviewSort_Desc,
},
},
Filters: []*model.BlockContentDataviewFilter{
{
RelationKey: bundle.RelationKeyLinks.String(),
Condition: model.BlockContentDataviewFilter_In,
Value: pbtypes.String(v.id),
},
},
Relations: []*model.BlockContentDataviewRelation{
{
Key: bundle.RelationKeyName.String(),
IsVisible: true,
},
{
Key: bundle.RelationKeyLastModifiedDate.String(),
IsVisible: true,
},
},
},
},
},
}
s := state.NewDoc(d.id, nil).(*state.State)
template.InitTemplate(s,
template.WithTitle,
template.WithDefaultFeaturedRelations,
template.WithDataview(dataview, true),
template.WithAllBlocksEditsRestricted,
)
s.SetDetails(d)
s.SetDetails(details)
s.SetObjectTypeKey(bundle.TypeKeyDate)
return s, nil
}
func (v *date) PushChange(params PushChangeParams) (id string, err error) {
func (d *date) PushChange(PushChangeParams) (id string, err error) {
return "", nil
}
func (v *date) Close() (err error) {
func (d *date) Close() (err error) {
return
}
func (v *date) Heads() []string {
return []string{v.id}
func (d *date) Heads() []string {
return []string{d.id}
}
func (v *date) GetFileKeysSnapshot() []*pb.ChangeFileKeys {
func (d *date) GetFileKeysSnapshot() []*pb.ChangeFileKeys {
return nil
}
func (v *date) GetCreationInfo() (creatorObjectId string, createdDate int64, err error) {
func (d *date) GetCreationInfo() (creatorObjectId string, createdDate int64, err error) {
return addr.AnytypeProfileId, 0, nil
}

View file

@ -22,10 +22,13 @@ import (
"github.com/anyproto/anytype-heart/core/files"
"github.com/anyproto/anytype-heart/pkg/lib/bundle"
"github.com/anyproto/anytype-heart/pkg/lib/core/smartblock"
"github.com/anyproto/anytype-heart/pkg/lib/database"
"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/pb/model"
"github.com/anyproto/anytype-heart/space/spacecore/storage"
"github.com/anyproto/anytype-heart/space/spacecore/typeprovider"
"github.com/anyproto/anytype-heart/util/pbtypes"
)
const CName = "source"
@ -140,9 +143,16 @@ func (s *service) newSource(ctx context.Context, space Space, id string, buildOp
if err == nil {
switch st {
case smartblock.SmartBlockTypeDate:
return NewDate(space, domain.FullID{
ObjectID: id,
SpaceID: space.Id(),
typeId, err := space.GetTypeIdByKey(context.Background(), bundle.TypeKeyDate)
if err != nil {
return nil, fmt.Errorf("failed to find Date type to build Date object: %w", err)
}
return NewDate(DateSourceParams{
Id: domain.FullID{
ObjectID: id,
SpaceID: space.Id(),
},
DateObjectTypeId: typeId,
}), nil
case smartblock.SmartBlockTypeBundledObjectType:
return NewBundledObjectType(id), nil
@ -208,7 +218,27 @@ func (s *service) DetailsFromIdBasedSource(id domain.FullID) (*types.Struct, err
if !strings.HasPrefix(id.ObjectID, addr.DatePrefix) {
return nil, fmt.Errorf("unsupported id")
}
ss := NewDate(nil, id)
records, err := s.objectStore.SpaceIndex(id.SpaceID).Query(database.Query{
Filters: []*model.BlockContentDataviewFilter{{
Condition: model.BlockContentDataviewFilter_Equal,
RelationKey: bundle.RelationKeyUniqueKey.String(),
Value: pbtypes.String(bundle.TypeKeyDate.URL()),
},
}})
if len(records) != 1 && err == nil {
err = fmt.Errorf("expected 1 record, got %d", len(records))
}
if err != nil {
return nil, fmt.Errorf("failed to query details of Date type object: %w", err)
}
ss := NewDate(DateSourceParams{
Id: id,
DateObjectTypeId: pbtypes.GetString(records[0].Details, bundle.RelationKeyId.String()),
})
defer ss.Close()
if v, ok := ss.(SourceIdEndodedDetails); ok {
return v.DetailsFromId()

155
core/date/suggest.go Normal file
View file

@ -0,0 +1,155 @@
package date
import (
"context"
"fmt"
"strconv"
"strings"
"time"
"github.com/anyproto/go-naturaldate/v2"
"github.com/araddon/dateparse"
"github.com/anyproto/anytype-heart/core/block/source"
"github.com/anyproto/anytype-heart/core/domain"
"github.com/anyproto/anytype-heart/pb"
"github.com/anyproto/anytype-heart/pkg/lib/bundle"
"github.com/anyproto/anytype-heart/pkg/lib/database"
"github.com/anyproto/anytype-heart/pkg/lib/localstore/objectstore"
"github.com/anyproto/anytype-heart/space"
"github.com/anyproto/anytype-heart/util/dateutil"
)
func EnrichRecordsWithDateSuggestion(
ctx context.Context,
records []database.Record,
req *pb.RpcObjectSearchRequest,
store objectstore.ObjectStore,
spaceService space.Service,
) ([]database.Record, error) {
dt := suggestDateForSearch(time.Now(), req.FullText)
if dt.IsZero() {
return records, nil
}
id := dateutil.TimeToDateId(dt)
// Don't duplicate search suggestions
var found bool
for _, r := range records {
if r.Details == nil || r.Details.Fields == nil {
continue
}
if v, ok := r.Details.Fields[bundle.RelationKeyId.String()]; ok {
if v.GetStringValue() == id {
found = true
break
}
}
}
if found {
return records, nil
}
spc, err := spaceService.Get(ctx, req.SpaceId)
if err != nil {
return nil, fmt.Errorf("get space: %w", err)
}
rec, err := makeSuggestedDateRecord(spc, dt)
if err != nil {
return nil, fmt.Errorf("make date record: %w", err)
}
f, _ := database.MakeFilters(req.Filters, store.SpaceIndex(req.SpaceId)) //nolint:errcheck
if f.FilterObject(rec.Details) {
return append([]database.Record{rec}, records...), nil
}
return records, nil
}
func suggestDateForSearch(now time.Time, raw string) time.Time {
suggesters := []func() time.Time{
func() time.Time {
var exprType naturaldate.ExprType
t, exprType, err := naturaldate.Parse(raw, now)
if err != nil {
return time.Time{}
}
if exprType == naturaldate.ExprTypeInvalid {
return time.Time{}
}
// naturaldate parses numbers without qualifiers (m,s) as hours in 24 hours clock format. It leads to weird behavior
// when inputs like "123" represented as "current time + 123 hours"
if (exprType & naturaldate.ExprTypeClock24Hour) != 0 {
t = time.Time{}
}
return t
},
func() time.Time {
// Don't use plain numbers, because they will be represented as years
if _, err := strconv.Atoi(strings.TrimSpace(raw)); err == nil {
return time.Time{}
}
// todo: use system locale to get preferred date format
t, err := dateparse.ParseIn(raw, now.Location(), dateparse.PreferMonthFirst(false))
if err != nil {
return time.Time{}
}
return t
},
}
var t time.Time
for _, s := range suggesters {
if t = s(); !t.IsZero() {
break
}
}
if t.IsZero() {
return t
}
// Sanitize date
// Date without year
if t.Year() == 0 {
_, month, day := t.Date()
h, m, s := t.Clock()
t = time.Date(now.Year(), month, day, h, m, s, 0, t.Location())
}
return t
}
func makeSuggestedDateRecord(spc source.Space, t time.Time) (database.Record, error) {
id := dateutil.TimeToDateId(t)
typeId, err := spc.GetTypeIdByKey(context.Background(), bundle.TypeKeyDate)
if err != nil {
return database.Record{}, fmt.Errorf("failed to find Date type to build Date object: %w", err)
}
dateSource := source.NewDate(source.DateSourceParams{
Id: domain.FullID{
ObjectID: id,
SpaceID: spc.Id(),
},
DateObjectTypeId: typeId,
})
v, ok := dateSource.(source.SourceIdEndodedDetails)
if !ok {
return database.Record{}, fmt.Errorf("source does not implement DetailsFromId")
}
details, err := v.DetailsFromId()
if err != nil {
return database.Record{}, err
}
return database.Record{
Details: details,
}, nil
}

View file

@ -1,4 +1,4 @@
package core
package date
import (
"testing"

View file

@ -4,12 +4,7 @@ import (
"context"
"errors"
"fmt"
"strconv"
"strings"
"time"
"github.com/anyproto/go-naturaldate/v2"
"github.com/araddon/dateparse"
"github.com/gogo/protobuf/types"
"github.com/hashicorp/go-multierror"
@ -17,12 +12,12 @@ import (
importer "github.com/anyproto/anytype-heart/core/block/import"
"github.com/anyproto/anytype-heart/core/block/import/common"
"github.com/anyproto/anytype-heart/core/block/object/objectgraph"
"github.com/anyproto/anytype-heart/core/date"
"github.com/anyproto/anytype-heart/core/domain/objectorigin"
"github.com/anyproto/anytype-heart/core/indexer"
"github.com/anyproto/anytype-heart/core/subscription"
"github.com/anyproto/anytype-heart/core/subscription/crossspacesub"
"github.com/anyproto/anytype-heart/pb"
"github.com/anyproto/anytype-heart/pkg/lib/bundle"
"github.com/anyproto/anytype-heart/pkg/lib/database"
"github.com/anyproto/anytype-heart/pkg/lib/localstore/ftsearch"
"github.com/anyproto/anytype-heart/pkg/lib/localstore/objectstore"
@ -108,7 +103,7 @@ func (mw *Middleware) ObjectSearch(cctx context.Context, req *pb.RpcObjectSearch
// Add dates only to the first page of search results
if req.Offset == 0 {
records, err = mw.enrichWithDateSuggestion(cctx, records, req, ds)
records, err = date.EnrichRecordsWithDateSuggestion(cctx, records, req, ds, getService[space.Service](mw))
if err != nil {
return response(pb.RpcObjectSearchResponseError_UNKNOWN_ERROR, nil, err)
}
@ -174,127 +169,6 @@ func (mw *Middleware) ObjectSearchWithMeta(cctx context.Context, req *pb.RpcObje
return response(pb.RpcObjectSearchWithMetaResponseError_NULL, resultsModels, nil)
}
func (mw *Middleware) enrichWithDateSuggestion(ctx context.Context, records []database.Record, req *pb.RpcObjectSearchRequest, store objectstore.ObjectStore) ([]database.Record, error) {
dt := suggestDateForSearch(time.Now(), req.FullText)
if dt.IsZero() {
return records, nil
}
id := deriveDateId(dt)
// Don't duplicate search suggestions
var found bool
for _, r := range records {
if r.Details == nil || r.Details.Fields == nil {
continue
}
if v, ok := r.Details.Fields[bundle.RelationKeyId.String()]; ok {
if v.GetStringValue() == id {
found = true
break
}
}
}
if found {
return records, nil
}
var rec database.Record
rec, err := mw.makeSuggestedDateRecord(ctx, req.SpaceId, dt)
if err != nil {
return nil, fmt.Errorf("make date record: %w", err)
}
f, _ := database.MakeFilters(req.Filters, store.SpaceIndex(req.SpaceId)) //nolint:errcheck
if f.FilterObject(rec.Details) {
return append([]database.Record{rec}, records...), nil
}
return records, nil
}
func suggestDateForSearch(now time.Time, raw string) time.Time {
suggesters := []func() time.Time{
func() time.Time {
var exprType naturaldate.ExprType
t, exprType, err := naturaldate.Parse(raw, now)
if err != nil {
return time.Time{}
}
if exprType == naturaldate.ExprTypeInvalid {
return time.Time{}
}
// naturaldate parses numbers without qualifiers (m,s) as hours in 24 hours clock format. It leads to weird behavior
// when inputs like "123" represented as "current time + 123 hours"
if (exprType & naturaldate.ExprTypeClock24Hour) != 0 {
t = time.Time{}
}
return t
},
func() time.Time {
// Don't use plain numbers, because they will be represented as years
if _, err := strconv.Atoi(strings.TrimSpace(raw)); err == nil {
return time.Time{}
}
// todo: use system locale to get preferred date format
t, err := dateparse.ParseIn(raw, now.Location(), dateparse.PreferMonthFirst(false))
if err != nil {
return time.Time{}
}
return t
},
}
var t time.Time
for _, s := range suggesters {
if t = s(); !t.IsZero() {
break
}
}
if t.IsZero() {
return t
}
// Sanitize date
// Date without year
if t.Year() == 0 {
_, month, day := t.Date()
h, m, s := t.Clock()
t = time.Date(now.Year(), month, day, h, m, s, 0, t.Location())
}
return t
}
func deriveDateId(t time.Time) string {
return "_date_" + t.Format("2006-01-02")
}
func (mw *Middleware) makeSuggestedDateRecord(ctx context.Context, spaceID string, t time.Time) (database.Record, error) {
id := deriveDateId(t)
spc, err := getService[space.Service](mw).Get(ctx, spaceID)
if err != nil {
return database.Record{}, fmt.Errorf("get space: %w", err)
}
typeId, err := spc.GetTypeIdByKey(ctx, bundle.TypeKeyDate)
if err != nil {
return database.Record{}, fmt.Errorf("get date type id: %w", err)
}
d := &types.Struct{Fields: map[string]*types.Value{
bundle.RelationKeyId.String(): pbtypes.String(id),
bundle.RelationKeyName.String(): pbtypes.String(t.Format("02 Jan 2006")),
bundle.RelationKeyLayout.String(): pbtypes.Int64(int64(model.ObjectType_date)),
bundle.RelationKeyType.String(): pbtypes.String(typeId),
bundle.RelationKeyIconEmoji.String(): pbtypes.String("📅"),
bundle.RelationKeySpaceId.String(): pbtypes.String(spaceID),
}}
return database.Record{
Details: d,
}, nil
}
func (mw *Middleware) ObjectSearchSubscribe(cctx context.Context, req *pb.RpcObjectSearchSubscribeRequest) *pb.RpcObjectSearchSubscribeResponse {
errResponse := func(err error) *pb.RpcObjectSearchSubscribeResponse {
r := &pb.RpcObjectSearchSubscribeResponse{

View file

@ -9,7 +9,7 @@ import (
"github.com/anyproto/anytype-heart/pkg/lib/pb/model"
)
const RelationChecksum = "414402e104d3b92fa6045649ffa60f6988979f427cf91ef678c53257a0d83fc4"
const RelationChecksum = "d020435326985f2804482fd51f0a5fde92c007683e42a11ce26193c9b2876033"
const (
RelationKeyTag domain.RelationKey = "tag"
RelationKeyCamera domain.RelationKey = "camera"
@ -143,6 +143,7 @@ const (
RelationKeyHasChat domain.RelationKey = "hasChat"
RelationKeyChatId domain.RelationKey = "chatId"
RelationKeyMentions domain.RelationKey = "mentions"
RelationKeyTimestamp domain.RelationKey = "timestamp"
)
var (
@ -1847,6 +1848,19 @@ var (
ReadOnlyRelation: true,
Scope: model.Relation_type,
},
RelationKeyTimestamp: {
DataSource: model.Relation_derived,
Description: "Unix time representation of date object",
Format: model.RelationFormat_date,
Hidden: true,
Id: "_brtimestamp",
Key: "timestamp",
Name: "Timestamp",
ReadOnly: true,
ReadOnlyRelation: true,
Scope: model.Relation_type,
},
RelationKeyToBeDeletedDate: {
DataSource: model.Relation_account,

View file

@ -1350,5 +1350,15 @@
"name": "Mentions",
"readonly": true,
"source": "local"
},
{
"description": "Unix time representation of date object",
"format": "date",
"hidden": true,
"key": "timestamp",
"maxCount": 0,
"name": "Timestamp",
"readonly": true,
"source": "derived"
}
]

View file

@ -6,7 +6,7 @@ package bundle
import domain "github.com/anyproto/anytype-heart/core/domain"
const SystemRelationsChecksum = "dccec4608e8b4d207055a77d1e475598b6ed9ef177d0d796065ebc6b9e0466f7"
const SystemRelationsChecksum = "cebd4ab7522c2ca901215e20861e005c1ad1a6ca2a77d7c1205e9e51edd901db"
// 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
@ -81,4 +81,5 @@ var SystemRelations = append(RequiredInternalRelations, []domain.RelationKey{
RelationKeyMentions,
RelationKeyChatId,
RelationKeyHasChat,
RelationKeyTimestamp,
}...)

View file

@ -89,5 +89,6 @@
"lastUsedDate",
"mentions",
"chatId",
"hasChat"
"hasChat",
"timestamp"
]

View file

@ -3,7 +3,6 @@ package addr
import (
"fmt"
"strings"
"time"
"github.com/anyproto/anytype-heart/pkg/lib/pb/model"
)
@ -44,7 +43,3 @@ func ExtractVirtualSourceType(id string) (model.SmartBlockType, error) {
}
return 0, fmt.Errorf("sb type '%s' not found", sbTypeName)
}
func TimeToID(t time.Time) string {
return DatePrefix + t.Format("2006-01-02")
}

33
util/dateutil/util.go Normal file
View file

@ -0,0 +1,33 @@
package dateutil
import (
"strings"
"time"
"github.com/anyproto/anytype-heart/pkg/lib/localstore/addr"
)
const (
dateIdLayout = "2006-01-02"
dateNameLayout = "02 Jan 2006"
)
func TimeToDateId(t time.Time) string {
return addr.DatePrefix + t.Format(dateIdLayout)
}
func ParseDateId(id string) (time.Time, error) {
return time.Parse(dateIdLayout, strings.TrimPrefix(id, addr.DatePrefix))
}
func TimeToDateName(t time.Time) string {
return t.Format(dateNameLayout)
}
func DateNameToId(name string) (string, error) {
t, err := time.Parse(dateNameLayout, name)
if err != nil {
return "", err
}
return TimeToDateId(t), nil
}