Change book (#85)

* Migrate edit_book action data and action

* Fix test

* Reduce change of books

* Bump version

* Add uuid
This commit is contained in:
Sung Won Cho 2018-06-06 21:45:25 +10:00 committed by GitHub
commit 01dc58f754
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
11 changed files with 427 additions and 13 deletions

View file

@ -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(),

View file

@ -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"

View file

@ -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)

View file

@ -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")

View file

@ -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")

View file

@ -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
}
]

View file

@ -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
}
]

View file

@ -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)
}

View file

@ -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")
}
}
}

View file

@ -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
}

View file

@ -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"
)