From 7272793ae3cbabaf2e9c0d8e416bf0e669a1ae4b Mon Sep 17 00:00:00 2001 From: Alexandr Burdiyan Date: Mon, 9 May 2022 11:12:49 +0200 Subject: [PATCH] feat(backend): implement ListDrafts Former-commit-id: 0a3e721dd530765d50ac13a9944608a7911da8cd --- backend/api_docs.go | 35 +------ backend/vcs/vcssql/sqlite_queries.gen.go | 91 +++++++++++++++++++ backend/vcs/vcssql/sqlite_queries.go | 50 ++++++++++ backend/vcs/vcstypes/{api.go => api_docs.go} | 69 +++++++++++++- .../{api_test.go => api_docs_test.go} | 23 ++--- backend/vcs_queries.gensum | 4 +- dev | 1 + .../.generated/documents/v1alpha/documents.ts | 84 ++++++++--------- 8 files changed, 267 insertions(+), 90 deletions(-) rename backend/vcs/vcstypes/{api.go => api_docs.go} (85%) rename backend/vcs/vcstypes/{api_test.go => api_docs_test.go} (96%) diff --git a/backend/api_docs.go b/backend/api_docs.go index dbb44400eb..0d942df24e 100644 --- a/backend/api_docs.go +++ b/backend/api_docs.go @@ -62,7 +62,7 @@ func newDocsAPI(back *backend) DocsServer { vcs := vcs.New(back.pool) - docsapi := vcstypes.NewDocsAPI(id, vcs) + docsapi := vcstypes.NewDocsAPI(id, back.pool, vcs) srv.DocsAPI = docsapi }() @@ -145,39 +145,8 @@ func draftToProto(d Draft) *documents.Document { } } -func (srv *docsAPI) ListDrafts(ctx context.Context, in *documents.ListDraftsRequest) (*documents.ListDraftsResponse, error) { - list, err := srv.back.ListDrafts(ctx) - if err != nil { - return nil, err - } - - out := &documents.ListDraftsResponse{ - Documents: make([]*documents.Document, len(list)), - } - - acc, err := srv.back.Account() - if err != nil { - return nil, err - } - - aid := acc.id.String() - - for i, l := range list { - out.Documents[i] = &documents.Document{ - Id: cid.NewCidV1(uint64(l.ObjectsCodec), l.ObjectsMultihash).String(), - Author: aid, - Title: l.DraftsTitle, - Subtitle: l.DraftsSubtitle, - CreateTime: timestamppb.New(timeFromSeconds(l.DraftsCreateTime)), - UpdateTime: timestamppb.New(timeFromSeconds(l.DraftsUpdateTime)), - } - } - - return out, nil -} - func (srv *docsAPI) UpdateDraft(ctx context.Context, in *documents.UpdateDraftRequest) (*documents.Document, error) { - return nil, status.Error(codes.Unimplemented, "not implemented") + return nil, status.Error(codes.Unimplemented, "deprecated") c, err := srv.parseDocumentID(in.Document.Id) if err != nil { diff --git a/backend/vcs/vcssql/sqlite_queries.gen.go b/backend/vcs/vcssql/sqlite_queries.gen.go index 231d62400f..11ecbbd4c2 100644 --- a/backend/vcs/vcssql/sqlite_queries.gen.go +++ b/backend/vcs/vcssql/sqlite_queries.gen.go @@ -379,3 +379,94 @@ LIMIT 1` return out, err } + +func DraftsInsert(conn *sqlite.Conn, objectsMultihash []byte, objectsCodec int, draftsTitle string, draftsSubtitle string, draftsCreateTime int, draftsUpdateTime int) error { + const query = `INSERT INTO drafts (id, title, subtitle, create_time, update_time) +VALUES (COALESCE((SELECT objects.id FROM objects WHERE objects.multihash = :objectsMultihash AND objects.codec = :objectsCodec LIMIT 1), -1000), :draftsTitle, :draftsSubtitle, :draftsCreateTime, :draftsUpdateTime)` + + before := func(stmt *sqlite.Stmt) { + stmt.SetBytes(":objectsMultihash", objectsMultihash) + stmt.SetInt(":objectsCodec", objectsCodec) + stmt.SetText(":draftsTitle", draftsTitle) + stmt.SetText(":draftsSubtitle", draftsSubtitle) + stmt.SetInt(":draftsCreateTime", draftsCreateTime) + stmt.SetInt(":draftsUpdateTime", draftsUpdateTime) + } + + onStep := func(i int, stmt *sqlite.Stmt) error { + return nil + } + + err := sqlitegen.ExecStmt(conn, query, before, onStep) + if err != nil { + err = fmt.Errorf("failed query: DraftsInsert: %w", err) + } + + return err +} + +func DraftsUpdate(conn *sqlite.Conn, draftsTitle string, draftsSubtitle string, draftsUpdateTime int, objectsMultihash []byte, objectsCodec int) error { + const query = `UPDATE drafts +SET (title, subtitle, update_time) = (:draftsTitle, :draftsSubtitle, :draftsUpdateTime) +WHERE drafts.id = COALESCE((SELECT objects.id FROM objects WHERE objects.multihash = :objectsMultihash AND objects.codec = :objectsCodec LIMIT 1), -1000)` + + before := func(stmt *sqlite.Stmt) { + stmt.SetText(":draftsTitle", draftsTitle) + stmt.SetText(":draftsSubtitle", draftsSubtitle) + stmt.SetInt(":draftsUpdateTime", draftsUpdateTime) + stmt.SetBytes(":objectsMultihash", objectsMultihash) + stmt.SetInt(":objectsCodec", objectsCodec) + } + + onStep := func(i int, stmt *sqlite.Stmt) error { + return nil + } + + err := sqlitegen.ExecStmt(conn, query, before, onStep) + if err != nil { + err = fmt.Errorf("failed query: DraftsUpdate: %w", err) + } + + return err +} + +type DraftsListResult struct { + ObjectsMultihash []byte + ObjectsCodec int + DraftsTitle string + DraftsSubtitle string + DraftsCreateTime int + DraftsUpdateTime int +} + +func DraftsList(conn *sqlite.Conn) ([]DraftsListResult, error) { + const query = `SELECT objects.multihash, objects.codec, drafts.title, drafts.subtitle, drafts.create_time, drafts.update_time +FROM drafts +JOIN objects ON objects.id = drafts.id +` + + var out []DraftsListResult + + before := func(stmt *sqlite.Stmt) { + } + + onStep := func(i int, stmt *sqlite.Stmt) error { + out = append(out, DraftsListResult{ + ObjectsMultihash: stmt.ColumnBytes(0), + ObjectsCodec: stmt.ColumnInt(1), + DraftsTitle: stmt.ColumnText(2), + DraftsSubtitle: stmt.ColumnText(3), + DraftsCreateTime: stmt.ColumnInt(4), + DraftsUpdateTime: stmt.ColumnInt(5), + }) + + return nil + } + + err := sqlitegen.ExecStmt(conn, query, before, onStep) + if err != nil { + err = fmt.Errorf("failed query: DraftsList: %w", err) + } + + return out, err +} diff --git a/backend/vcs/vcssql/sqlite_queries.go b/backend/vcs/vcssql/sqlite_queries.go index ed4d663a0a..ead8073ecc 100644 --- a/backend/vcs/vcssql/sqlite_queries.go +++ b/backend/vcs/vcssql/sqlite_queries.go @@ -157,6 +157,56 @@ func generateQueries() error { "AND", s.NamedVersionsName, "=", qb.VarCol(s.NamedVersionsName), qb.Line, "LIMIT 1", ), + + qb.MakeQuery(s.Schema, "DraftsInsert", sgen.QueryKindExec, + "INSERT INTO", s.Drafts, qb.ListColShort( + s.DraftsID, + s.DraftsTitle, + s.DraftsSubtitle, + s.DraftsCreateTime, + s.DraftsUpdateTime, + ), qb.Line, + "VALUES", qb.List( + qb.LookupSubQuery(s.ObjectsID, s.Objects, + "WHERE", s.ObjectsMultihash, "=", qb.VarCol(s.ObjectsMultihash), + "AND", s.ObjectsCodec, "=", qb.VarCol(s.ObjectsCodec), + ), + qb.VarCol(s.DraftsTitle), + qb.VarCol(s.DraftsSubtitle), + qb.VarCol(s.DraftsCreateTime), + qb.VarCol(s.DraftsUpdateTime), + ), + ), + + qb.MakeQuery(s.Schema, "DraftsUpdate", sgen.QueryKindExec, + "UPDATE", s.Drafts, qb.Line, + "SET", qb.ListColShort( + s.DraftsTitle, + s.DraftsSubtitle, + s.DraftsUpdateTime, + ), "=", qb.List( + qb.VarCol(s.DraftsTitle), + qb.VarCol(s.DraftsSubtitle), + qb.VarCol(s.DraftsUpdateTime), + ), qb.Line, + "WHERE", s.DraftsID, "=", qb.LookupSubQuery(s.ObjectsID, s.Objects, + "WHERE", s.ObjectsMultihash, "=", qb.VarCol(s.ObjectsMultihash), + "AND", s.ObjectsCodec, "=", qb.VarCol(s.ObjectsCodec), + ), + ), + + qb.MakeQuery(s.Schema, "DraftsList", sgen.QueryKindMany, + "SELECT", qb.Results( + qb.ResultCol(s.ObjectsMultihash), + qb.ResultCol(s.ObjectsCodec), + qb.ResultCol(s.DraftsTitle), + qb.ResultCol(s.DraftsSubtitle), + qb.ResultCol(s.DraftsCreateTime), + qb.ResultCol(s.DraftsUpdateTime), + ), qb.Line, + "FROM", s.Drafts, qb.Line, + "JOIN", s.Objects, "ON", s.ObjectsID, "=", s.DraftsID, qb.Line, + ), ) if err != nil { diff --git a/backend/vcs/vcstypes/api.go b/backend/vcs/vcstypes/api_docs.go similarity index 85% rename from backend/vcs/vcstypes/api.go rename to backend/vcs/vcstypes/api_docs.go index 8a0af5b106..43aee8e45d 100644 --- a/backend/vcs/vcstypes/api.go +++ b/backend/vcs/vcstypes/api_docs.go @@ -6,8 +6,11 @@ import ( documents "mintter/backend/api/documents/v1alpha" "mintter/backend/core" "mintter/backend/crdt" + "mintter/backend/ipfs" "mintter/backend/vcs" + "mintter/backend/vcs/vcssql" + "crawshaw.io/sqlite/sqlitex" "github.com/ipfs/go-cid" cbornode "github.com/ipfs/go-ipld-cbor" "google.golang.org/grpc/codes" @@ -17,13 +20,15 @@ import ( ) type DocsAPI struct { - vcs *vcs.SQLite me core.Identity + db *sqlitex.Pool + vcs *vcs.SQLite } -func NewDocsAPI(me core.Identity, vcs *vcs.SQLite) *DocsAPI { +func NewDocsAPI(me core.Identity, db *sqlitex.Pool, vcs *vcs.SQLite) *DocsAPI { return &DocsAPI{ me: me, + db: db, vcs: vcs, } } @@ -54,6 +59,20 @@ func (api *DocsAPI) CreateDraft(ctx context.Context, in *documents.CreateDraftRe return nil, err } + { + conn, release, err := api.db.Conn(ctx) + if err != nil { + return nil, err + } + defer release() + + ocodec, ohash := ipfs.DecodeCID(permablk.Cid()) + + if err := vcssql.DraftsInsert(conn, ohash, int(ocodec), "", "", int(p.CreateTime.Unix()), int(p.CreateTime.Unix())); err != nil { + return nil, err + } + } + return &documents.Document{ Id: permablk.Cid().String(), Author: me.String(), @@ -125,6 +144,20 @@ func (api *DocsAPI) UpdateDraftV2(ctx context.Context, in *documents.UpdateDraft return nil, fmt.Errorf("failed to save draft working copy: %w", err) } + { + conn, release, err := api.db.Conn(ctx) + if err != nil { + return nil, err + } + defer release() + + ocodec, ohash := ipfs.DecodeCID(oid) + + if err := vcssql.DraftsUpdate(conn, doc.state.Title, doc.state.Subtitle, int(doc.state.UpdateTime.Unix()), ohash, int(ocodec)); err != nil { + return nil, err + } + } + // TODO: index links. // Move old links insert new links. @@ -145,6 +178,38 @@ func (api *DocsAPI) GetDraft(ctx context.Context, in *documents.GetDraftRequest) return docToProto(draft.doc) } +func (api *DocsAPI) ListDrafts(ctx context.Context, in *documents.ListDraftsRequest) (*documents.ListDraftsResponse, error) { + conn, release, err := api.db.Conn(ctx) + if err != nil { + return nil, err + } + defer release() + + res, err := vcssql.DraftsList(conn) + if err != nil { + return nil, err + } + + out := &documents.ListDraftsResponse{ + Documents: make([]*documents.Document, len(res)), + } + + aid := api.me.AccountID().String() + + for i, l := range res { + out.Documents[i] = &documents.Document{ + Id: cid.NewCidV1(uint64(l.ObjectsCodec), l.ObjectsMultihash).String(), + Author: aid, + Title: l.DraftsTitle, + Subtitle: l.DraftsSubtitle, + CreateTime: ×tamppb.Timestamp{Seconds: int64(l.DraftsCreateTime)}, + UpdateTime: ×tamppb.Timestamp{Seconds: int64(l.DraftsUpdateTime)}, + } + } + + return out, nil +} + func (api *DocsAPI) PublishDraft(ctx context.Context, in *documents.PublishDraftRequest) (*documents.Publication, error) { oid, err := cid.Decode(in.DocumentId) if err != nil { diff --git a/backend/vcs/vcstypes/api_test.go b/backend/vcs/vcstypes/api_docs_test.go similarity index 96% rename from backend/vcs/vcstypes/api_test.go rename to backend/vcs/vcstypes/api_docs_test.go index a4e49ef086..6ac331312e 100644 --- a/backend/vcs/vcstypes/api_test.go +++ b/backend/vcs/vcstypes/api_docs_test.go @@ -104,17 +104,17 @@ func TestAPIUpdateDraft(t *testing.T) { testutil.ProtoEqual(t, want, updated, "UpdateDraft should return the updated document") - // list, err := api.ListDrafts(ctx, &documents.ListDraftsRequest{}) - // require.NoError(t, err) - // require.Len(t, list.Documents, 1) - // require.Equal(t, updated.Id, list.Documents[0].Id) - // require.Equal(t, updated.Author, list.Documents[0].Author) - // require.Equal(t, updated.Title, list.Documents[0].Title) + list, err := api.ListDrafts(ctx, &documents.ListDraftsRequest{}) + require.NoError(t, err) + require.Len(t, list.Documents, 1) + require.Equal(t, updated.Id, list.Documents[0].Id) + require.Equal(t, updated.Author, list.Documents[0].Author) + require.Equal(t, updated.Title, list.Documents[0].Title) - // got, err := api.GetDraft(ctx, &documents.GetDraftRequest{DocumentId: draft.Id}) - // require.NoError(t, err) + got, err := api.GetDraft(ctx, &documents.GetDraftRequest{DocumentId: draft.Id}) + require.NoError(t, err) - // testutil.ProtoEqual(t, draft, got, "must get draft that was updated") + testutil.ProtoEqual(t, updated, got, "must get draft that was updated") } func TestAPIUpdateDraft_Complex(t *testing.T) { @@ -435,9 +435,10 @@ func updateDraft(ctx context.Context, t *testing.T, api *DocsAPI, id string, upd func newTestDocsAPI(t *testing.T, name string) *DocsAPI { u := coretest.NewTester("alice") - v := vcs.New(newTestSQLite(t)) + db := newTestSQLite(t) + v := vcs.New(db) - return NewDocsAPI(u.Identity, v) + return NewDocsAPI(u.Identity, db, v) } func newTestSQLite(t *testing.T) *sqlitex.Pool { diff --git a/backend/vcs_queries.gensum b/backend/vcs_queries.gensum index 3b6795baf1..4d355aca7b 100644 --- a/backend/vcs_queries.gensum +++ b/backend/vcs_queries.gensum @@ -1,2 +1,2 @@ -srcs: 6e7d4655b12a9ee7ff61f4a6bee4864c -outs: b2040e5b02f8b6c794a89e8d5c73e003 +srcs: 33841ca30fc2fc523cfa3b1c9d22ad9c +outs: 371399a31c02115a9bf265cd0ab8771e diff --git a/dev b/dev index c4b117afdb..8d344a6f84 100755 --- a/dev +++ b/dev @@ -93,6 +93,7 @@ def main(): @cmd(cmds, "run-desktop", "Run Tauri desktop app for development.") def run_desktop(args): + run("rm -rf target/debug/mintter*") run("plz build //backend:mintterd //:yarn") return run("cd desktop/app && cargo tauri dev", args=args) diff --git a/frontend/app/src/client/.generated/documents/v1alpha/documents.ts b/frontend/app/src/client/.generated/documents/v1alpha/documents.ts index 72a0479462..9b01eb1a95 100644 --- a/frontend/app/src/client/.generated/documents/v1alpha/documents.ts +++ b/frontend/app/src/client/.generated/documents/v1alpha/documents.ts @@ -1,11 +1,11 @@ // Code generated by protoc-gen-ts_proto. DO NOT EDIT. //@ts-nocheck /* eslint-disable */ -import { grpc } from "@improbable-eng/grpc-web"; -import { BrowserHeaders } from "browser-headers"; import Long from "long"; +import { grpc } from "@improbable-eng/grpc-web"; import _m0 from "protobufjs/minimal"; import { Empty } from "../../google/protobuf/empty"; +import { BrowserHeaders } from "browser-headers"; import { Timestamp } from "../../google/protobuf/timestamp"; /** Request to create a new draft. */ @@ -53,11 +53,11 @@ export interface UpdateDraftRequestV2 { /** Granular document change. */ export interface DocumentChange { op?: - | { $case: "setTitle"; setTitle: string } - | { $case: "setSubtitle"; setSubtitle: string } - | { $case: "moveBlock"; moveBlock: DocumentChange_MoveBlock } - | { $case: "replaceBlock"; replaceBlock: Block } - | { $case: "deleteBlock"; deleteBlock: string }; + | { $case: "setTitle"; setTitle: string } + | { $case: "setSubtitle"; setSubtitle: string } + | { $case: "moveBlock"; moveBlock: DocumentChange_MoveBlock } + | { $case: "replaceBlock"; replaceBlock: Block } + | { $case: "deleteBlock"; deleteBlock: string }; } /** @@ -224,7 +224,7 @@ export interface Block { text: string; /** Arbitrary attributes of the block. */ attributes: { [key: string]: string }; - /** Annotations of the block. */ + /** Annotation "layers" of the block. */ annotations: Annotation[]; } @@ -674,20 +674,20 @@ export const DocumentChange = { op: isSet(object.setTitle) ? { $case: "setTitle", setTitle: String(object.setTitle) } : isSet(object.setSubtitle) - ? { $case: "setSubtitle", setSubtitle: String(object.setSubtitle) } - : isSet(object.moveBlock) - ? { - $case: "moveBlock", - moveBlock: DocumentChange_MoveBlock.fromJSON(object.moveBlock), - } - : isSet(object.replaceBlock) - ? { - $case: "replaceBlock", - replaceBlock: Block.fromJSON(object.replaceBlock), - } - : isSet(object.deleteBlock) - ? { $case: "deleteBlock", deleteBlock: String(object.deleteBlock) } - : undefined, + ? { $case: "setSubtitle", setSubtitle: String(object.setSubtitle) } + : isSet(object.moveBlock) + ? { + $case: "moveBlock", + moveBlock: DocumentChange_MoveBlock.fromJSON(object.moveBlock), + } + : isSet(object.replaceBlock) + ? { + $case: "replaceBlock", + replaceBlock: Block.fromJSON(object.replaceBlock), + } + : isSet(object.deleteBlock) + ? { $case: "deleteBlock", deleteBlock: String(object.deleteBlock) } + : undefined, }; }, @@ -1819,12 +1819,12 @@ export const Block = { text: isSet(object.text) ? String(object.text) : "", attributes: isObject(object.attributes) ? Object.entries(object.attributes).reduce<{ [key: string]: string }>( - (acc, [key, value]) => { - acc[key] = String(value); - return acc; - }, - {} - ) + (acc, [key, value]) => { + acc[key] = String(value); + return acc; + }, + {} + ) : {}, annotations: Array.isArray(object?.annotations) ? object.annotations.map((e: any) => Annotation.fromJSON(e)) @@ -2021,12 +2021,12 @@ export const Annotation = { type: isSet(object.type) ? String(object.type) : "", attributes: isObject(object.attributes) ? Object.entries(object.attributes).reduce<{ [key: string]: string }>( - (acc, [key, value]) => { - acc[key] = String(value); - return acc; - }, - {} - ) + (acc, [key, value]) => { + acc[key] = String(value); + return acc; + }, + {} + ) : {}, starts: Array.isArray(object?.starts) ? object.starts.map((e: any) => Number(e)) @@ -2811,9 +2811,9 @@ export class GrpcWebImpl { const maybeCombinedMetadata = metadata && this.options.metadata ? new BrowserHeaders({ - ...this.options?.metadata.headersMap, - ...metadata?.headersMap, - }) + ...this.options?.metadata.headersMap, + ...metadata?.headersMap, + }) : metadata || this.options.metadata; return new Promise((resolve, reject) => { grpc.unary(methodDesc, { @@ -2854,8 +2854,8 @@ type DeepPartial = T extends Builtin ? ReadonlyArray> : T extends { $case: string } ? { [K in keyof Omit]?: DeepPartial } & { - $case: T["$case"]; - } + $case: T["$case"]; + } : T extends {} ? { [K in keyof T]?: DeepPartial } : Partial; @@ -2864,9 +2864,9 @@ type KeysOfUnion = T extends T ? keyof T : never; type Exact = P extends Builtin ? P : P & { [K in keyof P]: Exact } & Record< - Exclude>, - never - >; + Exclude>, + never + >; function toTimestamp(date: Date): Timestamp { const seconds = date.getTime() / 1_000;