Skip to content

Commit

Permalink
Initial implementation of deletes
Browse files Browse the repository at this point in the history
  • Loading branch information
burdiyan committed Nov 5, 2024
1 parent c9e6aca commit 3653958
Show file tree
Hide file tree
Showing 13 changed files with 2,221 additions and 479 deletions.
2 changes: 1 addition & 1 deletion backend/api/documents/v3alpha/dochistory.go
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,7 @@ func (srv *Server) ListDocumentChanges(ctx context.Context, in *documents.ListDo
// We need to just use the database index, but it's currently too painful to work with,
// because we don't track latest heads for each space+path.

doc, err := srv.loadDocument(ctx, acc, in.Path, docmodel.Version(in.Version), false)
doc, err := srv.loadDocument(ctx, acc, in.Path, docmodel.Version(in.Version), false, false)
if err != nil {
return nil, err
}
Expand Down
209 changes: 193 additions & 16 deletions backend/api/documents/v3alpha/documents.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ import (
"seed/backend/util/errutil"
"seed/backend/util/sqlite"
"seed/backend/util/sqlite/sqlitex"
"time"

blocks "github.com/ipfs/go-block-format"
"github.com/ipfs/go-cid"
Expand All @@ -26,6 +27,7 @@ import (
"google.golang.org/grpc/codes"
"google.golang.org/grpc/status"
"google.golang.org/protobuf/types/known/emptypb"
"google.golang.org/protobuf/types/known/timestamppb"
)

// Server implements Documents API v3.
Expand Down Expand Up @@ -66,7 +68,7 @@ func (srv *Server) GetDocument(ctx context.Context, in *documents.GetDocumentReq
return nil, err
}

doc, err := srv.loadDocument(ctx, ns, in.Path, docmodel.Version(in.Version), false)
doc, err := srv.loadDocument(ctx, ns, in.Path, docmodel.Version(in.Version), false, in.IgnoreDeleted)
if err != nil {
return nil, err
}
Expand Down Expand Up @@ -120,7 +122,7 @@ func (srv *Server) CreateDocumentChange(ctx context.Context, in *documents.Creat

ver := docmodel.Version(in.BaseVersion)

doc, err := srv.loadDocument(ctx, ns, in.Path, ver, true)
doc, err := srv.loadDocument(ctx, ns, in.Path, ver, true, false)
if err != nil {
return nil, err
}
Expand Down Expand Up @@ -296,7 +298,7 @@ func (srv *Server) ListDocuments(ctx context.Context, in *documents.ListDocument
}
}

out := documents.ListDocumentsResponse{
out := &documents.ListDocumentsResponse{
Documents: make([]*documents.DocumentListItem, 0, in.PageSize),
}

Expand Down Expand Up @@ -345,14 +347,36 @@ func (srv *Server) ListDocuments(ctx context.Context, in *documents.ListDocument
release()
for _, req := range requests {
doc, err := srv.GetDocument(ctx, req)
if err != nil {
switch {
// If we are asking about deleted only, but the Get returns OK, then it's not deleted.
case in.DeletedOnly && err == nil:
continue
case in.DeletedOnly && err != nil:
status, ok := status.FromError(err)
if !ok {
return nil, err
}
// Failed precondition error code means the doc is deleted.
if status.Code() == codes.FailedPrecondition {
req.IgnoreDeleted = true
doc, err = srv.GetDocument(ctx, req)
if err != nil {
srv.log.Warn("GetDocumentFailedForDeletedDocument", zap.String("space", req.Account), zap.String("path", req.Path))
continue
}
out.Documents = append(out.Documents, DocumentToListItem(doc))
continue
} else {
continue
}
case !in.DeletedOnly && err == nil:
out.Documents = append(out.Documents, DocumentToListItem(doc))
case !in.DeletedOnly && err != nil:
continue
}
// TODO: use indexed data instead of loading the entire document.
out.Documents = append(out.Documents, DocumentToListItem(doc))
}

return &out, nil
return out, nil
}

var qListDocuments = dqb.Str(`
Expand All @@ -368,7 +392,147 @@ var qListDocuments = dqb.Str(`

// DeleteDocument implements Documents API v3.
func (srv *Server) DeleteDocument(ctx context.Context, in *documents.DeleteDocumentRequest) (*emptypb.Empty, error) {
return nil, status.Error(codes.Unimplemented, "DeleteDocument is not implemented yet")
return nil, status.Error(codes.Unimplemented, "Deprecated: Use CreateRef")
}

// CreateRef implements Documents API v3.
func (srv *Server) CreateRef(ctx context.Context, in *documents.CreateRefRequest) (*documents.Ref, error) {
{
if in.Account == "" {
return nil, errutil.MissingArgument("account")
}

if in.SigningKeyName == "" {
return nil, errutil.MissingArgument("signing_key_name")
}

if in.Target == nil {
return nil, errutil.MissingArgument("target")
}
}

ns, err := core.DecodePrincipal(in.Account)
if err != nil {
return nil, status.Errorf(codes.InvalidArgument, "faield to decode account ID: %v", err)
}

kp, err := srv.keys.GetKey(ctx, in.SigningKeyName)
if err != nil {
return nil, err
}

var capc cid.Cid
if in.Capability != "" {
capc, err = cid.Decode(in.Capability)
if err != nil {
return nil, err
}
}

if err := srv.checkWriteAccess(ctx, ns, in.Path, kp, capc); err != nil {
return nil, err
}

if in.Path == "" {
return nil, status.Errorf(codes.Unimplemented, "TODO: creating Refs for root documents is not implemented yet")
}

var ts time.Time
if in.Timestamp != nil {
ts = in.Timestamp.AsTime().Round(blob.ClockPrecision)
} else {
ts = cclock.New().MustNow()
}

var refBlob blob.Encoded[*blob.Ref]

switch in.Target.Target.(type) {
case *documents.RefTarget_Version_:
return nil, status.Errorf(codes.Unimplemented, "version Ref target is not implemented yet")
case *documents.RefTarget_Tombstone_:
refBlob, err = blob.NewRefTombstone(kp, ns, in.Path, ts)
if err != nil {
return nil, err
}
case *documents.RefTarget_Redirect_:
return nil, status.Errorf(codes.Unimplemented, "redirect Ref target is not implemented yet")
default:
return nil, fmt.Errorf("BUG: unhandled ref target type case")
}

if err := srv.idx.Put(ctx, refBlob); err != nil {
return nil, err
}

return refToProto(refBlob.CID, refBlob.Decoded)
}

// GetRef implements Documents API v3.
func (srv *Server) GetRef(ctx context.Context, in *documents.GetRefRequest) (*documents.Ref, error) {
if in.Id == "" {
return nil, errutil.MissingArgument("id")
}

c, err := cid.Decode(in.Id)
if err != nil {
return nil, status.Errorf(codes.InvalidArgument, "failed to parse Ref ID: %v", err)
}

ref, err := srv.getRef(ctx, c)
if err != nil {
return nil, err
}

return refToProto(ref.CID, ref.Value)
}

func refToProto(c cid.Cid, ref *blob.Ref) (*documents.Ref, error) {
pb := &documents.Ref{
Id: c.String(),
Account: ref.GetSpace().String(),
Path: ref.Path,
Signer: ref.Signer.String(),
Timestamp: timestamppb.New(ref.Ts),
}

switch {
case ref.GenesisBlob.Defined() && len(ref.Heads) > 0:
pb.Target = &documents.RefTarget{
Target: &documents.RefTarget_Version_{
Version: &documents.RefTarget_Version{
Genesis: ref.GenesisBlob.String(),
Version: string(blob.NewVersion(ref.Heads...)),
},
},
}
case !ref.GenesisBlob.Defined() && len(ref.Heads) == 0:
pb.Target = &documents.RefTarget{
Target: &documents.RefTarget_Tombstone_{
Tombstone: &documents.RefTarget_Tombstone{},
},
}
default:
return nil, fmt.Errorf("refToProto: invalid original ref %s: %+v", c, ref)
}

return pb, nil
}

func (srv *Server) getRef(ctx context.Context, c cid.Cid) (hb blob.WithCID[*blob.Ref], err error) {
blk, err := srv.idx.Get(ctx, c)
if err != nil {
return hb, err
}

ref := &blob.Ref{}
if err := cbornode.DecodeInto(blk.RawData(), ref); err != nil {
return hb, err
}

return blob.WithCID[*blob.Ref]{
CID: blk.Cid(),
Value: ref,
}, nil
}

func (srv *Server) ensureProfileGenesis(ctx context.Context, kp core.KeyPair) error {
Expand Down Expand Up @@ -403,20 +567,38 @@ func makeIRI(account core.Principal, path string) (blob.IRI, error) {
return blob.NewIRI(account, path)
}

func (srv *Server) loadDocument(ctx context.Context, account core.Principal, path string, version docmodel.Version, ensurePath bool) (*docmodel.Document, error) {
func (srv *Server) loadDocument(ctx context.Context, account core.Principal, path string, version docmodel.Version, ensurePath bool, ignoreDeleted bool) (*docmodel.Document, error) {
iri, err := makeIRI(account, path)
if err != nil {
return nil, err
}

heads, err := version.Parse()
if err != nil {
return nil, err
}

clock := cclock.New()
doc, err := docmodel.New(iri, clock)
if err != nil {
return nil, err
}

// We only check if it's deleted if we are asked about the latest version,
// unless we explicitly say to ignore deletions.
if version == "" && !ignoreDeleted {
isDeleted, err := srv.idx.IsDeleted(ctx, iri)
if err != nil {
return nil, err
}

if isDeleted {
return nil, status.Errorf(codes.FailedPrecondition, "document is marked as deleted")
}
}

var outErr error
changes, check := srv.idx.IterChanges(ctx, iri, account)
changes, check := srv.idx.IterChanges(ctx, iri)
for _, ch := range changes {
if err := doc.ApplyChange(ch.CID, ch.Data); err != nil {
outErr = errors.Join(outErr, err)
Expand All @@ -432,12 +614,7 @@ func (srv *Server) loadDocument(ctx context.Context, account core.Principal, pat
return nil, status.Errorf(codes.NotFound, "document not found: %s", iri)
}

if version != "" {
heads, err := version.Parse()
if err != nil {
return nil, err
}

if len(heads) > 0 {
doc, err = doc.Checkout(heads)
if err != nil {
return nil, fmt.Errorf("failed to checkout version: %w", err)
Expand Down
Loading

0 comments on commit 3653958

Please sign in to comment.