From 01dc58f75418319c759ec5fe3d9b7caa7eef9df5 Mon Sep 17 00:00:00 2001 From: Sung Won Cho Date: Wed, 6 Jun 2018 21:45:25 +1000 Subject: [PATCH] Change book (#85) * Migrate edit_book action data and action * Fix test * Reduce change of books * Bump version * Add uuid --- core/action.go | 16 ++++- core/core.go | 2 +- core/reducer.go | 44 ++++++++++--- core/reducer_test.go | 53 ++++++++++++++- main_test.go | 2 +- migrate/fixtures/5-post-actions.json | 52 +++++++++++++++ migrate/fixtures/5-pre-actions.json | 52 +++++++++++++++ migrate/migrate.go | 4 ++ migrate/migrate_test.go | 99 ++++++++++++++++++++++++++++ migrate/migrations.go | 65 ++++++++++++++++++ migrate/snapshots.go | 51 ++++++++++++++ 11 files changed, 427 insertions(+), 13 deletions(-) create mode 100644 migrate/fixtures/5-post-actions.json create mode 100644 migrate/fixtures/5-pre-actions.json diff --git a/core/action.go b/core/action.go index c437f2d9..2f95a9f2 100644 --- a/core/action.go +++ b/core/action.go @@ -6,6 +6,7 @@ import ( "github.com/dnote-io/cli/infra" "github.com/pkg/errors" + "github.com/satori/go.uuid" ) var ( @@ -17,7 +18,8 @@ var ( ) type Action struct { - ID int `json:"id"` + UUID string `json:"uuid"` + Schema int `json:"schema"` Type string `json:"type"` Data json.RawMessage `json:"data"` Timestamp int64 `json:"timestamp"` @@ -34,6 +36,8 @@ func LogActionAddNote(ctx infra.DnoteCtx, noteUUID, bookName, content string, ti } action := Action{ + UUID: uuid.NewV4().String(), + Schema: 1, Type: ActionAddNote, Data: b, Timestamp: timestamp, @@ -56,6 +60,8 @@ func LogActionRemoveNote(ctx infra.DnoteCtx, noteUUID, bookName string) error { } action := Action{ + UUID: uuid.NewV4().String(), + Schema: 1, Type: ActionRemoveNote, Data: b, Timestamp: time.Now().Unix(), @@ -71,7 +77,7 @@ func LogActionRemoveNote(ctx infra.DnoteCtx, noteUUID, bookName string) error { func LogActionEditNote(ctx infra.DnoteCtx, noteUUID, bookName, content string, ts int64) error { b, err := json.Marshal(EditNoteData{ NoteUUID: noteUUID, - BookName: bookName, + FromBook: bookName, Content: content, }) if err != nil { @@ -79,6 +85,8 @@ func LogActionEditNote(ctx infra.DnoteCtx, noteUUID, bookName, content string, t } action := Action{ + UUID: uuid.NewV4().String(), + Schema: 1, Type: ActionEditNote, Data: b, Timestamp: ts, @@ -100,6 +108,8 @@ func LogActionAddBook(ctx infra.DnoteCtx, name string) error { } action := Action{ + UUID: uuid.NewV4().String(), + Schema: 1, Type: ActionAddBook, Data: b, Timestamp: time.Now().Unix(), @@ -119,6 +129,8 @@ func LogActionRemoveBook(ctx infra.DnoteCtx, name string) error { } action := Action{ + UUID: uuid.NewV4().String(), + Schema: 1, Type: ActionRemoveBook, Data: b, Timestamp: time.Now().Unix(), diff --git a/core/core.go b/core/core.go index 5725d08c..9a00252d 100644 --- a/core/core.go +++ b/core/core.go @@ -18,7 +18,7 @@ import ( const ( // Version is the current version of dnote - Version = "0.2.1" + Version = "0.2.2" // TimestampFilename is the name of the file containing upgrade info TimestampFilename = "timestamps" diff --git a/core/reducer.go b/core/reducer.go index 22c19f06..a21d7bb1 100644 --- a/core/reducer.go +++ b/core/reducer.go @@ -16,7 +16,8 @@ type AddNoteData struct { type EditNoteData struct { NoteUUID string `json:"note_uuid"` - BookName string `json:"book_name"` + FromBook string `json:"from_book"` + ToBook string `json:"to_book"` Content string `json:"content"` } @@ -156,17 +157,44 @@ func handleEditNote(ctx infra.DnoteCtx, action Action) error { if err != nil { return errors.Wrap(err, "Failed to get dnote") } - book, ok := dnote[data.BookName] + fromBook, ok := dnote[data.FromBook] if !ok { - return errors.Errorf("Book with a name %s is not found", data.BookName) + return errors.Errorf("Origin book with a name %s is not found", data.FromBook) } - for idx, note := range book.Notes { - if note.UUID == data.NoteUUID { - note.Content = data.Content - note.EditedOn = action.Timestamp - dnote[book.Name].Notes[idx] = note + if data.ToBook == "" { + for idx, note := range fromBook.Notes { + if note.UUID == data.NoteUUID { + note.Content = data.Content + note.EditedOn = action.Timestamp + dnote[fromBook.Name].Notes[idx] = note + } } + } else { + // Change the book + + toBook, ok := dnote[data.ToBook] + if !ok { + return errors.Errorf("Destination book with a name %s is not found", data.FromBook) + } + + var index int + var note infra.Note + + // Find the note + for idx := range fromBook.Notes { + note = fromBook.Notes[idx] + + if note.UUID == data.NoteUUID { + index = idx + } + } + + note.Content = data.Content + note.EditedOn = action.Timestamp + + dnote[fromBook.Name] = GetUpdatedBook(dnote[fromBook.Name], append(fromBook.Notes[:index], fromBook.Notes[index+1:]...)) + dnote[toBook.Name] = GetUpdatedBook(dnote[toBook.Name], append(toBook.Notes, note)) } err = WriteDnote(ctx, dnote) diff --git a/core/reducer_test.go b/core/reducer_test.go index 36c8780f..b9760e06 100644 --- a/core/reducer_test.go +++ b/core/reducer_test.go @@ -147,7 +147,7 @@ func TestReduceEditNote(t *testing.T) { // Execute b, err := json.Marshal(&EditNoteData{ - BookName: "js", + FromBook: "js", NoteUUID: "f0d0fbb7-31ff-45ae-9f0f-4e429c0c797f", Content: "updated content", }) @@ -182,6 +182,57 @@ func TestReduceEditNote(t *testing.T) { testutils.AssertEqual(t, otherBook.Notes[0].Content, "wc -l to count words", "other book remaining note content mismatch") } +func TestReduceEditNote_changeBook(t *testing.T) { + // Setup + ctx := testutils.InitCtx("../tmp") + + testutils.SetupTmp(ctx) + defer testutils.ClearTmp(ctx) + testutils.WriteFile(ctx, "../testutils/fixtures/dnote3.json", "dnote") + + // Execute + b, err := json.Marshal(&EditNoteData{ + FromBook: "js", + ToBook: "linux", + NoteUUID: "f0d0fbb7-31ff-45ae-9f0f-4e429c0c797f", + Content: "updated content", + }) + action := Action{ + Type: ActionEditNote, + Data: b, + Timestamp: 1517629805, + } + err = Reduce(ctx, action) + if err != nil { + t.Fatal(errors.Wrap(err, "Failed to process action")) + } + + // Test + dnote, err := GetDnote(ctx) + if err != nil { + t.Fatal(errors.Wrap(err, "Failed to get dnote")) + } + + targetBook := dnote["js"] + otherBook := dnote["linux"] + + if len(targetBook.Notes) != 1 { + t.Fatalf("target book length mismatch. Got %d", len(targetBook.Notes)) + } + if len(otherBook.Notes) != 2 { + t.Fatalf("other book length mismatch. Got %d", len(targetBook.Notes)) + } + + testutils.AssertEqual(t, len(dnote), 2, "number of books mismatch") + testutils.AssertEqual(t, targetBook.Notes[0].UUID, "43827b9a-c2b0-4c06-a290-97991c896653", "remaining note uuid mismatch") + testutils.AssertEqual(t, targetBook.Notes[0].Content, "Booleans have toString()", "remaining note content mismatch") + testutils.AssertEqual(t, otherBook.Notes[0].UUID, "3e065d55-6d47-42f2-a6bf-f5844130b2d2", "other book remaining note uuid mismatch") + testutils.AssertEqual(t, otherBook.Notes[0].Content, "wc -l to count words", "other book remaining note content mismatch") + testutils.AssertEqual(t, otherBook.Notes[1].UUID, "f0d0fbb7-31ff-45ae-9f0f-4e429c0c797f", "edited note uuid mismatch") + testutils.AssertEqual(t, otherBook.Notes[1].Content, "updated content", "edited note content mismatch") + testutils.AssertEqual(t, otherBook.Notes[1].EditedOn, int64(1517629805), "edited note edited_on mismatch") +} + func TestReduceAddBook(t *testing.T) { // Setup ctx := testutils.InitCtx("../tmp") diff --git a/main_test.go b/main_test.go index e93638fb..39b33f08 100644 --- a/main_test.go +++ b/main_test.go @@ -216,7 +216,7 @@ func TestEdit_ContentFlag(t *testing.T) { testutils.AssertEqual(t, len(actions), 1, "There should be 1 action") testutils.AssertEqual(t, action.Type, core.ActionEditNote, "action type mismatch") testutils.AssertEqual(t, actionData.Content, "foo bar", "action data name mismatch") - testutils.AssertEqual(t, actionData.BookName, "js", "action data book_name mismatch") + testutils.AssertEqual(t, actionData.FromBook, "js", "action data from_book mismatch") testutils.AssertEqual(t, actionData.NoteUUID, "f0d0fbb7-31ff-45ae-9f0f-4e429c0c797f", "action data note_uuis mismatch") testutils.AssertNotEqual(t, action.Timestamp, 0, "action timestamp mismatch") testutils.AssertEqual(t, len(book.Notes), 2, "Book should have one note") diff --git a/migrate/fixtures/5-post-actions.json b/migrate/fixtures/5-post-actions.json new file mode 100644 index 00000000..46301785 --- /dev/null +++ b/migrate/fixtures/5-post-actions.json @@ -0,0 +1,52 @@ +[ + { + "schema": 1, + "type": "add_book", + "data": { "book_name": "js" }, + "timestamp": 1528111176 + }, + { + "schema": 1, + "type": "add_note", + "data": { + "note_uuid": "557d6d5d-95c0-4dd3-8252-a8e187254f5c", + "book_name": "js", + "content": "some content" + }, + "timestamp": 1528111176 + }, + { + "schema": 1, + "type": "edit_note", + "data": { + "note_uuid": "557d6d5d-95c0-4dd3-8252-a8e187254f5c", + "from_book": "js", + "content": "some content edited" + }, + "timestamp": 1528111193 + }, + { + "schema": 1, + "type": "remove_note", + "data": { + "note_uuid": "753f8053-145d-46e9-a44b-bc7b0a47d9d5", + "book_name": "js" + }, + "timestamp": 1528111232 + }, + { + "schema": 1, + "type": "remove_note", + "data": { + "note_uuid": "753f8053-145d-46e9-a44b-bc7b0a47d9d5", + "book_name": "js" + }, + "timestamp": 1528111235 + }, + { + "schema": 1, + "type": "remove_book", + "data": { "book_name": "js" }, + "timestamp": 1528111245 + } +] diff --git a/migrate/fixtures/5-pre-actions.json b/migrate/fixtures/5-pre-actions.json new file mode 100644 index 00000000..c54b8131 --- /dev/null +++ b/migrate/fixtures/5-pre-actions.json @@ -0,0 +1,52 @@ +[ + { + "id": 0, + "type": "add_book", + "data": { "book_name": "js" }, + "timestamp": 1528111176 + }, + { + "id": 0, + "type": "add_note", + "data": { + "note_uuid": "557d6d5d-95c0-4dd3-8252-a8e187254f5c", + "book_name": "js", + "content": "some content" + }, + "timestamp": 1528111176 + }, + { + "id": 0, + "type": "edit_note", + "data": { + "note_uuid": "557d6d5d-95c0-4dd3-8252-a8e187254f5c", + "book_name": "js", + "content": "some content edited" + }, + "timestamp": 1528111193 + }, + { + "id": 0, + "type": "remove_note", + "data": { + "note_uuid": "753f8053-145d-46e9-a44b-bc7b0a47d9d5", + "book_name": "js" + }, + "timestamp": 1528111232 + }, + { + "id": 0, + "type": "remove_note", + "data": { + "note_uuid": "753f8053-145d-46e9-a44b-bc7b0a47d9d5", + "book_name": "js" + }, + "timestamp": 1528111235 + }, + { + "id": 0, + "type": "remove_book", + "data": { "book_name": "js" }, + "timestamp": 1528111245 + } +] diff --git a/migrate/migrate.go b/migrate/migrate.go index 017823f3..c3368348 100644 --- a/migrate/migrate.go +++ b/migrate/migrate.go @@ -25,6 +25,7 @@ const ( migrationV2 migrationV3 migrationV4 + migrationV5 ) var migrationSequence = []int{ @@ -32,6 +33,7 @@ var migrationSequence = []int{ migrationV2, migrationV3, migrationV4, + migrationV5, } type schema struct { @@ -85,6 +87,8 @@ func performMigration(ctx infra.DnoteCtx, migrationID int) error { migrationError = migrateToV3(ctx) case migrationV4: migrationError = migrateToV4(ctx) + case migrationV5: + migrationError = migrateToV5(ctx) default: return errors.Errorf("Unrecognized migration id %d", migrationID) } diff --git a/migrate/migrate_test.go b/migrate/migrate_test.go index a9897936..82e1214d 100644 --- a/migrate/migrate_test.go +++ b/migrate/migrate_test.go @@ -2,6 +2,7 @@ package migrate import ( "encoding/json" + "fmt" "io/ioutil" "os" "path/filepath" @@ -193,3 +194,101 @@ func TestMigrateToV4(t *testing.T) { testutils.AssertEqual(t, config.APIKey, "Oev6e1082ORasdf9rjkfjkasdfjhgei", "api key mismatch") testutils.AssertEqual(t, config.Editor, "vim", "editor mismatch") } + +func TestMigrateToV5(t *testing.T) { + ctx := testutils.InitCtx("../tmp") + + // set up + testutils.SetupTmp(ctx) + testutils.WriteFile(ctx, "./fixtures/5-pre-actions.json", "actions") + defer testutils.ClearTmp(ctx) + + // execute + if err := migrateToV5(ctx); err != nil { + t.Fatal(errors.Wrap(err, "migrating").Error()) + } + + // test + var oldActions []migrateToV5PreAction + testutils.ReadJSON("./fixtures/5-pre-actions.json", &oldActions) + + b := testutils.ReadFile(ctx, "actions") + var migratedActions []migrateToV5PostAction + if err := json.Unmarshal(b, &migratedActions); err != nil { + t.Fatal(errors.Wrap(err, "unmarhsalling migrated actions").Error()) + } + + if len(oldActions) != len(migratedActions) { + t.Fatalf("There were %d actions but after migration there were %d", len(oldActions), len(migratedActions)) + } + + for idx := range migratedActions { + migrated := migratedActions[idx] + old := oldActions[idx] + + testutils.AssertNotEqual(t, migrated.UUID, "", fmt.Sprintf("uuid mismatch for migrated item with index %d", idx)) + testutils.AssertEqual(t, migrated.Schema, 1, fmt.Sprintf("schema mismatch for migrated item with index %d", idx)) + testutils.AssertEqual(t, migrated.Timestamp, old.Timestamp, fmt.Sprintf("timestamp mismatch for migrated item with index %d", idx)) + testutils.AssertEqual(t, migrated.Type, old.Type, fmt.Sprintf("timestamp mismatch for migrated item with index %d", idx)) + + switch migrated.Type { + case migrateToV5ActionAddNote: + var oldData, migratedData migrateToV5AddNoteData + if err := json.Unmarshal(old.Data, &oldData); err != nil { + t.Fatal(errors.Wrap(err, "unmarhsalling old data").Error()) + } + if err := json.Unmarshal(migrated.Data, &migratedData); err != nil { + t.Fatal(errors.Wrap(err, "unmarhsalling new data").Error()) + } + + testutils.AssertEqual(t, oldData.BookName, migratedData.BookName, fmt.Sprintf("data book_name mismatch for item idx %d", idx)) + testutils.AssertEqual(t, oldData.Content, migratedData.Content, fmt.Sprintf("data content mismatch for item idx %d", idx)) + testutils.AssertEqual(t, oldData.NoteUUID, migratedData.NoteUUID, fmt.Sprintf("data note_uuid mismatch for item idx %d", idx)) + case migrateToV5ActionRemoveNote: + var oldData, migratedData migrateToV5RemoveNoteData + if err := json.Unmarshal(old.Data, &oldData); err != nil { + t.Fatal(errors.Wrap(err, "unmarhsalling old data").Error()) + } + if err := json.Unmarshal(migrated.Data, &migratedData); err != nil { + t.Fatal(errors.Wrap(err, "unmarhsalling new data").Error()) + } + + testutils.AssertEqual(t, oldData.BookName, migratedData.BookName, fmt.Sprintf("data book_name mismatch for item idx %d", idx)) + testutils.AssertEqual(t, oldData.NoteUUID, migratedData.NoteUUID, fmt.Sprintf("data note_uuid mismatch for item idx %d", idx)) + case migrateToV5ActionAddBook: + var oldData, migratedData migrateToV5AddBookData + if err := json.Unmarshal(old.Data, &oldData); err != nil { + t.Fatal(errors.Wrap(err, "unmarhsalling old data").Error()) + } + if err := json.Unmarshal(migrated.Data, &migratedData); err != nil { + t.Fatal(errors.Wrap(err, "unmarhsalling new data").Error()) + } + + testutils.AssertEqual(t, oldData.BookName, migratedData.BookName, fmt.Sprintf("data book_name mismatch for item idx %d", idx)) + case migrateToV5ActionRemoveBook: + var oldData, migratedData migrateToV5RemoveBookData + if err := json.Unmarshal(old.Data, &oldData); err != nil { + t.Fatal(errors.Wrap(err, "unmarhsalling old data").Error()) + } + if err := json.Unmarshal(migrated.Data, &migratedData); err != nil { + t.Fatal(errors.Wrap(err, "unmarhsalling new data").Error()) + } + + testutils.AssertEqual(t, oldData.BookName, migratedData.BookName, fmt.Sprintf("data book_name mismatch for item idx %d", idx)) + case migrateToV5ActionEditNote: + var oldData migrateToV5PreEditNoteData + var migratedData migrateToV5PostEditNoteData + if err := json.Unmarshal(old.Data, &oldData); err != nil { + t.Fatal(errors.Wrap(err, "unmarhsalling old data").Error()) + } + if err := json.Unmarshal(migrated.Data, &migratedData); err != nil { + t.Fatal(errors.Wrap(err, "unmarhsalling new data").Error()) + } + + testutils.AssertEqual(t, oldData.NoteUUID, migratedData.NoteUUID, fmt.Sprintf("data note_uuid mismatch for item idx %d", idx)) + testutils.AssertEqual(t, oldData.Content, migratedData.Content, fmt.Sprintf("data content mismatch for item idx %d", idx)) + testutils.AssertEqual(t, oldData.BookName, migratedData.FromBook, "book_name should have been renamed to from_book") + testutils.AssertEqual(t, migratedData.ToBook, "", "to_book should be empty") + } + } +} diff --git a/migrate/migrations.go b/migrate/migrations.go index 1154bce2..96a0c1d3 100644 --- a/migrate/migrations.go +++ b/migrate/migrations.go @@ -197,3 +197,68 @@ func migrateToV4(ctx infra.DnoteCtx) error { return nil } + +// migrateToV5 migrates actions +func migrateToV5(ctx infra.DnoteCtx) error { + actionsPath := fmt.Sprintf("%s/actions", ctx.DnoteDir) + + b, err := ioutil.ReadFile(actionsPath) + if err != nil { + return errors.Wrap(err, "reading the actions file") + } + + var actions []migrateToV5PreAction + err = json.Unmarshal(b, &actions) + if err != nil { + return errors.Wrap(err, "unmarshalling actions to JSON") + } + + result := []migrateToV5PostAction{} + + for _, action := range actions { + var data json.RawMessage + + switch action.Type { + case migrateToV5ActionEditNote: + var oldData migrateToV5PreEditNoteData + if err = json.Unmarshal(action.Data, &oldData); err != nil { + return errors.Wrapf(err, "unmarshalling old data of an edit note action %s", action.ID) + } + + migratedData := migrateToV5PostEditNoteData{ + NoteUUID: oldData.NoteUUID, + FromBook: oldData.BookName, + Content: oldData.Content, + } + b, err = json.Marshal(migratedData) + if err != nil { + return errors.Wrap(err, "marshalling data") + } + + data = b + default: + data = action.Data + } + + migrated := migrateToV5PostAction{ + UUID: uuid.NewV4().String(), + Schema: 1, + Type: action.Type, + Data: data, + Timestamp: action.Timestamp, + } + + result = append(result, migrated) + } + + a, err := json.Marshal(result) + if err != nil { + return errors.Wrap(err, "marshalling result into JSON") + } + err = ioutil.WriteFile(actionsPath, a, 0644) + if err != nil { + return errors.Wrap(err, "writing the result into a file") + } + + return nil +} diff --git a/migrate/snapshots.go b/migrate/snapshots.go index 715722e4..4ec40b98 100644 --- a/migrate/snapshots.go +++ b/migrate/snapshots.go @@ -1,5 +1,7 @@ package migrate +import "encoding/json" + // v2 type migrateToV2PreNote struct { UID string @@ -53,3 +55,52 @@ type migrateToV4PostConfig struct { Editor string APIKey string } + +// v5 +type migrateToV5AddNoteData struct { + NoteUUID string `json:"note_uuid"` + BookName string `json:"book_name"` + Content string `json:"content"` +} +type migrateToV5RemoveNoteData struct { + NoteUUID string `json:"note_uuid"` + BookName string `json:"book_name"` +} +type migrateToV5AddBookData struct { + BookName string `json:"book_name"` +} +type migrateToV5RemoveBookData struct { + BookName string `json:"book_name"` +} +type migrateToV5PreEditNoteData struct { + NoteUUID string `json:"note_uuid"` + BookName string `json:"book_name"` + Content string `json:"content"` +} +type migrateToV5PostEditNoteData struct { + NoteUUID string `json:"note_uuid"` + FromBook string `json:"from_book"` + ToBook string `json:"to_book"` + Content string `json:"content"` +} +type migrateToV5PreAction struct { + ID int `json:"id"` + Type string `json:"type"` + Data json.RawMessage `json:"data"` + Timestamp int64 `json:"timestamp"` +} +type migrateToV5PostAction struct { + UUID string `json:"uuid"` + Schema int `json:"schema"` + Type string `json:"type"` + Data json.RawMessage `json:"data"` + Timestamp int64 `json:"timestamp"` +} + +var ( + migrateToV5ActionAddNote = "add_note" + migrateToV5ActionRemoveNote = "remove_note" + migrateToV5ActionEditNote = "edit_note" + migrateToV5ActionAddBook = "add_book" + migrateToV5ActionRemoveBook = "remove_book" +)