Implement state-based sync (#144)

* Migrate uuids of books that already exist in server

* Remove actions

* Add dirty flag for notes and books

* Drop actions table

* Implement sync

* Add debug

* Update uuid after posting resources to the server

* Fix dev script
This commit is contained in:
Sung Won Cho 2018-12-02 11:04:16 +10:00 committed by GitHub
commit fa1da50fc5
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
52 changed files with 6558 additions and 1389 deletions

420
client/client.go Normal file
View file

@ -0,0 +1,420 @@
// Package client provides interfaces for interacting with the Dnote server
// and the data structures for responses
package client
import (
"encoding/json"
"fmt"
"io/ioutil"
"net/http"
"net/url"
"strconv"
"strings"
"time"
"github.com/dnote/cli/infra"
"github.com/dnote/cli/utils"
"github.com/pkg/errors"
)
// GetSyncStateResp is the response get sync state endpoint
type GetSyncStateResp struct {
FullSyncBefore int `json:"full_sync_before"`
MaxUSN int `json:"max_usn"`
CurrentTime int64 `json:"current_time"`
}
// GetSyncState gets the sync state response from the server
func GetSyncState(apiKey string, ctx infra.DnoteCtx) (GetSyncStateResp, error) {
var ret GetSyncStateResp
res, err := utils.DoAuthorizedReq(ctx, apiKey, "GET", "/v1/sync/state", "")
if err != nil {
return ret, errors.Wrap(err, "constructing http request")
}
body, err := ioutil.ReadAll(res.Body)
if err != nil {
return ret, errors.Wrap(err, "reading the response body")
}
if err = json.Unmarshal(body, &ret); err != nil {
return ret, errors.Wrap(err, "unmarshalling the payload")
}
return ret, nil
}
// SyncFragNote represents a note in a sync fragment and contains only the necessary information
// for the client to sync the note locally
type SyncFragNote struct {
UUID string `json:"uuid"`
BookUUID string `json:"book_uuid"`
USN int `json:"usn"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
AddedOn int64 `json:"added_on"`
EditedOn int64 `json:"edited_on"`
Content string `json:"content"`
Public bool `json:"public"`
Deleted bool `json:"deleted"`
}
// SyncFragBook represents a book in a sync fragment and contains only the necessary information
// for the client to sync the note locally
type SyncFragBook struct {
UUID string `json:"uuid"`
USN int `json:"usn"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
AddedOn int64 `json:"added_on"`
Label string `json:"label"`
Deleted bool `json:"deleted"`
}
// SyncFragment contains a piece of information about the server's state.
type SyncFragment struct {
FragMaxUSN int `json:"frag_max_usn"`
UserMaxUSN int `json:"user_max_usn"`
CurrentTime int64 `json:"current_time"`
Notes []SyncFragNote `json:"notes"`
Books []SyncFragBook `json:"books"`
ExpungedNotes []string `json:"expunged_notes"`
ExpungedBooks []string `json:"expunged_books"`
}
// GetSyncFragmentResp is the response from the get sync fragment endpoint
type GetSyncFragmentResp struct {
Fragment SyncFragment `json:"fragment"`
}
// GetSyncFragment gets a sync fragment response from the server
func GetSyncFragment(ctx infra.DnoteCtx, apiKey string, afterUSN int) (GetSyncFragmentResp, error) {
v := url.Values{}
v.Set("after_usn", strconv.Itoa(afterUSN))
queryStr := v.Encode()
path := fmt.Sprintf("/v1/sync/fragment?%s", queryStr)
res, err := utils.DoAuthorizedReq(ctx, apiKey, "GET", path, "")
body, err := ioutil.ReadAll(res.Body)
if err != nil {
return GetSyncFragmentResp{}, errors.Wrap(err, "reading the response body")
}
var resp GetSyncFragmentResp
if err = json.Unmarshal(body, &resp); err != nil {
return resp, errors.Wrap(err, "unmarshalling the payload")
}
return resp, nil
}
// RespBook is the book in the response from the create book api
type RespBook struct {
ID int `json:"id"`
UUID string `json:"uuid"`
USN int `json:"usn"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
Label string `json:"label"`
}
// CreateBookPayload is a payload for creating a book
type CreateBookPayload struct {
Name string `json:"name"`
}
// CreateBookResp is the response from create book api
type CreateBookResp struct {
Book RespBook `json:"book"`
}
// checkRespErr checks if the given http response indicates an error. It returns a boolean indicating
// if the response is an error, and a decoded error message.
func checkRespErr(res *http.Response) (bool, string, error) {
if res.StatusCode < 400 {
return false, "", nil
}
body, err := ioutil.ReadAll(res.Body)
if err != nil {
return true, "", errors.Wrapf(err, "server responded with %d but could not read the response body", res.StatusCode)
}
bodyStr := string(body)
message := fmt.Sprintf(`response %d "%s"`, res.StatusCode, strings.TrimRight(bodyStr, "\n"))
return true, message, nil
}
// CreateBook creates a new book in the server
func CreateBook(ctx infra.DnoteCtx, apiKey, label string) (CreateBookResp, error) {
payload := CreateBookPayload{
Name: label,
}
b, err := json.Marshal(payload)
if err != nil {
return CreateBookResp{}, errors.Wrap(err, "marshaling payload")
}
res, err := utils.DoAuthorizedReq(ctx, apiKey, "POST", "/v1/books", string(b))
if err != nil {
return CreateBookResp{}, errors.Wrap(err, "posting a book to the server")
}
ok, message, err := checkRespErr(res)
if err != nil {
return CreateBookResp{}, errors.Wrap(err, "checking repsonse error")
}
if ok {
return CreateBookResp{}, errors.New(message)
}
var resp CreateBookResp
if err := json.NewDecoder(res.Body).Decode(&resp); err != nil {
return resp, errors.Wrap(err, "decoding response payload")
}
return resp, nil
}
type updateBookPayload struct {
Name *string `json:"name"`
}
// UpdateBookResp is the response from create book api
type UpdateBookResp struct {
Book RespBook `json:"book"`
}
// UpdateBook updates a book in the server
func UpdateBook(ctx infra.DnoteCtx, apiKey, label, uuid string) (UpdateBookResp, error) {
payload := updateBookPayload{
Name: &label,
}
b, err := json.Marshal(payload)
if err != nil {
return UpdateBookResp{}, errors.Wrap(err, "marshaling payload")
}
endpoint := fmt.Sprintf("/v1/books/%s", uuid)
res, err := utils.DoAuthorizedReq(ctx, apiKey, "PATCH", endpoint, string(b))
if err != nil {
return UpdateBookResp{}, errors.Wrap(err, "posting a book to the server")
}
ok, message, err := checkRespErr(res)
if err != nil {
return UpdateBookResp{}, errors.Wrap(err, "checking repsonse error")
}
if ok {
return UpdateBookResp{}, errors.New(message)
}
var resp UpdateBookResp
if err := json.NewDecoder(res.Body).Decode(&resp); err != nil {
return resp, errors.Wrap(err, "decoding payload")
}
return resp, nil
}
// DeleteBookResp is the response from create book api
type DeleteBookResp struct {
Status int `json:"status"`
Book RespBook `json:"book"`
}
// DeleteBook deletes a book in the server
func DeleteBook(ctx infra.DnoteCtx, apiKey, uuid string) (DeleteBookResp, error) {
endpoint := fmt.Sprintf("/v1/books/%s", uuid)
res, err := utils.DoAuthorizedReq(ctx, apiKey, "DELETE", endpoint, "")
if err != nil {
return DeleteBookResp{}, errors.Wrap(err, "deleting a book in the server")
}
ok, message, err := checkRespErr(res)
if err != nil {
return DeleteBookResp{}, errors.Wrap(err, "checking repsonse error")
}
if ok {
return DeleteBookResp{}, errors.New(message)
}
var resp DeleteBookResp
if err := json.NewDecoder(res.Body).Decode(&resp); err != nil {
return resp, errors.Wrap(err, "decoding the response")
}
return resp, nil
}
// CreateNotePayload is a payload for creating a note
type CreateNotePayload struct {
BookUUID string `json:"book_uuid"`
Content string `json:"content"`
}
// CreateNoteResp is the response from create note endpoint
type CreateNoteResp struct {
Result RespNote `json:"result"`
}
type respNoteBook struct {
UUID string `json:"uuid"`
Label string `json:"label"`
}
type respNoteUser struct {
Name string `json:"name"`
}
// RespNote is a note in the response
type RespNote struct {
UUID string `json:"uuid"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
Content string `json:"content"`
AddedOn int64 `json:"added_on"`
Public bool `json:"public"`
USN int `json:"usn"`
Book respNoteBook `json:"book"`
User respNoteUser `json:"user"`
}
// CreateNote creates a note in the server
func CreateNote(ctx infra.DnoteCtx, apiKey, bookUUID, content string) (CreateNoteResp, error) {
payload := CreateNotePayload{
BookUUID: bookUUID,
Content: content,
}
b, err := json.Marshal(payload)
if err != nil {
return CreateNoteResp{}, errors.Wrap(err, "marshaling payload")
}
res, err := utils.DoAuthorizedReq(ctx, apiKey, "POST", "/v1/notes", string(b))
if err != nil {
return CreateNoteResp{}, errors.Wrap(err, "posting a book to the server")
}
ok, message, err := checkRespErr(res)
if err != nil {
return CreateNoteResp{}, errors.Wrap(err, "checking repsonse error")
}
if ok {
return CreateNoteResp{}, errors.New(message)
}
var resp CreateNoteResp
if err := json.NewDecoder(res.Body).Decode(&resp); err != nil {
return CreateNoteResp{}, errors.Wrap(err, "decoding payload")
}
return resp, nil
}
type updateNotePayload struct {
BookUUID *string `json:"book_uuid"`
Content *string `json:"content"`
Public *bool `json:"public"`
}
// UpdateNoteResp is the response from create book api
type UpdateNoteResp struct {
Status int `json:"status"`
Result RespNote `json:"result"`
}
// UpdateNote updates a note in the server
func UpdateNote(ctx infra.DnoteCtx, apiKey, uuid, bookUUID, content string, public bool) (UpdateNoteResp, error) {
payload := updateNotePayload{
BookUUID: &bookUUID,
Content: &content,
Public: &public,
}
b, err := json.Marshal(payload)
if err != nil {
return UpdateNoteResp{}, errors.Wrap(err, "marshaling payload")
}
endpoint := fmt.Sprintf("/v1/notes/%s", uuid)
res, err := utils.DoAuthorizedReq(ctx, apiKey, "PATCH", endpoint, string(b))
if err != nil {
return UpdateNoteResp{}, errors.Wrap(err, "patching a note to the server")
}
ok, message, err := checkRespErr(res)
if err != nil {
return UpdateNoteResp{}, errors.Wrap(err, "checking repsonse error")
}
if ok {
return UpdateNoteResp{}, errors.New(message)
}
var resp UpdateNoteResp
if err := json.NewDecoder(res.Body).Decode(&resp); err != nil {
return UpdateNoteResp{}, errors.Wrap(err, "decoding payload")
}
return resp, nil
}
// DeleteNoteResp is the response from remove note api
type DeleteNoteResp struct {
Status int `json:"status"`
Result RespNote `json:"result"`
}
// DeleteNote removes a note in the server
func DeleteNote(ctx infra.DnoteCtx, apiKey, uuid string) (DeleteNoteResp, error) {
endpoint := fmt.Sprintf("/v1/notes/%s", uuid)
res, err := utils.DoAuthorizedReq(ctx, apiKey, "DELETE", endpoint, "")
if err != nil {
return DeleteNoteResp{}, errors.Wrap(err, "patching a note to the server")
}
ok, message, err := checkRespErr(res)
if err != nil {
return DeleteNoteResp{}, errors.Wrap(err, "checking repsonse error")
}
if ok {
return DeleteNoteResp{}, errors.New(message)
}
var resp DeleteNoteResp
if err := json.NewDecoder(res.Body).Decode(&resp); err != nil {
return DeleteNoteResp{}, errors.Wrap(err, "decoding payload")
}
return resp, nil
}
// GetBooksResp is a response from get books endpoint
type GetBooksResp []struct {
UUID string `json:"uuid"`
Label string `json:"label"`
}
// GetBooks gets books from the server
func GetBooks(ctx infra.DnoteCtx, apiKey string) (GetBooksResp, error) {
res, err := utils.DoAuthorizedReq(ctx, apiKey, "GET", "/v1/books", "")
if err != nil {
return GetBooksResp{}, errors.Wrap(err, "making http request")
}
ok, message, err := checkRespErr(res)
if err != nil {
return GetBooksResp{}, errors.Wrap(err, "checking repsonse error")
}
if ok {
return GetBooksResp{}, errors.New(message)
}
var resp GetBooksResp
if err := json.NewDecoder(res.Body).Decode(&resp); err != nil {
return GetBooksResp{}, errors.Wrap(err, "decoding payload")
}
return resp, nil
}

View file

@ -13,6 +13,8 @@ import (
"github.com/spf13/cobra"
)
var reservedBookNames = []string{"trash", "conflicts"}
var content string
var example = `
@ -47,10 +49,24 @@ func NewCmd(ctx infra.DnoteCtx) *cobra.Command {
return cmd
}
func isReservedName(name string) bool {
for _, n := range reservedBookNames {
if name == n {
return true
}
}
return false
}
func newRun(ctx infra.DnoteCtx) core.RunEFunc {
return func(cmd *cobra.Command, args []string) error {
bookName := args[0]
if isReservedName(bookName) {
return errors.Errorf("book name '%s' is reserved", bookName)
}
if content == "" {
fpath := core.GetDnoteTmpContentPath(ctx)
err := core.GetEditorInput(ctx, fpath, &content)
@ -92,33 +108,25 @@ func writeNote(ctx infra.DnoteCtx, bookLabel string, content string, ts int64) e
err = tx.QueryRow("SELECT uuid FROM books WHERE label = ?", bookLabel).Scan(&bookUUID)
if err == sql.ErrNoRows {
bookUUID = utils.GenerateUUID()
_, err = tx.Exec("INSERT INTO books (uuid, label) VALUES (?, ?)", bookUUID, bookLabel)
b := core.NewBook(bookUUID, bookLabel, 0, false, true)
err = b.Insert(tx)
if err != nil {
tx.Rollback()
return errors.Wrap(err, "creating the book")
}
err = core.LogActionAddBook(tx, bookLabel)
if err != nil {
tx.Rollback()
return errors.Wrap(err, "logging action")
}
} else if err != nil {
return errors.Wrap(err, "finding the book")
}
noteUUID := utils.GenerateUUID()
_, err = tx.Exec(`INSERT INTO notes (uuid, book_uuid, content, added_on, public)
VALUES (?, ?, ?, ?, ?);`, noteUUID, bookUUID, content, ts, false)
n := core.NewNote(noteUUID, bookUUID, content, ts, 0, 0, false, false, true)
err = n.Insert(tx)
if err != nil {
tx.Rollback()
return errors.Wrap(err, "creating the note")
}
err = core.LogActionAddNote(tx, noteUUID, bookLabel, content, ts)
if err != nil {
tx.Rollback()
return errors.Wrap(err, "logging action")
}
tx.Commit()

View file

@ -91,20 +91,15 @@ func newRun(ctx infra.DnoteCtx) core.RunEFunc {
if err != nil {
return errors.Wrap(err, "beginning a transaction")
}
_, err = tx.Exec(`UPDATE notes
SET content = ?, edited_on = ?
WHERE id = ? AND book_uuid = ?`, newContent, ts, noteID, bookUUID)
SET content = ?, edited_on = ?, dirty = ?
WHERE id = ? AND book_uuid = ?`, newContent, ts, true, noteID, bookUUID)
if err != nil {
tx.Rollback()
return errors.Wrap(err, "updating the note")
}
err = core.LogActionEditNote(tx, noteUUID, bookLabel, newContent, ts)
if err != nil {
tx.Rollback()
return errors.Wrap(err, "logging an action")
}
tx.Commit()
log.Success("edited the note\n")

View file

@ -95,12 +95,9 @@ func removeNote(ctx infra.DnoteCtx, noteID, bookLabel string) error {
return errors.Wrap(err, "beginning a transaction")
}
if _, err = tx.Exec("DELETE FROM notes WHERE uuid = ? AND book_uuid = ?", noteUUID, bookUUID); err != nil {
if _, err = tx.Exec("UPDATE notes SET deleted = ?, dirty = ?, content = ? WHERE uuid = ? AND book_uuid = ?", true, true, "", noteUUID, bookUUID); err != nil {
return errors.Wrap(err, "removing the note")
}
if err = core.LogActionRemoveNote(tx, noteUUID, bookLabel); err != nil {
return errors.Wrap(err, "logging the remove_note action")
}
tx.Commit()
log.Successf("removed from %s\n", bookLabel)
@ -130,15 +127,15 @@ func removeBook(ctx infra.DnoteCtx, bookLabel string) error {
return errors.Wrap(err, "beginning a transaction")
}
if _, err = tx.Exec("DELETE FROM notes WHERE book_uuid = ?", bookUUID); err != nil {
if _, err = tx.Exec("UPDATE notes SET deleted = ?, dirty = ?, content = ? WHERE book_uuid = ?", true, true, "", bookUUID); err != nil {
return errors.Wrap(err, "removing notes in the book")
}
if _, err = tx.Exec("DELETE FROM books WHERE uuid = ?", bookUUID); err != nil {
// override the label with a random string
uniqLabel := utils.GenerateUUID()
if _, err = tx.Exec("UPDATE books SET deleted = ?, dirty = ?, label = ? WHERE uuid = ?", true, true, uniqLabel, bookUUID); err != nil {
return errors.Wrap(err, "removing the book")
}
if err = core.LogActionRemoveBook(tx, bookLabel); err != nil {
return errors.Wrap(err, "loging the remove_book action")
}
tx.Commit()

View file

@ -34,14 +34,14 @@ func Prepare(ctx infra.DnoteCtx) error {
if err := infra.InitDB(ctx); err != nil {
return errors.Wrap(err, "initializing database")
}
if err := infra.InitSystem(ctx); err != nil {
if err := core.InitSystem(ctx); err != nil {
return errors.Wrap(err, "initializing system data")
}
if err := migrate.Legacy(ctx); err != nil {
return errors.Wrap(err, "running legacy migration")
}
if err := migrate.Run(ctx, migrate.LocalSequence); err != nil {
if err := migrate.Run(ctx, migrate.LocalSequence, migrate.LocalMode); err != nil {
return errors.Wrap(err, "running migration")
}

File diff suppressed because it is too large Load diff

3001
cmd/sync/sync_test.go Normal file

File diff suppressed because it is too large Load diff

View file

@ -7,6 +7,7 @@ import (
"github.com/spf13/cobra"
)
// NewCmd returns a new version command
func NewCmd(ctx infra.DnoteCtx) *cobra.Command {
cmd := &cobra.Command{
Use: "version",

View file

@ -29,6 +29,7 @@ func preRun(cmd *cobra.Command, args []string) error {
return nil
}
// NewCmd returns a new view command
func NewCmd(ctx infra.DnoteCtx) *cobra.Command {
cmd := &cobra.Command{
Use: "view <book name?> <note index?>",

View file

@ -1,98 +0,0 @@
package core
import (
"database/sql"
"encoding/json"
"time"
"github.com/dnote/actions"
"github.com/pkg/errors"
)
// LogActionAddNote logs an action for adding a note
func LogActionAddNote(tx *sql.Tx, noteUUID, bookName, content string, timestamp int64) error {
b, err := json.Marshal(actions.AddNoteDataV2{
NoteUUID: noteUUID,
BookName: bookName,
Content: content,
// TODO: support adding a public note
Public: false,
})
if err != nil {
return errors.Wrap(err, "marshalling data into JSON")
}
if err := LogAction(tx, 2, actions.ActionAddNote, string(b), timestamp); err != nil {
return errors.Wrapf(err, "logging action")
}
return nil
}
// LogActionRemoveNote logs an action for removing a book
func LogActionRemoveNote(tx *sql.Tx, noteUUID, bookName string) error {
b, err := json.Marshal(actions.RemoveNoteDataV2{
NoteUUID: noteUUID,
})
if err != nil {
return errors.Wrap(err, "marshalling data into JSON")
}
ts := time.Now().UnixNano()
if err := LogAction(tx, 2, actions.ActionRemoveNote, string(b), ts); err != nil {
return errors.Wrapf(err, "logging action")
}
return nil
}
// LogActionEditNote logs an action for editing a note
func LogActionEditNote(tx *sql.Tx, noteUUID, bookName, content string, ts int64) error {
b, err := json.Marshal(actions.EditNoteDataV3{
NoteUUID: noteUUID,
Content: &content,
BookName: nil,
Public: nil,
})
if err != nil {
return errors.Wrap(err, "marshalling data into JSON")
}
if err := LogAction(tx, 3, actions.ActionEditNote, string(b), ts); err != nil {
return errors.Wrapf(err, "logging action")
}
return nil
}
// LogActionAddBook logs an action for adding a book
func LogActionAddBook(tx *sql.Tx, name string) error {
b, err := json.Marshal(actions.AddBookDataV1{
BookName: name,
})
if err != nil {
return errors.Wrap(err, "marshalling data into JSON")
}
ts := time.Now().UnixNano()
if err := LogAction(tx, 1, actions.ActionAddBook, string(b), ts); err != nil {
return errors.Wrapf(err, "logging action")
}
return nil
}
// LogActionRemoveBook logs an action for removing book
func LogActionRemoveBook(tx *sql.Tx, name string) error {
b, err := json.Marshal(actions.RemoveBookDataV1{BookName: name})
if err != nil {
return errors.Wrap(err, "marshalling data into JSON")
}
ts := time.Now().UnixNano()
if err := LogAction(tx, 1, actions.ActionRemoveBook, string(b), ts); err != nil {
return errors.Wrapf(err, "logging action")
}
return nil
}

View file

@ -1,56 +0,0 @@
package core
import (
"encoding/json"
"testing"
"github.com/dnote/actions"
"github.com/dnote/cli/testutils"
"github.com/pkg/errors"
)
func TestLogActionEditNote(t *testing.T) {
// Setup
ctx := testutils.InitEnv("../tmp", "../testutils/fixtures/schema.sql")
defer testutils.TeardownEnv(ctx)
// Execute
db := ctx.DB
tx, err := db.Begin()
if err != nil {
panic(errors.Wrap(err, "beginning a transaction"))
}
if err := LogActionEditNote(tx, "f0d0fbb7-31ff-45ae-9f0f-4e429c0c797f", "js", "updated content", 1536168581); err != nil {
t.Fatalf("Failed to perform %s", err.Error())
}
tx.Commit()
// Test
var actionCount int
if err := db.QueryRow("SELECT count(*) FROM actions;").Scan(&actionCount); err != nil {
panic(errors.Wrap(err, "counting actions"))
}
var action actions.Action
if err := db.QueryRow("SELECT uuid, schema, type, timestamp, data FROM actions").
Scan(&action.UUID, &action.Schema, &action.Type, &action.Timestamp, &action.Data); err != nil {
panic(errors.Wrap(err, "querying action"))
}
var actionData actions.EditNoteDataV3
if err := json.Unmarshal(action.Data, &actionData); err != nil {
panic(errors.Wrap(err, "unmarshalling action data"))
}
if actionCount != 1 {
t.Fatalf("action count mismatch. got %d", actionCount)
}
testutils.AssertNotEqual(t, action.UUID, "", "action uuid mismatch")
testutils.AssertEqual(t, action.Schema, 3, "action schema mismatch")
testutils.AssertEqual(t, action.Type, actions.ActionEditNote, "action type mismatch")
testutils.AssertNotEqual(t, action.Timestamp, 0, "action timestamp mismatch")
testutils.AssertEqual(t, actionData.NoteUUID, "f0d0fbb7-31ff-45ae-9f0f-4e429c0c797f", "action data note_uuid mismatch")
testutils.AssertEqual(t, *actionData.Content, "updated content", "action data content mismatch")
testutils.AssertEqual(t, actionData.BookName, (*string)(nil), "action data book_name mismatch")
testutils.AssertEqual(t, actionData.Public, (*bool)(nil), "action data public mismatch")
}

View file

@ -6,7 +6,9 @@ import (
"io/ioutil"
"os"
"os/exec"
"strconv"
"strings"
"time"
"github.com/dnote/cli/infra"
"github.com/dnote/cli/utils"
@ -260,3 +262,47 @@ func GetEditorInput(ctx infra.DnoteCtx, fpath string, content *string) error {
return nil
}
func initSystemKV(tx *sql.Tx, key string, val string) error {
var count int
if err := tx.QueryRow("SELECT count(*) FROM system WHERE key = ?", key).Scan(&count); err != nil {
return errors.Wrapf(err, "counting %s", key)
}
if count > 0 {
return nil
}
if _, err := tx.Exec("INSERT INTO system (key, value) VALUES (?, ?)", key, val); err != nil {
tx.Rollback()
return errors.Wrapf(err, "inserting %s %s", key, val)
}
return nil
}
// InitSystem inserts system data if missing
func InitSystem(ctx infra.DnoteCtx) error {
db := ctx.DB
tx, err := db.Begin()
if err != nil {
return errors.Wrap(err, "beginning a transaction")
}
nowStr := strconv.FormatInt(time.Now().Unix(), 10)
if err := initSystemKV(tx, infra.SystemLastUpgrade, nowStr); err != nil {
return errors.Wrapf(err, "initializing system config for %s", infra.SystemLastUpgrade)
}
if err := initSystemKV(tx, infra.SystemLastMaxUSN, "0"); err != nil {
return errors.Wrapf(err, "initializing system config for %s", infra.SystemLastMaxUSN)
}
if err := initSystemKV(tx, infra.SystemLastSyncAt, "0"); err != nil {
return errors.Wrapf(err, "initializing system config for %s", infra.SystemLastSyncAt)
}
tx.Commit()
return nil
}

77
core/core_test.go Normal file
View file

@ -0,0 +1,77 @@
package core
import (
"testing"
"github.com/dnote/cli/testutils"
"github.com/pkg/errors"
)
func TestInitSystemKV(t *testing.T) {
// Setup
ctx := testutils.InitEnv(t, "../tmp", "../testutils/fixtures/schema.sql", true)
defer testutils.TeardownEnv(ctx)
db := ctx.DB
var originalCount int
testutils.MustScan(t, "counting system configs", db.QueryRow("SELECT count(*) FROM system"), &originalCount)
// Execute
tx, err := db.Begin()
if err != nil {
t.Fatal(errors.Wrap(err, "beginning a transaction"))
}
if err := initSystemKV(tx, "testKey", "testVal"); err != nil {
tx.Rollback()
t.Fatal(errors.Wrap(err, "executing"))
}
tx.Commit()
// Test
var count int
testutils.MustScan(t, "counting system configs", db.QueryRow("SELECT count(*) FROM system"), &count)
testutils.AssertEqual(t, count, originalCount+1, "system count mismatch")
var val string
testutils.MustScan(t, "getting system value",
db.QueryRow("SELECT value FROM system WHERE key = ?", "testKey"), &val)
testutils.AssertEqual(t, val, "testVal", "system value mismatch")
}
func TestInitSystemKV_existing(t *testing.T) {
// Setup
ctx := testutils.InitEnv(t, "../tmp", "../testutils/fixtures/schema.sql", true)
defer testutils.TeardownEnv(ctx)
db := ctx.DB
testutils.MustExec(t, "inserting a system config", db, "INSERT INTO system (key, value) VALUES (?, ?)", "testKey", "testVal")
var originalCount int
testutils.MustScan(t, "counting system configs", db.QueryRow("SELECT count(*) FROM system"), &originalCount)
// Execute
tx, err := db.Begin()
if err != nil {
t.Fatal(errors.Wrap(err, "beginning a transaction"))
}
if err := initSystemKV(tx, "testKey", "newTestVal"); err != nil {
tx.Rollback()
t.Fatal(errors.Wrap(err, "executing"))
}
tx.Commit()
// Test
var count int
testutils.MustScan(t, "counting system configs", db.QueryRow("SELECT count(*) FROM system"), &count)
testutils.AssertEqual(t, count, originalCount, "system count mismatch")
var val string
testutils.MustScan(t, "getting system value",
db.QueryRow("SELECT value FROM system WHERE key = ?", "testKey"), &val)
testutils.AssertEqual(t, val, "testVal", "system value should not have been updated")
}

150
core/models.go Normal file
View file

@ -0,0 +1,150 @@
package core
import (
"database/sql"
"github.com/pkg/errors"
)
// Book holds a metadata and its notes
type Book struct {
UUID string `json:"uuid"`
Label string `json:"label"`
USN int `json:"usn"`
Notes []Note `json:"notes"`
Deleted bool `json:"deleted"`
Dirty bool `json:"dirty"`
}
// Note represents a note
type Note struct {
UUID string `json:"uuid"`
BookUUID string `json:"book_uuid"`
Content string `json:"content"`
AddedOn int64 `json:"added_on"`
EditedOn int64 `json:"edited_on"`
USN int `json:"usn"`
Public bool `json:"public"`
Deleted bool `json:"deleted"`
Dirty bool `json:"dirty"`
}
// NewNote constructs a note with the given data
func NewNote(uuid, bookUUID, content string, addedOn, editedOn int64, usn int, public, deleted, dirty bool) Note {
return Note{
UUID: uuid,
BookUUID: bookUUID,
Content: content,
AddedOn: addedOn,
EditedOn: editedOn,
USN: usn,
Public: public,
Deleted: deleted,
Dirty: dirty,
}
}
// Insert inserts a new note
func (n Note) Insert(tx *sql.Tx) error {
_, err := tx.Exec("INSERT INTO notes (uuid, book_uuid, content, added_on, edited_on, usn, public, deleted, dirty) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)",
n.UUID, n.BookUUID, n.Content, n.AddedOn, n.EditedOn, n.USN, n.Public, n.Deleted, n.Dirty)
if err != nil {
return errors.Wrapf(err, "inserting note with uuid %s", n.UUID)
}
return nil
}
// Update updates the note with the given data
func (n Note) Update(tx *sql.Tx) error {
_, err := tx.Exec("UPDATE notes SET book_uuid = ?, content = ?, added_on = ?, edited_on = ?, usn = ?, public = ?, deleted = ?, dirty = ? WHERE uuid = ?",
n.BookUUID, n.Content, n.AddedOn, n.EditedOn, n.USN, n.Public, n.Deleted, n.Dirty, n.UUID)
if err != nil {
return errors.Wrapf(err, "updating the note with uuid %s", n.UUID)
}
return nil
}
// UpdateUUID updates the uuid of a book
func (n *Note) UpdateUUID(tx *sql.Tx, newUUID string) error {
_, err := tx.Exec("UPDATE notes SET uuid = ? WHERE uuid = ?", newUUID, n.UUID)
if err != nil {
return errors.Wrapf(err, "updating note uuid from '%s' to '%s'", n.UUID, newUUID)
}
n.UUID = newUUID
return nil
}
// Expunge hard-deletes the note from the database
func (n Note) Expunge(tx *sql.Tx) error {
_, err := tx.Exec("DELETE FROM notes WHERE uuid = ?", n.UUID)
if err != nil {
return errors.Wrap(err, "expunging a note locally")
}
return nil
}
// NewBook constructs a book with the given data
func NewBook(uuid, label string, usn int, deleted, dirty bool) Book {
return Book{
UUID: uuid,
Label: label,
USN: usn,
Deleted: deleted,
Dirty: dirty,
}
}
// Insert inserts a new book
func (b Book) Insert(tx *sql.Tx) error {
_, err := tx.Exec("INSERT INTO books (uuid, label, usn, dirty, deleted) VALUES (?, ?, ?, ?, ?)",
b.UUID, b.Label, b.USN, b.Dirty, b.Deleted)
if err != nil {
return errors.Wrapf(err, "inserting book with uuid %s", b.UUID)
}
return nil
}
// Update updates the book with the given data
func (b Book) Update(tx *sql.Tx) error {
_, err := tx.Exec("UPDATE books SET label = ?, usn = ?, dirty = ?, deleted = ? WHERE uuid = ?",
b.Label, b.USN, b.Dirty, b.Deleted, b.UUID)
if err != nil {
return errors.Wrapf(err, "updating the book with uuid %s", b.UUID)
}
return nil
}
// UpdateUUID updates the uuid of a book
func (b *Book) UpdateUUID(tx *sql.Tx, newUUID string) error {
_, err := tx.Exec("UPDATE books SET uuid = ? WHERE uuid = ?", newUUID, b.UUID)
if err != nil {
return errors.Wrapf(err, "updating book uuid from '%s' to '%s'", b.UUID, newUUID)
}
b.UUID = newUUID
return nil
}
// Expunge hard-deletes the book from the database
func (b Book) Expunge(tx *sql.Tx) error {
_, err := tx.Exec("DELETE FROM books WHERE uuid = ?", b.UUID)
if err != nil {
return errors.Wrap(err, "expunging a book locally")
}
return nil
}

795
core/models_test.go Normal file
View file

@ -0,0 +1,795 @@
package core
import (
"fmt"
"testing"
"github.com/dnote/cli/testutils"
"github.com/pkg/errors"
)
func TestNewNote(t *testing.T) {
testCases := []struct {
uuid string
bookUUID string
content string
addedOn int64
editedOn int64
usn int
public bool
deleted bool
dirty bool
}{
{
uuid: "n1-uuid",
bookUUID: "b1-uuid",
content: "n1-content",
addedOn: 1542058875,
editedOn: 0,
usn: 0,
public: false,
deleted: false,
dirty: false,
},
{
uuid: "n2-uuid",
bookUUID: "b2-uuid",
content: "n2-content",
addedOn: 1542058875,
editedOn: 1542058876,
usn: 1008,
public: true,
deleted: true,
dirty: true,
},
}
for idx, tc := range testCases {
got := NewNote(tc.uuid, tc.bookUUID, tc.content, tc.addedOn, tc.editedOn, tc.usn, tc.public, tc.deleted, tc.dirty)
testutils.AssertEqual(t, got.UUID, tc.uuid, fmt.Sprintf("UUID mismatch for test case %d", idx))
testutils.AssertEqual(t, got.BookUUID, tc.bookUUID, fmt.Sprintf("BookUUID mismatch for test case %d", idx))
testutils.AssertEqual(t, got.Content, tc.content, fmt.Sprintf("Content mismatch for test case %d", idx))
testutils.AssertEqual(t, got.AddedOn, tc.addedOn, fmt.Sprintf("AddedOn mismatch for test case %d", idx))
testutils.AssertEqual(t, got.EditedOn, tc.editedOn, fmt.Sprintf("EditedOn mismatch for test case %d", idx))
testutils.AssertEqual(t, got.USN, tc.usn, fmt.Sprintf("USN mismatch for test case %d", idx))
testutils.AssertEqual(t, got.Public, tc.public, fmt.Sprintf("Public mismatch for test case %d", idx))
testutils.AssertEqual(t, got.Deleted, tc.deleted, fmt.Sprintf("Deleted mismatch for test case %d", idx))
testutils.AssertEqual(t, got.Dirty, tc.dirty, fmt.Sprintf("Dirty mismatch for test case %d", idx))
}
}
func TestNoteInsert(t *testing.T) {
testCases := []struct {
uuid string
bookUUID string
content string
addedOn int64
editedOn int64
usn int
public bool
deleted bool
dirty bool
}{
{
uuid: "n1-uuid",
bookUUID: "b1-uuid",
content: "n1-content",
addedOn: 1542058875,
editedOn: 0,
usn: 0,
public: false,
deleted: false,
dirty: false,
},
{
uuid: "n2-uuid",
bookUUID: "b2-uuid",
content: "n2-content",
addedOn: 1542058875,
editedOn: 1542058876,
usn: 1008,
public: true,
deleted: true,
dirty: true,
},
}
for idx, tc := range testCases {
func() {
// Setup
ctx := testutils.InitEnv(t, "../tmp", "../testutils/fixtures/schema.sql", true)
defer testutils.TeardownEnv(ctx)
n := Note{
UUID: tc.uuid,
BookUUID: tc.bookUUID,
Content: tc.content,
AddedOn: tc.addedOn,
EditedOn: tc.editedOn,
USN: tc.usn,
Public: tc.public,
Deleted: tc.deleted,
Dirty: tc.dirty,
}
// execute
db := ctx.DB
tx, err := db.Begin()
if err != nil {
t.Fatalf(errors.Wrap(err, fmt.Sprintf("beginning a transaction for test case %d", idx)).Error())
}
if err := n.Insert(tx); err != nil {
tx.Rollback()
t.Fatalf(errors.Wrap(err, fmt.Sprintf("executing for test case %d", idx)).Error())
}
tx.Commit()
// test
var uuid, bookUUID, content string
var addedOn, editedOn int64
var usn int
var public, deleted, dirty bool
testutils.MustScan(t, "getting n1",
db.QueryRow("SELECT uuid, book_uuid, content, added_on, edited_on, usn, public, deleted, dirty FROM notes WHERE uuid = ?", tc.uuid),
&uuid, &bookUUID, &content, &addedOn, &editedOn, &usn, &public, &deleted, &dirty)
testutils.AssertEqual(t, uuid, tc.uuid, fmt.Sprintf("uuid mismatch for test case %d", idx))
testutils.AssertEqual(t, bookUUID, tc.bookUUID, fmt.Sprintf("bookUUID mismatch for test case %d", idx))
testutils.AssertEqual(t, content, tc.content, fmt.Sprintf("content mismatch for test case %d", idx))
testutils.AssertEqual(t, addedOn, tc.addedOn, fmt.Sprintf("addedOn mismatch for test case %d", idx))
testutils.AssertEqual(t, editedOn, tc.editedOn, fmt.Sprintf("editedOn mismatch for test case %d", idx))
testutils.AssertEqual(t, usn, tc.usn, fmt.Sprintf("usn mismatch for test case %d", idx))
testutils.AssertEqual(t, public, tc.public, fmt.Sprintf("public mismatch for test case %d", idx))
testutils.AssertEqual(t, deleted, tc.deleted, fmt.Sprintf("deleted mismatch for test case %d", idx))
testutils.AssertEqual(t, dirty, tc.dirty, fmt.Sprintf("dirty mismatch for test case %d", idx))
}()
}
}
func TestNoteUpdate(t *testing.T) {
testCases := []struct {
uuid string
bookUUID string
content string
addedOn int64
editedOn int64
usn int
public bool
deleted bool
dirty bool
newBookUUID string
newContent string
newEditedOn int64
newUSN int
newPublic bool
newDeleted bool
newDirty bool
}{
{
uuid: "n1-uuid",
bookUUID: "b1-uuid",
content: "n1-content",
addedOn: 1542058875,
editedOn: 0,
usn: 0,
public: false,
deleted: false,
dirty: false,
newBookUUID: "b1-uuid",
newContent: "n1-content edited",
newEditedOn: 1542058879,
newUSN: 0,
newPublic: false,
newDeleted: false,
newDirty: false,
},
{
uuid: "n1-uuid",
bookUUID: "b1-uuid",
content: "n1-content",
addedOn: 1542058875,
editedOn: 0,
usn: 0,
public: false,
deleted: false,
dirty: true,
newBookUUID: "b2-uuid",
newContent: "n1-content",
newEditedOn: 1542058879,
newUSN: 0,
newPublic: true,
newDeleted: false,
newDirty: false,
},
{
uuid: "n1-uuid",
bookUUID: "b1-uuid",
content: "n1-content",
addedOn: 1542058875,
editedOn: 0,
usn: 10,
public: false,
deleted: false,
dirty: false,
newBookUUID: "",
newContent: "",
newEditedOn: 1542058879,
newUSN: 151,
newPublic: false,
newDeleted: true,
newDirty: false,
},
{
uuid: "n1-uuid",
bookUUID: "b1-uuid",
content: "n1-content",
addedOn: 1542058875,
editedOn: 0,
usn: 0,
public: false,
deleted: false,
dirty: false,
newBookUUID: "",
newContent: "",
newEditedOn: 1542058879,
newUSN: 15,
newPublic: false,
newDeleted: true,
newDirty: false,
},
}
for idx, tc := range testCases {
func() {
// Setup
ctx := testutils.InitEnv(t, "../tmp", "../testutils/fixtures/schema.sql", true)
defer testutils.TeardownEnv(ctx)
n1 := Note{
UUID: tc.uuid,
BookUUID: tc.bookUUID,
Content: tc.content,
AddedOn: tc.addedOn,
EditedOn: tc.editedOn,
USN: tc.usn,
Public: tc.public,
Deleted: tc.deleted,
Dirty: tc.dirty,
}
n2 := Note{
UUID: "n2-uuid",
BookUUID: "b10-uuid",
Content: "n2 content",
AddedOn: 1542058875,
EditedOn: 0,
USN: 39,
Public: false,
Deleted: false,
Dirty: false,
}
db := ctx.DB
testutils.MustExec(t, fmt.Sprintf("inserting n1 for test case %d", idx), db, "INSERT INTO notes (uuid, book_uuid, usn, added_on, edited_on, content, public, deleted, dirty) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)", n1.UUID, n1.BookUUID, n1.USN, n1.AddedOn, n1.EditedOn, n1.Content, n1.Public, n1.Deleted, n1.Dirty)
testutils.MustExec(t, fmt.Sprintf("inserting n2 for test case %d", idx), db, "INSERT INTO notes (uuid, book_uuid, usn, added_on, edited_on, content, public, deleted, dirty) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)", n2.UUID, n2.BookUUID, n2.USN, n2.AddedOn, n2.EditedOn, n2.Content, n2.Public, n2.Deleted, n2.Dirty)
// execute
tx, err := db.Begin()
if err != nil {
t.Fatalf(errors.Wrap(err, fmt.Sprintf("beginning a transaction for test case %d", idx)).Error())
}
n1.BookUUID = tc.newBookUUID
n1.Content = tc.newContent
n1.EditedOn = tc.newEditedOn
n1.USN = tc.newUSN
n1.Public = tc.newPublic
n1.Deleted = tc.newDeleted
n1.Dirty = tc.newDirty
if err := n1.Update(tx); err != nil {
tx.Rollback()
t.Fatalf(errors.Wrap(err, fmt.Sprintf("executing for test case %d", idx)).Error())
}
tx.Commit()
// test
var n1Record, n2Record Note
testutils.MustScan(t, "getting n1",
db.QueryRow("SELECT uuid, book_uuid, content, added_on, edited_on, usn, public, deleted, dirty FROM notes WHERE uuid = ?", tc.uuid),
&n1Record.UUID, &n1Record.BookUUID, &n1Record.Content, &n1Record.AddedOn, &n1Record.EditedOn, &n1Record.USN, &n1Record.Public, &n1Record.Deleted, &n1Record.Dirty)
testutils.MustScan(t, "getting n2",
db.QueryRow("SELECT uuid, book_uuid, content, added_on, edited_on, usn, public, deleted, dirty FROM notes WHERE uuid = ?", n2.UUID),
&n2Record.UUID, &n2Record.BookUUID, &n2Record.Content, &n2Record.AddedOn, &n2Record.EditedOn, &n2Record.USN, &n2Record.Public, &n2Record.Deleted, &n2Record.Dirty)
testutils.AssertEqual(t, n1Record.UUID, n1.UUID, fmt.Sprintf("n1 uuid mismatch for test case %d", idx))
testutils.AssertEqual(t, n1Record.BookUUID, tc.newBookUUID, fmt.Sprintf("n1 bookUUID mismatch for test case %d", idx))
testutils.AssertEqual(t, n1Record.Content, tc.newContent, fmt.Sprintf("n1 content mismatch for test case %d", idx))
testutils.AssertEqual(t, n1Record.AddedOn, n1.AddedOn, fmt.Sprintf("n1 addedOn mismatch for test case %d", idx))
testutils.AssertEqual(t, n1Record.EditedOn, tc.newEditedOn, fmt.Sprintf("n1 editedOn mismatch for test case %d", idx))
testutils.AssertEqual(t, n1Record.USN, tc.newUSN, fmt.Sprintf("n1 usn mismatch for test case %d", idx))
testutils.AssertEqual(t, n1Record.Public, tc.newPublic, fmt.Sprintf("n1 public mismatch for test case %d", idx))
testutils.AssertEqual(t, n1Record.Deleted, tc.newDeleted, fmt.Sprintf("n1 deleted mismatch for test case %d", idx))
testutils.AssertEqual(t, n1Record.Dirty, tc.newDirty, fmt.Sprintf("n1 dirty mismatch for test case %d", idx))
testutils.AssertEqual(t, n2Record.UUID, n2.UUID, fmt.Sprintf("n2 uuid mismatch for test case %d", idx))
testutils.AssertEqual(t, n2Record.BookUUID, n2.BookUUID, fmt.Sprintf("n2 bookUUID mismatch for test case %d", idx))
testutils.AssertEqual(t, n2Record.Content, n2.Content, fmt.Sprintf("n2 content mismatch for test case %d", idx))
testutils.AssertEqual(t, n2Record.AddedOn, n2.AddedOn, fmt.Sprintf("n2 addedOn mismatch for test case %d", idx))
testutils.AssertEqual(t, n2Record.EditedOn, n2.EditedOn, fmt.Sprintf("n2 editedOn mismatch for test case %d", idx))
testutils.AssertEqual(t, n2Record.USN, n2.USN, fmt.Sprintf("n2 usn mismatch for test case %d", idx))
testutils.AssertEqual(t, n2Record.Public, n2.Public, fmt.Sprintf("n2 public mismatch for test case %d", idx))
testutils.AssertEqual(t, n2Record.Deleted, n2.Deleted, fmt.Sprintf("n2 deleted mismatch for test case %d", idx))
testutils.AssertEqual(t, n2Record.Dirty, n2.Dirty, fmt.Sprintf("n2 dirty mismatch for test case %d", idx))
}()
}
}
func TestNoteUpdateUUID(t *testing.T) {
testCases := []struct {
newUUID string
}{
{
newUUID: "n1-new-uuid",
},
{
newUUID: "n2-new-uuid",
},
}
for idx, tc := range testCases {
t.Run(fmt.Sprintf("testCase%d", idx), func(t *testing.T) {
// Setup
ctx := testutils.InitEnv(t, "../tmp", "../testutils/fixtures/schema.sql", true)
defer testutils.TeardownEnv(ctx)
n1 := Note{
UUID: "n1-uuid",
BookUUID: "b1-uuid",
AddedOn: 1542058874,
Content: "n1-content",
USN: 1,
Deleted: true,
Dirty: false,
}
n2 := Note{
UUID: "n2-uuid",
BookUUID: "b1-uuid",
AddedOn: 1542058874,
Content: "n2-content",
USN: 1,
Deleted: true,
Dirty: false,
}
db := ctx.DB
testutils.MustExec(t, "inserting n1", db, "INSERT INTO notes (uuid, book_uuid, content, added_on, usn, deleted, dirty) VALUES (?, ?, ?, ?, ?, ?, ?)", n1.UUID, n1.BookUUID, n1.Content, n1.AddedOn, n1.USN, n1.Deleted, n1.Dirty)
testutils.MustExec(t, "inserting n2", db, "INSERT INTO notes (uuid, book_uuid, content, added_on, usn, deleted, dirty) VALUES (?, ?, ?, ?, ?, ?, ?)", n2.UUID, n2.BookUUID, n2.Content, n2.AddedOn, n2.USN, n2.Deleted, n2.Dirty)
// execute
tx, err := db.Begin()
if err != nil {
t.Fatalf(errors.Wrap(err, "beginning a transaction").Error())
}
if err := n1.UpdateUUID(tx, tc.newUUID); err != nil {
tx.Rollback()
t.Fatalf(errors.Wrap(err, "executing").Error())
}
tx.Commit()
// test
var n1Record, n2Record Note
testutils.MustScan(t, "getting n1",
db.QueryRow("SELECT uuid, content, usn, deleted, dirty FROM notes WHERE content = ?", "n1-content"),
&n1Record.UUID, &n1Record.Content, &n1Record.USN, &n1Record.Deleted, &n1Record.Dirty)
testutils.MustScan(t, "getting n2",
db.QueryRow("SELECT uuid, content, usn, deleted, dirty FROM notes WHERE content = ?", "n2-content"),
&n2Record.UUID, &n2Record.Content, &n2Record.USN, &n2Record.Deleted, &n2Record.Dirty)
testutils.AssertEqual(t, n1.UUID, tc.newUUID, "n1 original reference uuid mismatch")
testutils.AssertEqual(t, n1Record.UUID, tc.newUUID, "n1 uuid mismatch")
testutils.AssertEqual(t, n2Record.UUID, n2.UUID, "n2 uuid mismatch")
})
}
}
func TestNoteExpunge(t *testing.T) {
// Setup
ctx := testutils.InitEnv(t, "../tmp", "../testutils/fixtures/schema.sql", true)
defer testutils.TeardownEnv(ctx)
n1 := Note{
UUID: "n1-uuid",
BookUUID: "b9-uuid",
Content: "n1 content",
AddedOn: 1542058874,
EditedOn: 0,
USN: 22,
Public: false,
Deleted: false,
Dirty: false,
}
n2 := Note{
UUID: "n2-uuid",
BookUUID: "b10-uuid",
Content: "n2 content",
AddedOn: 1542058875,
EditedOn: 0,
USN: 39,
Public: false,
Deleted: false,
Dirty: false,
}
db := ctx.DB
testutils.MustExec(t, "inserting n1", db, "INSERT INTO notes (uuid, book_uuid, usn, added_on, edited_on, content, public, deleted, dirty) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)", n1.UUID, n1.BookUUID, n1.USN, n1.AddedOn, n1.EditedOn, n1.Content, n1.Public, n1.Deleted, n1.Dirty)
testutils.MustExec(t, "inserting n2", db, "INSERT INTO notes (uuid, book_uuid, usn, added_on, edited_on, content, public, deleted, dirty) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)", n2.UUID, n2.BookUUID, n2.USN, n2.AddedOn, n2.EditedOn, n2.Content, n2.Public, n2.Deleted, n2.Dirty)
// execute
tx, err := db.Begin()
if err != nil {
t.Fatalf(errors.Wrap(err, "beginning a transaction").Error())
}
if err := n1.Expunge(tx); err != nil {
tx.Rollback()
t.Fatalf(errors.Wrap(err, "executing").Error())
}
tx.Commit()
// test
var noteCount int
testutils.MustScan(t, "counting notes", db.QueryRow("SELECT count(*) FROM notes"), &noteCount)
testutils.AssertEqualf(t, noteCount, 1, "note count mismatch")
var n2Record Note
testutils.MustScan(t, "getting n2",
db.QueryRow("SELECT uuid, book_uuid, content, added_on, edited_on, usn, public, deleted, dirty FROM notes WHERE uuid = ?", n2.UUID),
&n2Record.UUID, &n2Record.BookUUID, &n2Record.Content, &n2Record.AddedOn, &n2Record.EditedOn, &n2Record.USN, &n2Record.Public, &n2Record.Deleted, &n2Record.Dirty)
testutils.AssertEqual(t, n2Record.UUID, n2.UUID, "n2 uuid mismatch")
testutils.AssertEqual(t, n2Record.BookUUID, n2.BookUUID, "n2 bookUUID mismatch")
testutils.AssertEqual(t, n2Record.Content, n2.Content, "n2 content mismatch")
testutils.AssertEqual(t, n2Record.AddedOn, n2.AddedOn, "n2 addedOn mismatch")
testutils.AssertEqual(t, n2Record.EditedOn, n2.EditedOn, "n2 editedOn mismatch")
testutils.AssertEqual(t, n2Record.USN, n2.USN, "n2 usn mismatch")
testutils.AssertEqual(t, n2Record.Public, n2.Public, "n2 public mismatch")
testutils.AssertEqual(t, n2Record.Deleted, n2.Deleted, "n2 deleted mismatch")
testutils.AssertEqual(t, n2Record.Dirty, n2.Dirty, "n2 dirty mismatch")
}
func TestNewBook(t *testing.T) {
testCases := []struct {
uuid string
label string
usn int
deleted bool
dirty bool
}{
{
uuid: "b1-uuid",
label: "b1-label",
usn: 0,
deleted: false,
dirty: false,
},
{
uuid: "b2-uuid",
label: "b2-label",
usn: 1008,
deleted: false,
dirty: true,
},
}
for idx, tc := range testCases {
got := NewBook(tc.uuid, tc.label, tc.usn, tc.deleted, tc.dirty)
testutils.AssertEqual(t, got.UUID, tc.uuid, fmt.Sprintf("UUID mismatch for test case %d", idx))
testutils.AssertEqual(t, got.Label, tc.label, fmt.Sprintf("Label mismatch for test case %d", idx))
testutils.AssertEqual(t, got.USN, tc.usn, fmt.Sprintf("USN mismatch for test case %d", idx))
testutils.AssertEqual(t, got.Deleted, tc.deleted, fmt.Sprintf("Deleted mismatch for test case %d", idx))
testutils.AssertEqual(t, got.Dirty, tc.dirty, fmt.Sprintf("Dirty mismatch for test case %d", idx))
}
}
func TestBookInsert(t *testing.T) {
testCases := []struct {
uuid string
label string
usn int
deleted bool
dirty bool
}{
{
uuid: "b1-uuid",
label: "b1-label",
usn: 10808,
deleted: false,
dirty: false,
},
{
uuid: "b1-uuid",
label: "b1-label",
usn: 10808,
deleted: false,
dirty: true,
},
}
for idx, tc := range testCases {
func() {
// Setup
ctx := testutils.InitEnv(t, "../tmp", "../testutils/fixtures/schema.sql", true)
defer testutils.TeardownEnv(ctx)
b := Book{
UUID: tc.uuid,
Label: tc.label,
USN: tc.usn,
Dirty: tc.dirty,
Deleted: tc.deleted,
}
// execute
db := ctx.DB
tx, err := db.Begin()
if err != nil {
t.Fatalf(errors.Wrap(err, fmt.Sprintf("beginning a transaction for test case %d", idx)).Error())
}
if err := b.Insert(tx); err != nil {
tx.Rollback()
t.Fatalf(errors.Wrap(err, fmt.Sprintf("executing for test case %d", idx)).Error())
}
tx.Commit()
// test
var uuid, label string
var usn int
var deleted, dirty bool
testutils.MustScan(t, "getting b1",
db.QueryRow("SELECT uuid, label, usn, deleted, dirty FROM books WHERE uuid = ?", tc.uuid),
&uuid, &label, &usn, &deleted, &dirty)
testutils.AssertEqual(t, uuid, tc.uuid, fmt.Sprintf("uuid mismatch for test case %d", idx))
testutils.AssertEqual(t, label, tc.label, fmt.Sprintf("label mismatch for test case %d", idx))
testutils.AssertEqual(t, usn, tc.usn, fmt.Sprintf("usn mismatch for test case %d", idx))
testutils.AssertEqual(t, deleted, tc.deleted, fmt.Sprintf("deleted mismatch for test case %d", idx))
testutils.AssertEqual(t, dirty, tc.dirty, fmt.Sprintf("dirty mismatch for test case %d", idx))
}()
}
}
func TestBookUpdate(t *testing.T) {
testCases := []struct {
uuid string
label string
usn int
deleted bool
dirty bool
newLabel string
newUSN int
newDeleted bool
newDirty bool
}{
{
uuid: "b1-uuid",
label: "b1-label",
usn: 0,
deleted: false,
dirty: false,
newLabel: "b1-label-edited",
newUSN: 0,
newDeleted: false,
newDirty: true,
},
{
uuid: "b1-uuid",
label: "b1-label",
usn: 0,
deleted: false,
dirty: false,
newLabel: "",
newUSN: 10,
newDeleted: true,
newDirty: false,
},
}
for idx, tc := range testCases {
func() {
// Setup
ctx := testutils.InitEnv(t, "../tmp", "../testutils/fixtures/schema.sql", true)
defer testutils.TeardownEnv(ctx)
b1 := Book{
UUID: "b1-uuid",
Label: "b1-label",
USN: 1,
Deleted: true,
Dirty: false,
}
b2 := Book{
UUID: "b2-uuid",
Label: "b2-label",
USN: 1,
Deleted: true,
Dirty: false,
}
db := ctx.DB
testutils.MustExec(t, fmt.Sprintf("inserting b1 for test case %d", idx), db, "INSERT INTO books (uuid, label, usn, deleted, dirty) VALUES (?, ?, ?, ?, ?)", b1.UUID, b1.Label, b1.USN, b1.Deleted, b1.Dirty)
testutils.MustExec(t, fmt.Sprintf("inserting b2 for test case %d", idx), db, "INSERT INTO books (uuid, label, usn, deleted, dirty) VALUES (?, ?, ?, ?, ?)", b2.UUID, b2.Label, b2.USN, b2.Deleted, b2.Dirty)
// execute
tx, err := db.Begin()
if err != nil {
t.Fatalf(errors.Wrap(err, fmt.Sprintf("beginning a transaction for test case %d", idx)).Error())
}
b1.Label = tc.newLabel
b1.USN = tc.newUSN
b1.Deleted = tc.newDeleted
b1.Dirty = tc.newDirty
if err := b1.Update(tx); err != nil {
tx.Rollback()
t.Fatalf(errors.Wrap(err, fmt.Sprintf("executing for test case %d", idx)).Error())
}
tx.Commit()
// test
var b1Record, b2Record Book
testutils.MustScan(t, "getting b1",
db.QueryRow("SELECT uuid, label, usn, deleted, dirty FROM books WHERE uuid = ?", tc.uuid),
&b1Record.UUID, &b1Record.Label, &b1Record.USN, &b1Record.Deleted, &b1Record.Dirty)
testutils.MustScan(t, "getting b2",
db.QueryRow("SELECT uuid, label, usn, deleted, dirty FROM books WHERE uuid = ?", b2.UUID),
&b2Record.UUID, &b2Record.Label, &b2Record.USN, &b2Record.Deleted, &b2Record.Dirty)
testutils.AssertEqual(t, b1Record.UUID, b1.UUID, fmt.Sprintf("b1 uuid mismatch for test case %d", idx))
testutils.AssertEqual(t, b1Record.Label, tc.newLabel, fmt.Sprintf("b1 label mismatch for test case %d", idx))
testutils.AssertEqual(t, b1Record.USN, tc.newUSN, fmt.Sprintf("b1 usn mismatch for test case %d", idx))
testutils.AssertEqual(t, b1Record.Deleted, tc.newDeleted, fmt.Sprintf("b1 deleted mismatch for test case %d", idx))
testutils.AssertEqual(t, b1Record.Dirty, tc.newDirty, fmt.Sprintf("b1 dirty mismatch for test case %d", idx))
testutils.AssertEqual(t, b2Record.UUID, b2.UUID, fmt.Sprintf("b2 uuid mismatch for test case %d", idx))
testutils.AssertEqual(t, b2Record.Label, b2.Label, fmt.Sprintf("b2 label mismatch for test case %d", idx))
testutils.AssertEqual(t, b2Record.USN, b2.USN, fmt.Sprintf("b2 usn mismatch for test case %d", idx))
testutils.AssertEqual(t, b2Record.Deleted, b2.Deleted, fmt.Sprintf("b2 deleted mismatch for test case %d", idx))
testutils.AssertEqual(t, b2Record.Dirty, b2.Dirty, fmt.Sprintf("b2 dirty mismatch for test case %d", idx))
}()
}
}
func TestBookUpdateUUID(t *testing.T) {
testCases := []struct {
newUUID string
}{
{
newUUID: "b1-new-uuid",
},
{
newUUID: "b2-new-uuid",
},
}
for idx, tc := range testCases {
t.Run(fmt.Sprintf("testCase%d", idx), func(t *testing.T) {
// Setup
ctx := testutils.InitEnv(t, "../tmp", "../testutils/fixtures/schema.sql", true)
defer testutils.TeardownEnv(ctx)
b1 := Book{
UUID: "b1-uuid",
Label: "b1-label",
USN: 1,
Deleted: true,
Dirty: false,
}
b2 := Book{
UUID: "b2-uuid",
Label: "b2-label",
USN: 1,
Deleted: true,
Dirty: false,
}
db := ctx.DB
testutils.MustExec(t, "inserting b1", db, "INSERT INTO books (uuid, label, usn, deleted, dirty) VALUES (?, ?, ?, ?, ?)", b1.UUID, b1.Label, b1.USN, b1.Deleted, b1.Dirty)
testutils.MustExec(t, "inserting b2", db, "INSERT INTO books (uuid, label, usn, deleted, dirty) VALUES (?, ?, ?, ?, ?)", b2.UUID, b2.Label, b2.USN, b2.Deleted, b2.Dirty)
// execute
tx, err := db.Begin()
if err != nil {
t.Fatalf(errors.Wrap(err, "beginning a transaction").Error())
}
if err := b1.UpdateUUID(tx, tc.newUUID); err != nil {
tx.Rollback()
t.Fatalf(errors.Wrap(err, "executing").Error())
}
tx.Commit()
// test
var b1Record, b2Record Book
testutils.MustScan(t, "getting b1",
db.QueryRow("SELECT uuid, label, usn, deleted, dirty FROM books WHERE label = ?", "b1-label"),
&b1Record.UUID, &b1Record.Label, &b1Record.USN, &b1Record.Deleted, &b1Record.Dirty)
testutils.MustScan(t, "getting b2",
db.QueryRow("SELECT uuid, label, usn, deleted, dirty FROM books WHERE label = ?", "b2-label"),
&b2Record.UUID, &b2Record.Label, &b2Record.USN, &b2Record.Deleted, &b2Record.Dirty)
testutils.AssertEqual(t, b1.UUID, tc.newUUID, "b1 original reference uuid mismatch")
testutils.AssertEqual(t, b1Record.UUID, tc.newUUID, "b1 uuid mismatch")
testutils.AssertEqual(t, b2Record.UUID, b2.UUID, "b2 uuid mismatch")
})
}
}
func TestBookExpunge(t *testing.T) {
// Setup
ctx := testutils.InitEnv(t, "../tmp", "../testutils/fixtures/schema.sql", true)
defer testutils.TeardownEnv(ctx)
b1 := Book{
UUID: "b1-uuid",
Label: "b1-label",
USN: 1,
Deleted: true,
Dirty: false,
}
b2 := Book{
UUID: "b2-uuid",
Label: "b2-label",
USN: 1,
Deleted: true,
Dirty: false,
}
db := ctx.DB
testutils.MustExec(t, "inserting b1", db, "INSERT INTO books (uuid, label, usn, deleted, dirty) VALUES (?, ?, ?, ?, ?)", b1.UUID, b1.Label, b1.USN, b1.Deleted, b1.Dirty)
testutils.MustExec(t, "inserting b2", db, "INSERT INTO books (uuid, label, usn, deleted, dirty) VALUES (?, ?, ?, ?, ?)", b2.UUID, b2.Label, b2.USN, b2.Deleted, b2.Dirty)
// execute
tx, err := db.Begin()
if err != nil {
t.Fatalf(errors.Wrap(err, "beginning a transaction").Error())
}
if err := b1.Expunge(tx); err != nil {
tx.Rollback()
t.Fatalf(errors.Wrap(err, "executing").Error())
}
tx.Commit()
// test
var bookCount int
testutils.MustScan(t, "counting books", db.QueryRow("SELECT count(*) FROM books"), &bookCount)
testutils.AssertEqualf(t, bookCount, 1, "book count mismatch")
var b2Record Book
testutils.MustScan(t, "getting b2",
db.QueryRow("SELECT uuid, label, usn, deleted, dirty FROM books WHERE uuid = ?", "b2-uuid"),
&b2Record.UUID, &b2Record.Label, &b2Record.USN, &b2Record.Deleted, &b2Record.Dirty)
testutils.AssertEqual(t, b2Record.UUID, b2.UUID, "b2 uuid mismatch")
testutils.AssertEqual(t, b2Record.Label, b2.Label, "b2 label mismatch")
testutils.AssertEqual(t, b2Record.USN, b2.USN, "b2 usn mismatch")
testutils.AssertEqual(t, b2Record.Deleted, b2.Deleted, "b2 deleted mismatch")
testutils.AssertEqual(t, b2Record.Dirty, b2.Dirty, "b2 dirty mismatch")
}

View file

@ -1,257 +0,0 @@
package core
import (
"database/sql"
"encoding/json"
"fmt"
"github.com/dnote/actions"
"github.com/dnote/cli/infra"
"github.com/dnote/cli/log"
"github.com/dnote/cli/utils"
"github.com/pkg/errors"
)
// ReduceAll reduces all actions
func ReduceAll(ctx infra.DnoteCtx, tx *sql.Tx, actionSlice []actions.Action) error {
for _, action := range actionSlice {
if err := Reduce(ctx, tx, action); err != nil {
return errors.Wrap(err, "reducing action")
}
}
return nil
}
// Reduce transitions the local dnote state by consuming the action returned
// from the server
func Reduce(ctx infra.DnoteCtx, tx *sql.Tx, action actions.Action) error {
log.Debug("reducing %s. uuid: %s, schema: %d, timestamp: %d\n", action.Type, action.UUID, action.Schema, action.Timestamp)
var err error
switch action.Type {
case actions.ActionAddNote:
err = handleAddNote(ctx, tx, action)
case actions.ActionRemoveNote:
err = handleRemoveNote(ctx, tx, action)
case actions.ActionEditNote:
err = handleEditNote(ctx, tx, action)
case actions.ActionAddBook:
err = handleAddBook(ctx, tx, action)
case actions.ActionRemoveBook:
err = handleRemoveBook(ctx, tx, action)
default:
return errors.Errorf("Unsupported action %s", action.Type)
}
if err != nil {
return errors.Wrapf(err, "reducing %s", action.Type)
}
return nil
}
func getBookUUIDWithTx(tx *sql.Tx, bookLabel string) (string, error) {
var ret string
err := tx.QueryRow("SELECT uuid FROM books WHERE label = ?", bookLabel).Scan(&ret)
if err == sql.ErrNoRows {
return ret, errors.Errorf("book '%s' not found", bookLabel)
} else if err != nil {
return ret, errors.Wrap(err, "querying the book")
}
return ret, nil
}
func handleAddNote(ctx infra.DnoteCtx, tx *sql.Tx, action actions.Action) error {
if action.Schema != 2 {
return errors.Errorf("data schema '%d' not supported", action.Schema)
}
var data actions.AddNoteDataV2
if err := json.Unmarshal(action.Data, &data); err != nil {
return errors.Wrap(err, "parsing the action data")
}
log.Debug("data: %+v\n", data)
bookUUID, err := getBookUUIDWithTx(tx, data.BookName)
if err != nil {
return errors.Wrap(err, "getting book uuid")
}
var noteCount int
if err := tx.
QueryRow("SELECT count(uuid) FROM notes WHERE uuid = ? AND book_uuid = ?", data.NoteUUID, bookUUID).
Scan(&noteCount); err != nil {
return errors.Wrap(err, "counting note")
}
if noteCount > 0 {
// if a duplicate exists, it is because the same action has been previously synced to the server
// but the client did not bring the bookmark up-to-date at the time because it had error reducing
// the returned actions.
// noop so that the client can update bookmark
return nil
}
_, err = tx.Exec(`INSERT INTO notes
(uuid, book_uuid, content, added_on, public)
VALUES (?, ?, ?, ?, ?)`, data.NoteUUID, bookUUID, data.Content, action.Timestamp, data.Public)
if err != nil {
return errors.Wrap(err, "inserting a note")
}
return nil
}
func handleRemoveNote(ctx infra.DnoteCtx, tx *sql.Tx, action actions.Action) error {
if action.Schema != 2 {
return errors.Errorf("data schema '%d' not supported", action.Schema)
}
var data actions.RemoveNoteDataV2
if err := json.Unmarshal(action.Data, &data); err != nil {
return errors.Wrap(err, "parsing the action data")
}
log.Debug("data: %+v\n", data)
_, err := tx.Exec("DELETE FROM notes WHERE uuid = ?", data.NoteUUID)
if err != nil {
return errors.Wrap(err, "removing a note")
}
return nil
}
func buildEditNoteQuery(ctx infra.DnoteCtx, tx *sql.Tx, noteUUID string, ts int64, data actions.EditNoteDataV3) (string, []interface{}, error) {
setTmpl := "edited_on = ?"
queryArgs := []interface{}{ts}
if data.Content != nil {
setTmpl = fmt.Sprintf("%s, content = ?", setTmpl)
queryArgs = append(queryArgs, *data.Content)
}
if data.Public != nil {
setTmpl = fmt.Sprintf("%s, public = ?", setTmpl)
queryArgs = append(queryArgs, *data.Public)
}
if data.BookName != nil {
setTmpl = fmt.Sprintf("%s, book_uuid = ?", setTmpl)
bookUUID, err := getBookUUIDWithTx(tx, *data.BookName)
if err != nil {
return setTmpl, queryArgs, errors.Wrap(err, "getting book uuid")
}
queryArgs = append(queryArgs, bookUUID)
}
queryTmpl := fmt.Sprintf("UPDATE notes SET %s WHERE uuid = ?", setTmpl)
queryArgs = append(queryArgs, noteUUID)
return queryTmpl, queryArgs, nil
}
func handleEditNote(ctx infra.DnoteCtx, tx *sql.Tx, action actions.Action) error {
if action.Schema != 3 {
return errors.Errorf("data schema '%d' not supported", action.Schema)
}
var data actions.EditNoteDataV3
err := json.Unmarshal(action.Data, &data)
if err != nil {
return errors.Wrap(err, "parsing the action data")
}
log.Debug("data: %+v\n", data)
queryTmpl, queryArgs, err := buildEditNoteQuery(ctx, tx, data.NoteUUID, action.Timestamp, data)
if err != nil {
return errors.Wrap(err, "building edit note query")
}
_, err = tx.Exec(queryTmpl, queryArgs...)
if err != nil {
return errors.Wrap(err, "updating a note")
}
return nil
}
func handleAddBook(ctx infra.DnoteCtx, tx *sql.Tx, action actions.Action) error {
if action.Schema != 1 {
return errors.Errorf("data schema '%d' not supported", action.Schema)
}
var data actions.AddBookDataV1
err := json.Unmarshal(action.Data, &data)
if err != nil {
return errors.Wrap(err, "parsing the action data")
}
log.Debug("data: %+v\n", data)
var bookCount int
err = tx.QueryRow("SELECT count(uuid) FROM books WHERE label = ?", data.BookName).Scan(&bookCount)
if err != nil {
return errors.Wrap(err, "counting books")
}
if bookCount > 0 {
// If book already exists, another machine added a book with the same name.
// noop
return nil
}
_, err = tx.Exec("INSERT INTO books (uuid, label) VALUES (?, ?)", utils.GenerateUUID(), data.BookName)
if err != nil {
return errors.Wrap(err, "inserting a book")
}
return nil
}
func handleRemoveBook(ctx infra.DnoteCtx, tx *sql.Tx, action actions.Action) error {
if action.Schema != 1 {
return errors.Errorf("data schema '%d' not supported", action.Schema)
}
var data actions.RemoveBookDataV1
if err := json.Unmarshal(action.Data, &data); err != nil {
return errors.Wrap(err, "parsing the action data")
}
log.Debug("data: %+v\n", data)
var bookCount int
if err := tx.
QueryRow("SELECT count(uuid) FROM books WHERE label = ?", data.BookName).
Scan(&bookCount); err != nil {
return errors.Wrap(err, "counting note")
}
if bookCount == 0 {
// If book does not exist, another client added and removed the book, making the add_book action
// obsolete. noop.
return nil
}
bookUUID, err := getBookUUIDWithTx(tx, data.BookName)
if err != nil {
return errors.Wrap(err, "getting book uuid")
}
_, err = tx.Exec("DELETE FROM notes WHERE book_uuid = ?", bookUUID)
if err != nil {
return errors.Wrap(err, "removing notes")
}
_, err = tx.Exec("DELETE FROM books WHERE uuid = ?", bookUUID)
if err != nil {
return errors.Wrap(err, "removing a book")
}
return nil
}

View file

@ -1,304 +0,0 @@
package core
import (
"encoding/json"
"testing"
"github.com/dnote/actions"
"github.com/dnote/cli/infra"
"github.com/dnote/cli/testutils"
"github.com/pkg/errors"
)
func TestReduceAddNote(t *testing.T) {
// Setup
ctx := testutils.InitEnv("../tmp", "../testutils/fixtures/schema.sql")
defer testutils.TeardownEnv(ctx)
testutils.Setup1(t, ctx)
// Execute
b, err := json.Marshal(&actions.AddNoteDataV2{
Content: "new content",
BookName: "js",
NoteUUID: "06896551-8a06-4996-89cc-0d866308b0f6",
Public: false,
})
action := actions.Action{
Type: actions.ActionAddNote,
Schema: 2,
Data: b,
Timestamp: 1517629805,
}
db := ctx.DB
tx, err := db.Begin()
if err != nil {
panic(errors.Wrap(err, "beginning a transaction"))
}
if err = Reduce(ctx, tx, action); err != nil {
tx.Rollback()
t.Fatal(errors.Wrap(err, "processing action"))
}
tx.Commit()
// Test
var noteCount, jsNoteCount, linuxNoteCount int
testutils.MustScan(t, "counting note", db.QueryRow("SELECT count(*) FROM notes"), &noteCount)
testutils.MustScan(t, "counting js note", db.QueryRow("SELECT count(*) FROM notes WHERE book_uuid = ?", "js-book-uuid"), &jsNoteCount)
testutils.MustScan(t, "counting linux note", db.QueryRow("SELECT count(*) FROM notes WHERE book_uuid = ?", "linux-book-uuid"), &linuxNoteCount)
testutils.AssertEqual(t, noteCount, 2, "notes length mismatch")
testutils.AssertEqual(t, jsNoteCount, 2, "js notes length mismatch")
testutils.AssertEqual(t, linuxNoteCount, 0, "linux notes length mismatch")
var existingNote, newNote infra.Note
testutils.MustScan(t, "scanning existing note", db.QueryRow("SELECT uuid, content FROM notes WHERE uuid = ?", "43827b9a-c2b0-4c06-a290-97991c896653"), &existingNote.UUID, &existingNote.Content)
testutils.MustScan(t, "scanning new note", db.QueryRow("SELECT uuid, content, added_on FROM notes WHERE uuid = ?", "06896551-8a06-4996-89cc-0d866308b0f6"), &newNote.UUID, &newNote.Content, &newNote.AddedOn)
testutils.AssertEqual(t, existingNote.UUID, "43827b9a-c2b0-4c06-a290-97991c896653", "existing note uuid mismatch")
testutils.AssertEqual(t, existingNote.Content, "Booleans have toString()", "existing note content mismatch")
testutils.AssertEqual(t, newNote.UUID, "06896551-8a06-4996-89cc-0d866308b0f6", "new note uuid mismatch")
testutils.AssertEqual(t, newNote.Content, "new content", "new note content mismatch")
testutils.AssertEqual(t, newNote.AddedOn, int64(1517629805), "new note added_on mismatch")
}
func TestReduceRemoveNote(t *testing.T) {
// Setup
ctx := testutils.InitEnv("../tmp", "../testutils/fixtures/schema.sql")
defer testutils.TeardownEnv(ctx)
testutils.Setup2(t, ctx)
// Execute
b, err := json.Marshal(&actions.RemoveNoteDataV2{
NoteUUID: "f0d0fbb7-31ff-45ae-9f0f-4e429c0c797f",
})
action := actions.Action{
Type: actions.ActionRemoveNote,
Schema: 2,
Data: b,
Timestamp: 1517629805,
}
db := ctx.DB
tx, err := db.Begin()
if err != nil {
panic(errors.Wrap(err, "beginning a transaction"))
}
if err = Reduce(ctx, tx, action); err != nil {
tx.Rollback()
t.Fatal(errors.Wrap(err, "processing action"))
}
tx.Commit()
// Test
var bookCount, noteCount, jsNoteCount, linuxNoteCount int
testutils.MustScan(t, "counting books", db.QueryRow("SELECT count(*) FROM books"), &bookCount)
testutils.MustScan(t, "counting note", db.QueryRow("SELECT count(*) FROM notes"), &noteCount)
testutils.MustScan(t, "counting js note", db.QueryRow("SELECT count(*) FROM notes WHERE book_uuid = ?", "js-book-uuid"), &jsNoteCount)
testutils.MustScan(t, "counting linux note", db.QueryRow("SELECT count(*) FROM notes WHERE book_uuid = ?", "linux-book-uuid"), &linuxNoteCount)
var n1, n2 infra.Note
testutils.MustScan(t, "scanning note 1", db.QueryRow("SELECT uuid, content FROM notes WHERE uuid = ?", "43827b9a-c2b0-4c06-a290-97991c896653"), &n1.UUID, &n1.Content)
testutils.MustScan(t, "scanning note 2", db.QueryRow("SELECT uuid, content FROM notes WHERE uuid = ?", "3e065d55-6d47-42f2-a6bf-f5844130b2d2"), &n2.UUID, &n2.Content)
testutils.AssertEqual(t, bookCount, 2, "number of books mismatch")
testutils.AssertEqual(t, jsNoteCount, 1, "target book notes length mismatch")
testutils.AssertEqual(t, linuxNoteCount, 1, "other book notes length mismatch")
testutils.AssertEqual(t, n1.UUID, "43827b9a-c2b0-4c06-a290-97991c896653", "remaining note uuid mismatch")
testutils.AssertEqual(t, n1.Content, "Booleans have toString()", "remaining note content mismatch")
testutils.AssertEqual(t, n2.UUID, "3e065d55-6d47-42f2-a6bf-f5844130b2d2", "other book remaining note uuid mismatch")
testutils.AssertEqual(t, n2.Content, "wc -l to count words", "other book remaining note content mismatch")
}
func TestReduceEditNote(t *testing.T) {
testCases := []struct {
data string
expectedNoteUUID string
expectedNoteBookUUID string
expectedNoteContent string
expectedNoteAddedOn int64
expectedNoteEditedOn int64
expectedNotePublic bool
expectedJsNoteCount int
expectedLinuxNoteCount int
}{
{
data: `{"note_uuid": "f0d0fbb7-31ff-45ae-9f0f-4e429c0c797f", "content": "updated content"}`,
expectedNoteUUID: "f0d0fbb7-31ff-45ae-9f0f-4e429c0c797f",
expectedNoteBookUUID: "js-book-uuid",
expectedNoteContent: "updated content",
expectedNoteAddedOn: int64(1515199951),
expectedNoteEditedOn: int64(1517629805),
expectedNotePublic: false,
expectedJsNoteCount: 2,
expectedLinuxNoteCount: 1,
},
{
data: `{"note_uuid": "f0d0fbb7-31ff-45ae-9f0f-4e429c0c797f", "public": true}`,
expectedNoteUUID: "f0d0fbb7-31ff-45ae-9f0f-4e429c0c797f",
expectedNoteBookUUID: "js-book-uuid",
expectedNoteContent: "Date object implements mathematical comparisons",
expectedNoteAddedOn: int64(1515199951),
expectedNoteEditedOn: int64(1517629805),
expectedNotePublic: true,
expectedJsNoteCount: 2,
expectedLinuxNoteCount: 1,
},
{
data: `{"note_uuid": "f0d0fbb7-31ff-45ae-9f0f-4e429c0c797f", "book_name": "linux", "content": "updated content"}`,
expectedNoteUUID: "f0d0fbb7-31ff-45ae-9f0f-4e429c0c797f",
expectedNoteBookUUID: "linux-book-uuid",
expectedNoteContent: "updated content",
expectedNoteAddedOn: int64(1515199951),
expectedNoteEditedOn: int64(1517629805),
expectedNotePublic: false,
expectedJsNoteCount: 1,
expectedLinuxNoteCount: 2,
}}
for _, tc := range testCases {
// Setup
func() {
ctx := testutils.InitEnv("../tmp", "../testutils/fixtures/schema.sql")
defer testutils.TeardownEnv(ctx)
testutils.Setup2(t, ctx)
db := ctx.DB
// Execute
action := actions.Action{
Type: actions.ActionEditNote,
Data: json.RawMessage(tc.data),
Schema: 3,
Timestamp: 1517629805,
}
tx, err := db.Begin()
if err != nil {
panic(errors.Wrap(err, "beginning a transaction"))
}
err = Reduce(ctx, tx, action)
if err != nil {
tx.Rollback()
t.Fatal(errors.Wrap(err, "Failed to process action"))
}
tx.Commit()
// Test
var bookCount, noteCount, jsNoteCount, linuxNoteCount int
testutils.MustScan(t, "counting books", db.QueryRow("SELECT count(*) FROM books"), &bookCount)
testutils.MustScan(t, "counting note", db.QueryRow("SELECT count(*) FROM notes"), &noteCount)
testutils.MustScan(t, "counting js note", db.QueryRow("SELECT count(*) FROM notes WHERE book_uuid = ?", "js-book-uuid"), &jsNoteCount)
testutils.MustScan(t, "counting linux note", db.QueryRow("SELECT count(*) FROM notes WHERE book_uuid = ?", "linux-book-uuid"), &linuxNoteCount)
var n1, n2, n3 infra.Note
testutils.MustScan(t, "scanning note 1", db.QueryRow("SELECT uuid, content FROM notes WHERE uuid = ?", "43827b9a-c2b0-4c06-a290-97991c896653"), &n1.UUID, &n1.Content)
testutils.MustScan(t, "scanning note 2", db.QueryRow("SELECT uuid, content FROM notes WHERE uuid = ?", "3e065d55-6d47-42f2-a6bf-f5844130b2d2"), &n2.UUID, &n2.Content)
testutils.MustScan(t, "scanning note 2", db.QueryRow("SELECT uuid, content, added_on, edited_on, public FROM notes WHERE uuid = ?", "f0d0fbb7-31ff-45ae-9f0f-4e429c0c797f"), &n3.UUID, &n3.Content, &n3.AddedOn, &n3.EditedOn, &n3.Public)
testutils.AssertEqual(t, bookCount, 2, "number of books mismatch")
testutils.AssertEqual(t, noteCount, 3, "number of notes mismatch")
testutils.AssertEqual(t, jsNoteCount, tc.expectedJsNoteCount, "js book notes length mismatch")
testutils.AssertEqual(t, linuxNoteCount, tc.expectedLinuxNoteCount, "linux book notes length mismatch")
testutils.AssertEqual(t, n1.UUID, "43827b9a-c2b0-4c06-a290-97991c896653", "n1 mismatch")
testutils.AssertEqual(t, n1.Content, "Booleans have toString()", "n1 content mismatch")
testutils.AssertEqual(t, n2.UUID, "3e065d55-6d47-42f2-a6bf-f5844130b2d2", "n2 uuid mismatch")
testutils.AssertEqual(t, n2.Content, "wc -l to count words", "n2 content mismatch")
testutils.AssertEqual(t, n3.UUID, tc.expectedNoteUUID, "edited note uuid mismatch")
testutils.AssertEqual(t, n3.Content, tc.expectedNoteContent, "edited note content mismatch")
testutils.AssertEqual(t, n3.AddedOn, tc.expectedNoteAddedOn, "edited note added_on mismatch")
testutils.AssertEqual(t, n3.EditedOn, tc.expectedNoteEditedOn, "edited note edited_on mismatch")
testutils.AssertEqual(t, n3.Public, tc.expectedNotePublic, "edited note public mismatch")
}()
}
}
func TestReduceAddBook(t *testing.T) {
// Setup
ctx := testutils.InitEnv("../tmp", "../testutils/fixtures/schema.sql")
defer testutils.TeardownEnv(ctx)
testutils.Setup1(t, ctx)
// Execute
b, err := json.Marshal(&actions.AddBookDataV1{BookName: "new_book"})
action := actions.Action{
Type: actions.ActionAddBook,
Schema: 1,
Data: b,
Timestamp: 1517629805,
}
db := ctx.DB
tx, err := db.Begin()
if err != nil {
panic(errors.Wrap(err, "beginning a transaction"))
}
if err = Reduce(ctx, tx, action); err != nil {
tx.Rollback()
t.Fatal(errors.Wrap(err, "Failed to process action"))
}
tx.Commit()
// Test
var bookCount, newBookNoteCount int
testutils.MustScan(t, "counting books", db.QueryRow("SELECT count(*) FROM books"), &bookCount)
testutils.MustScan(t, "counting note in the new book", db.QueryRow("SELECT count(*) FROM notes INNER JOIN books ON books.uuid = notes.book_uuid WHERE books.label = ?", "new_book"), &newBookNoteCount)
testutils.AssertEqual(t, bookCount, 3, "number of books mismatch")
testutils.AssertEqual(t, newBookNoteCount, 0, "new book number of notes mismatch")
}
func TestReduceRemoveBook(t *testing.T) {
// Setup
ctx := testutils.InitEnv("../tmp", "../testutils/fixtures/schema.sql")
defer testutils.TeardownEnv(ctx)
testutils.Setup2(t, ctx)
// Execute
b, err := json.Marshal(&actions.RemoveBookDataV1{BookName: "linux"})
action := actions.Action{
Type: actions.ActionRemoveBook,
Schema: 1,
Data: b,
Timestamp: 1517629805,
}
db := ctx.DB
tx, err := db.Begin()
if err != nil {
panic(errors.Wrap(err, "beginning a transaction"))
}
if err = Reduce(ctx, tx, action); err != nil {
tx.Rollback()
t.Fatal(errors.Wrap(err, "Failed to process action"))
}
tx.Commit()
// Test
var bookCount, noteCount, jsNoteCount, linuxNoteCount int
var jsBookLabel string
testutils.MustScan(t, "counting books", db.QueryRow("SELECT count(*) FROM books"), &bookCount)
testutils.MustScan(t, "counting note", db.QueryRow("SELECT count(*) FROM notes"), &noteCount)
testutils.MustScan(t, "counting js note", db.QueryRow("SELECT count(*) FROM notes WHERE book_uuid = ?", "js-book-uuid"), &jsNoteCount)
testutils.MustScan(t, "counting linux note", db.QueryRow("SELECT count(*) FROM notes WHERE book_uuid = ?", "linux-book-uuid"), &linuxNoteCount)
testutils.MustScan(t, "scanning book", db.QueryRow("SELECT label FROM books WHERE uuid = ?", "js-book-uuid"), &jsBookLabel)
var n1, n2 infra.Note
testutils.MustScan(t, "scanning note 1", db.QueryRow("SELECT uuid, content FROM notes WHERE uuid = ?", "43827b9a-c2b0-4c06-a290-97991c896653"), &n1.UUID, &n1.Content)
testutils.MustScan(t, "scanning note 2", db.QueryRow("SELECT uuid, content FROM notes WHERE uuid = ?", "f0d0fbb7-31ff-45ae-9f0f-4e429c0c797f"), &n2.UUID, &n2.Content)
testutils.AssertEqual(t, bookCount, 1, "number of books mismatch")
testutils.AssertEqual(t, noteCount, 2, "number of notes mismatch")
testutils.AssertEqual(t, jsNoteCount, 2, "js note count mismatch")
testutils.AssertEqual(t, linuxNoteCount, 0, "linux note count mismatch")
testutils.AssertEqual(t, jsBookLabel, "js", "remaining book name mismatch")
testutils.AssertEqual(t, n1.UUID, "43827b9a-c2b0-4c06-a290-97991c896653", "remaining note uuid mismatch")
testutils.AssertEqual(t, n1.Content, "Booleans have toString()", "remaining note content mismatch")
testutils.AssertEqual(t, n2.UUID, "f0d0fbb7-31ff-45ae-9f0f-4e429c0c797f", "edited note uuid mismatch")
testutils.AssertEqual(t, n2.Content, "Date object implements mathematical comparisons", "edited note content mismatch")
}

View file

@ -20,7 +20,7 @@ func shouldCheckUpdate(ctx infra.DnoteCtx) (bool, error) {
db := ctx.DB
var lastUpgrade int64
err := db.QueryRow("SELECT value FROM system WHERE key = ?", "last_upgrade").Scan(&lastUpgrade)
err := db.QueryRow("SELECT value FROM system WHERE key = ?", infra.SystemLastUpgrade).Scan(&lastUpgrade)
if err != nil {
return false, errors.Wrap(err, "getting last_udpate")
}
@ -34,7 +34,7 @@ func touchLastUpgrade(ctx infra.DnoteCtx) error {
db := ctx.DB
now := time.Now().Unix()
_, err := db.Exec("UPDATE system SET value = ? WHERE key = ?", now, "last_upgrade")
_, err := db.Exec("UPDATE system SET value = ? WHERE key = ?", now, infra.SystemLastUpgrade)
if err != nil {
return errors.Wrap(err, "updating last_upgrade")
}

View file

@ -6,7 +6,6 @@ import (
"fmt"
"os"
"os/user"
"time"
// use sqlite
_ "github.com/mattn/go-sqlite3"
@ -20,6 +19,14 @@ var (
// SystemSchema is the key for schema in the system table
SystemSchema = "schema"
// SystemRemoteSchema is the key for remote schema in the system table
SystemRemoteSchema = "remote_schema"
// SystemLastSyncAt is the timestamp of the server at the last sync
SystemLastSyncAt = "last_sync_time"
// SystemLastMaxUSN is the user's max_usn from the server at the alst sync
SystemLastMaxUSN = "last_max_usn"
// SystemLastUpgrade is the timestamp at which the system more recently checked for an upgrade
SystemLastUpgrade = "last_upgrade"
)
// DnoteCtx is a context holding the information of the current runtime
@ -37,33 +44,6 @@ type Config struct {
APIKey string
}
// Dnote holds the whole dnote data
type Dnote map[string]Book
// Book holds a metadata and its notes
type Book struct {
Name string `json:"name"`
Notes []Note `json:"notes"`
}
// Note represents a single microlesson
type Note struct {
UUID string `json:"uuid"`
Content string `json:"content"`
AddedOn int64 `json:"added_on"`
EditedOn int64 `json:"edited_on"`
Public bool `json:"public"`
}
// Timestamp holds time information
type Timestamp struct {
LastUpgrade int64 `yaml:"last_upgrade"`
// id of the most recent action synced from the server
Bookmark int `yaml:"bookmark"`
// timestamp of the most recent action performed by the cli
LastAction int64 `yaml:"last_action"`
}
// NewCtx returns a new dnote context
func NewCtx(apiEndpoint, versionTag string) (DnoteCtx, error) {
homeDir, err := getHomeDir()
@ -145,6 +125,15 @@ func InitDB(ctx DnoteCtx) error {
return errors.Wrap(err, "creating books table")
}
_, err = db.Exec(`CREATE TABLE IF NOT EXISTS system
(
key string NOT NULL,
value text NOT NULL
)`)
if err != nil {
return errors.Wrap(err, "creating system table")
}
_, err = db.Exec(`CREATE TABLE IF NOT EXISTS actions
(
uuid text PRIMARY KEY,
@ -157,15 +146,6 @@ func InitDB(ctx DnoteCtx) error {
return errors.Wrap(err, "creating actions table")
}
_, err = db.Exec(`CREATE TABLE IF NOT EXISTS system
(
key string NOT NULL,
value text NOT NULL
)`)
if err != nil {
return errors.Wrap(err, "creating system table")
}
_, err = db.Exec(`
CREATE UNIQUE INDEX IF NOT EXISTS idx_books_label ON books(label);
CREATE UNIQUE INDEX IF NOT EXISTS idx_notes_uuid ON notes(uuid);
@ -178,43 +158,3 @@ func InitDB(ctx DnoteCtx) error {
return nil
}
// InitSystem inserts system data if missing
func InitSystem(ctx DnoteCtx) error {
db := ctx.DB
tx, err := db.Begin()
if err != nil {
return errors.Wrap(err, "beginning a transaction")
}
var bookmarkCount, lastUpgradeCount int
if err := db.QueryRow("SELECT count(*) FROM system WHERE key = ?", "bookmark").
Scan(&bookmarkCount); err != nil {
return errors.Wrap(err, "counting bookmarks")
}
if bookmarkCount == 0 {
_, err := tx.Exec("INSERT INTO system (key, value) VALUES (?, ?)", "bookmark", 0)
if err != nil {
tx.Rollback()
return errors.Wrap(err, "inserting bookmark")
}
}
if err := db.QueryRow("SELECT count(*) FROM system WHERE key = ?", "last_upgrade").
Scan(&lastUpgradeCount); err != nil {
return errors.Wrap(err, "counting last_upgrade")
}
if lastUpgradeCount == 0 {
now := time.Now().Unix()
_, err := tx.Exec("INSERT INTO system (key, value) VALUES (?, ?)", "last_upgrade", now)
if err != nil {
tx.Rollback()
return errors.Wrap(err, "inserting bookmark")
}
}
tx.Commit()
return nil
}

View file

@ -1,7 +1,6 @@
package main
import (
"encoding/json"
"fmt"
"log"
"os"
@ -10,7 +9,6 @@ import (
"github.com/pkg/errors"
"github.com/dnote/actions"
"github.com/dnote/cli/core"
"github.com/dnote/cli/infra"
"github.com/dnote/cli/testutils"
@ -30,7 +28,7 @@ func TestMain(m *testing.M) {
func TestInit(t *testing.T) {
// Set up
ctx := testutils.InitEnv("../tmp", "./testutils/fixtures/schema.sql")
ctx := testutils.InitEnv(t, "./tmp", "./testutils/fixtures/schema.sql", true)
defer testutils.TeardownEnv(ctx)
// Execute
@ -46,25 +44,35 @@ func TestInit(t *testing.T) {
db := ctx.DB
var notesTableCount, booksTableCount, actionsTableCount, systemTableCount int
var notesTableCount, booksTableCount, systemTableCount int
testutils.MustScan(t, "counting notes",
db.QueryRow("SELECT count(*) FROM sqlite_master WHERE type = ? AND name = ?", "table", "notes"), &notesTableCount)
testutils.MustScan(t, "counting books",
db.QueryRow("SELECT count(*) FROM sqlite_master WHERE type = ? AND name = ?", "table", "books"), &booksTableCount)
testutils.MustScan(t, "counting actions",
db.QueryRow("SELECT count(*) FROM sqlite_master WHERE type = ? AND name = ?", "table", "actions"), &actionsTableCount)
testutils.MustScan(t, "counting system",
db.QueryRow("SELECT count(*) FROM sqlite_master WHERE type = ? AND name = ?", "table", "system"), &systemTableCount)
testutils.AssertEqual(t, notesTableCount, 1, "notes table count mismatch")
testutils.AssertEqual(t, booksTableCount, 1, "books table count mismatch")
testutils.AssertEqual(t, actionsTableCount, 1, "actions table count mismatch")
testutils.AssertEqual(t, systemTableCount, 1, "system table count mismatch")
// test that all default system configurations are generated
var lastUpgrade, lastMaxUSN, lastSyncAt string
testutils.MustScan(t, "scanning last upgrade",
db.QueryRow("SELECT value FROM system WHERE key = ?", infra.SystemLastUpgrade), &lastUpgrade)
testutils.MustScan(t, "scanning last max usn",
db.QueryRow("SELECT value FROM system WHERE key = ?", infra.SystemLastMaxUSN), &lastMaxUSN)
testutils.MustScan(t, "scanning last sync at",
db.QueryRow("SELECT value FROM system WHERE key = ?", infra.SystemLastSyncAt), &lastSyncAt)
testutils.AssertNotEqual(t, lastUpgrade, "", "last upgrade should not be empty")
testutils.AssertNotEqual(t, lastMaxUSN, "", "last max usn should not be empty")
testutils.AssertNotEqual(t, lastSyncAt, "", "last sync at should not be empty")
}
func TestAddNote_NewBook_ContentFlag(t *testing.T) {
// Set up
ctx := testutils.InitEnv("../tmp", "./testutils/fixtures/schema.sql")
ctx := testutils.InitEnv(t, "./tmp", "./testutils/fixtures/schema.sql", true)
defer testutils.TeardownEnv(ctx)
// Execute
@ -73,49 +81,30 @@ func TestAddNote_NewBook_ContentFlag(t *testing.T) {
// Test
db := ctx.DB
var actionCount, noteCount, bookCount int
testutils.MustScan(t, "counting actions", db.QueryRow("SELECT count(*) FROM actions"), &actionCount)
var noteCount, bookCount int
testutils.MustScan(t, "counting books", db.QueryRow("SELECT count(*) FROM books"), &bookCount)
testutils.MustScan(t, "counting notes", db.QueryRow("SELECT count(*) FROM notes"), &noteCount)
testutils.AssertEqualf(t, actionCount, 2, "action count mismatch")
testutils.AssertEqualf(t, bookCount, 1, "book count mismatch")
testutils.AssertEqualf(t, noteCount, 1, "note count mismatch")
var jsBookUUID string
testutils.MustScan(t, "getting js book uuid", db.QueryRow("SELECT uuid FROM books where label = ?", "js"), &jsBookUUID)
var note infra.Note
var book core.Book
testutils.MustScan(t, "getting book", db.QueryRow("SELECT uuid, dirty FROM books where label = ?", "js"), &book.UUID, &book.Dirty)
var note core.Note
testutils.MustScan(t, "getting note",
db.QueryRow("SELECT uuid, content, added_on FROM notes where book_uuid = ?", jsBookUUID), &note.UUID, &note.Content, &note.AddedOn)
var bookAction, noteAction actions.Action
testutils.MustScan(t, "getting book action",
db.QueryRow("SELECT data, timestamp FROM actions where type = ?", actions.ActionAddBook), &bookAction.Data, &bookAction.Timestamp)
testutils.MustScan(t, "getting note action",
db.QueryRow("SELECT data, timestamp FROM actions where type = ?", actions.ActionAddNote), &noteAction.Data, &noteAction.Timestamp)
db.QueryRow("SELECT uuid, content, added_on, dirty FROM notes where book_uuid = ?", book.UUID), &note.UUID, &note.Content, &note.AddedOn, &note.Dirty)
var noteActionData actions.AddNoteDataV1
var bookActionData actions.AddBookDataV1
if err := json.Unmarshal(bookAction.Data, &bookActionData); err != nil {
log.Fatalf("unmarshalling the action data: %s", err)
}
if err := json.Unmarshal(noteAction.Data, &noteActionData); err != nil {
log.Fatalf("unmarshalling the action data: %s", err)
}
testutils.AssertEqual(t, book.Dirty, true, "Book dirty mismatch")
testutils.AssertNotEqual(t, bookActionData.BookName, "", "bookAction data note_uuid mismatch")
testutils.AssertNotEqual(t, bookAction.Timestamp, 0, "bookAction timestamp mismatch")
testutils.AssertEqual(t, noteActionData.Content, "foo", "noteAction data name mismatch")
testutils.AssertNotEqual(t, noteActionData.NoteUUID, nil, "noteAction data note_uuid mismatch")
testutils.AssertNotEqual(t, noteActionData.BookName, "", "noteAction data note_uuid mismatch")
testutils.AssertNotEqual(t, noteAction.Timestamp, 0, "noteAction timestamp mismatch")
testutils.AssertNotEqual(t, note.UUID, "", "Note should have UUID")
testutils.AssertEqual(t, note.Content, "foo", "Note content mismatch")
testutils.AssertEqual(t, note.Dirty, true, "Note dirty mismatch")
testutils.AssertNotEqual(t, note.AddedOn, int64(0), "Note added_on mismatch")
}
func TestAddNote_ExistingBook_ContentFlag(t *testing.T) {
// Set up
ctx := testutils.InitEnv("../tmp", "./testutils/fixtures/schema.sql")
ctx := testutils.InitEnv(t, "./tmp", "./testutils/fixtures/schema.sql", true)
defer testutils.TeardownEnv(ctx)
testutils.Setup3(t, ctx)
@ -126,43 +115,37 @@ func TestAddNote_ExistingBook_ContentFlag(t *testing.T) {
// Test
db := ctx.DB
var actionCount, noteCount, bookCount int
testutils.MustScan(t, "counting actions", db.QueryRow("SELECT count(*) FROM actions"), &actionCount)
var noteCount, bookCount int
testutils.MustScan(t, "counting books", db.QueryRow("SELECT count(*) FROM books"), &bookCount)
testutils.MustScan(t, "counting notes", db.QueryRow("SELECT count(*) FROM notes"), &noteCount)
testutils.AssertEqualf(t, actionCount, 1, "action count mismatch")
testutils.AssertEqualf(t, bookCount, 1, "book count mismatch")
testutils.AssertEqualf(t, noteCount, 2, "note count mismatch")
var n1, n2 infra.Note
var n1, n2 core.Note
testutils.MustScan(t, "getting n1",
db.QueryRow("SELECT uuid, content, added_on FROM notes WHERE book_uuid = ? AND uuid = ?", "js-book-uuid", "43827b9a-c2b0-4c06-a290-97991c896653"), &n1.UUID, &n1.Content, &n1.AddedOn)
db.QueryRow("SELECT uuid, content, added_on, dirty FROM notes WHERE book_uuid = ? AND uuid = ?", "js-book-uuid", "43827b9a-c2b0-4c06-a290-97991c896653"), &n1.UUID, &n1.Content, &n1.AddedOn, &n1.Dirty)
testutils.MustScan(t, "getting n2",
db.QueryRow("SELECT uuid, content, added_on FROM notes WHERE book_uuid = ? AND content = ?", "js-book-uuid", "foo"), &n2.UUID, &n2.Content, &n2.AddedOn)
var noteAction actions.Action
testutils.MustScan(t, "getting note action",
db.QueryRow("SELECT data, timestamp FROM actions WHERE type = ?", actions.ActionAddNote), &noteAction.Data, &noteAction.Timestamp)
db.QueryRow("SELECT uuid, content, added_on, dirty FROM notes WHERE book_uuid = ? AND content = ?", "js-book-uuid", "foo"), &n2.UUID, &n2.Content, &n2.AddedOn, &n2.Dirty)
var noteActionData actions.AddNoteDataV1
if err := json.Unmarshal(noteAction.Data, &noteActionData); err != nil {
log.Fatalf("unmarshalling the action data: %s", err)
}
var book core.Book
testutils.MustScan(t, "getting book", db.QueryRow("SELECT dirty FROM books where label = ?", "js"), &book.Dirty)
testutils.AssertEqual(t, noteActionData.Content, "foo", "action data name mismatch")
testutils.AssertNotEqual(t, noteActionData.NoteUUID, "", "action data note_uuid mismatch")
testutils.AssertEqual(t, noteActionData.BookName, "js", "action data book_name mismatch")
testutils.AssertNotEqual(t, noteAction.Timestamp, 0, "action timestamp mismatch")
testutils.AssertNotEqual(t, n1.UUID, "", "Note should have UUID")
testutils.AssertEqual(t, n1.Content, "Booleans have toString()", "Note content mismatch")
testutils.AssertEqual(t, n1.AddedOn, int64(1515199943), "Note added_on mismatch")
testutils.AssertNotEqual(t, n2.UUID, "", "Note should have UUID")
testutils.AssertEqual(t, n2.Content, "foo", "Note content mismatch")
testutils.AssertEqual(t, book.Dirty, false, "Book dirty mismatch")
testutils.AssertNotEqual(t, n1.UUID, "", "n1 should have UUID")
testutils.AssertEqual(t, n1.Content, "Booleans have toString()", "n1 content mismatch")
testutils.AssertEqual(t, n1.AddedOn, int64(1515199943), "n1 added_on mismatch")
testutils.AssertEqual(t, n1.Dirty, false, "n1 dirty mismatch")
testutils.AssertNotEqual(t, n2.UUID, "", "n2 should have UUID")
testutils.AssertEqual(t, n2.Content, "foo", "n2 content mismatch")
testutils.AssertEqual(t, n2.Dirty, true, "n2 dirty mismatch")
}
func TestEditNote_ContentFlag(t *testing.T) {
// Set up
ctx := testutils.InitEnv("../tmp", "./testutils/fixtures/schema.sql")
ctx := testutils.InitEnv(t, "./tmp", "./testutils/fixtures/schema.sql", true)
defer testutils.TeardownEnv(ctx)
testutils.Setup4(t, ctx)
@ -173,46 +156,32 @@ func TestEditNote_ContentFlag(t *testing.T) {
// Test
db := ctx.DB
var actionCount, noteCount, bookCount int
testutils.MustScan(t, "counting actions", db.QueryRow("SELECT count(*) FROM actions"), &actionCount)
var noteCount, bookCount int
testutils.MustScan(t, "counting books", db.QueryRow("SELECT count(*) FROM books"), &bookCount)
testutils.MustScan(t, "counting notes", db.QueryRow("SELECT count(*) FROM notes"), &noteCount)
testutils.AssertEqualf(t, actionCount, 1, "action count mismatch")
testutils.AssertEqualf(t, bookCount, 1, "book count mismatch")
testutils.AssertEqualf(t, noteCount, 2, "note count mismatch")
var n1, n2 infra.Note
var n1, n2 core.Note
testutils.MustScan(t, "getting n1",
db.QueryRow("SELECT uuid, content, added_on FROM notes where book_uuid = ? AND uuid = ?", "js-book-uuid", "43827b9a-c2b0-4c06-a290-97991c896653"), &n1.UUID, &n1.Content, &n1.AddedOn)
db.QueryRow("SELECT uuid, content, added_on, dirty FROM notes where book_uuid = ? AND uuid = ?", "js-book-uuid", "43827b9a-c2b0-4c06-a290-97991c896653"), &n1.UUID, &n1.Content, &n1.AddedOn, &n1.Dirty)
testutils.MustScan(t, "getting n2",
db.QueryRow("SELECT uuid, content, added_on FROM notes where book_uuid = ? AND uuid = ?", "js-book-uuid", "f0d0fbb7-31ff-45ae-9f0f-4e429c0c797f"), &n2.UUID, &n2.Content, &n2.AddedOn)
var noteAction actions.Action
testutils.MustScan(t, "getting note action",
db.QueryRow("SELECT data, type, schema FROM actions where type = ?", actions.ActionEditNote),
&noteAction.Data, &noteAction.Type, &noteAction.Schema)
db.QueryRow("SELECT uuid, content, added_on, dirty FROM notes where book_uuid = ? AND uuid = ?", "js-book-uuid", "f0d0fbb7-31ff-45ae-9f0f-4e429c0c797f"), &n2.UUID, &n2.Content, &n2.AddedOn, &n2.Dirty)
var actionData actions.EditNoteDataV3
if err := json.Unmarshal(noteAction.Data, &actionData); err != nil {
log.Fatalf("Failed to unmarshal the action data: %s", err)
}
testutils.AssertEqual(t, n1.UUID, "43827b9a-c2b0-4c06-a290-97991c896653", "n1 should have UUID")
testutils.AssertEqual(t, n1.Content, "Booleans have toString()", "n1 content mismatch")
testutils.AssertEqual(t, n1.Dirty, false, "n1 dirty mismatch")
testutils.AssertEqual(t, noteAction.Type, actions.ActionEditNote, "action type mismatch")
testutils.AssertEqual(t, noteAction.Schema, 3, "action schema mismatch")
testutils.AssertEqual(t, *actionData.Content, "foo bar", "action data name mismatch")
testutils.AssertEqual(t, actionData.BookName, (*string)(nil), "action data book_name mismatch")
testutils.AssertEqual(t, actionData.NoteUUID, "f0d0fbb7-31ff-45ae-9f0f-4e429c0c797f", "action data note_uuis mismatch")
testutils.AssertNotEqual(t, noteAction.Timestamp, 0, "action timestamp mismatch")
testutils.AssertEqual(t, n1.UUID, "43827b9a-c2b0-4c06-a290-97991c896653", "Note should have UUID")
testutils.AssertEqual(t, n1.Content, "Booleans have toString()", "Note content mismatch")
testutils.AssertEqual(t, n2.UUID, "f0d0fbb7-31ff-45ae-9f0f-4e429c0c797f", "Note should have UUID")
testutils.AssertEqual(t, n2.Content, "foo bar", "Note content mismatch")
testutils.AssertEqual(t, n2.Dirty, true, "n2 dirty mismatch")
testutils.AssertNotEqual(t, n2.EditedOn, 0, "Note edited_on mismatch")
}
func TestRemoveNote(t *testing.T) {
// Set up
ctx := testutils.InitEnv("../tmp", "./testutils/fixtures/schema.sql")
ctx := testutils.InitEnv(t, "./tmp", "./testutils/fixtures/schema.sql", true)
defer testutils.TeardownEnv(ctx)
testutils.Setup2(t, ctx)
@ -223,53 +192,67 @@ func TestRemoveNote(t *testing.T) {
// Test
db := ctx.DB
var actionCount, noteCount, bookCount, jsNoteCount, linuxNoteCount int
testutils.MustScan(t, "counting actions", db.QueryRow("SELECT count(*) FROM actions"), &actionCount)
var noteCount, bookCount, jsNoteCount, linuxNoteCount int
testutils.MustScan(t, "counting books", db.QueryRow("SELECT count(*) FROM books"), &bookCount)
testutils.MustScan(t, "counting notes", db.QueryRow("SELECT count(*) FROM notes"), &noteCount)
testutils.MustScan(t, "counting js notes", db.QueryRow("SELECT count(*) FROM notes WHERE book_uuid = ?", "js-book-uuid"), &jsNoteCount)
testutils.MustScan(t, "counting linux notes", db.QueryRow("SELECT count(*) FROM notes WHERE book_uuid = ?", "linux-book-uuid"), &linuxNoteCount)
testutils.AssertEqualf(t, actionCount, 1, "action count mismatch")
testutils.AssertEqualf(t, bookCount, 2, "book count mismatch")
testutils.AssertEqualf(t, noteCount, 2, "note count mismatch")
testutils.AssertEqual(t, jsNoteCount, 1, "Book should have one note")
testutils.AssertEqual(t, linuxNoteCount, 1, "Other book should have one note")
testutils.AssertEqualf(t, noteCount, 3, "note count mismatch")
testutils.AssertEqual(t, jsNoteCount, 2, "js book should have 2 notes")
testutils.AssertEqual(t, linuxNoteCount, 1, "linux book book should have 1 note")
var b1, b2 infra.Book
var n1 infra.Note
var b1, b2 core.Book
var n1, n2, n3 core.Note
testutils.MustScan(t, "getting b1",
db.QueryRow("SELECT label FROM books WHERE uuid = ?", "js-book-uuid"),
&b1.Name)
db.QueryRow("SELECT label, deleted, usn FROM books WHERE uuid = ?", "js-book-uuid"),
&b1.Label, &b1.Deleted, &b1.USN)
testutils.MustScan(t, "getting b2",
db.QueryRow("SELECT label FROM books WHERE uuid = ?", "linux-book-uuid"),
&b2.Name)
db.QueryRow("SELECT label, deleted, usn FROM books WHERE uuid = ?", "linux-book-uuid"),
&b2.Label, &b2.Deleted, &b2.USN)
testutils.MustScan(t, "getting n1",
db.QueryRow("SELECT uuid, content, added_on FROM notes WHERE book_uuid = ? AND id = ?", "js-book-uuid", 2),
&n1.UUID, &n1.Content, &n1.AddedOn)
db.QueryRow("SELECT uuid, content, added_on, deleted, dirty, usn FROM notes WHERE book_uuid = ? AND id = ?", "js-book-uuid", 1),
&n1.UUID, &n1.Content, &n1.AddedOn, &n1.Deleted, &n1.Dirty, &n1.USN)
testutils.MustScan(t, "getting n2",
db.QueryRow("SELECT uuid, content, added_on, deleted, dirty, usn FROM notes WHERE book_uuid = ? AND id = ?", "js-book-uuid", 2),
&n2.UUID, &n2.Content, &n2.AddedOn, &n2.Deleted, &n2.Dirty, &n2.USN)
testutils.MustScan(t, "getting n3",
db.QueryRow("SELECT uuid, content, added_on, deleted, dirty, usn FROM notes WHERE book_uuid = ? AND id = ?", "linux-book-uuid", 3),
&n3.UUID, &n3.Content, &n3.AddedOn, &n3.Deleted, &n3.Dirty, &n3.USN)
var noteAction actions.Action
testutils.MustScan(t, "getting note action",
db.QueryRow("SELECT type, schema, data FROM actions WHERE type = ?", actions.ActionRemoveNote), &noteAction.Type, &noteAction.Schema, &noteAction.Data)
testutils.AssertEqual(t, b1.Label, "js", "b1 label mismatch")
testutils.AssertEqual(t, b1.Deleted, false, "b1 deleted mismatch")
testutils.AssertEqual(t, b1.Dirty, false, "b1 Dirty mismatch")
testutils.AssertEqual(t, b1.USN, 111, "b1 usn mismatch")
var actionData actions.RemoveNoteDataV1
if err := json.Unmarshal(noteAction.Data, &actionData); err != nil {
log.Fatalf("unmarshalling the action data: %s", err)
}
testutils.AssertEqual(t, b2.Label, "linux", "b2 label mismatch")
testutils.AssertEqual(t, b2.Deleted, false, "b2 deleted mismatch")
testutils.AssertEqual(t, b2.Dirty, false, "b2 Dirty mismatch")
testutils.AssertEqual(t, b2.USN, 122, "b2 usn mismatch")
testutils.AssertEqual(t, b1.Name, "js", "b1 label mismatch")
testutils.AssertEqual(t, b2.Name, "linux", "b2 label mismatch")
testutils.AssertEqual(t, noteAction.Schema, 2, "action schema mismatch")
testutils.AssertEqual(t, noteAction.Type, actions.ActionRemoveNote, "action type mismatch")
testutils.AssertEqual(t, actionData.NoteUUID, "f0d0fbb7-31ff-45ae-9f0f-4e429c0c797f", "action data note_uuid mismatch")
testutils.AssertNotEqual(t, noteAction.Timestamp, 0, "action timestamp mismatch")
testutils.AssertEqual(t, n1.UUID, "43827b9a-c2b0-4c06-a290-97991c896653", "Note should have UUID")
testutils.AssertEqual(t, n1.Content, "Booleans have toString()", "Note content mismatch")
testutils.AssertEqual(t, n1.UUID, "f0d0fbb7-31ff-45ae-9f0f-4e429c0c797f", "n1 should have UUID")
testutils.AssertEqual(t, n1.Content, "", "n1 content mismatch")
testutils.AssertEqual(t, n1.Deleted, true, "n1 deleted mismatch")
testutils.AssertEqual(t, n1.Dirty, true, "n1 Dirty mismatch")
testutils.AssertEqual(t, n1.USN, 11, "n1 usn mismatch")
testutils.AssertEqual(t, n2.UUID, "43827b9a-c2b0-4c06-a290-97991c896653", "n2 should have UUID")
testutils.AssertEqual(t, n2.Content, "n2 content", "n2 content mismatch")
testutils.AssertEqual(t, n2.Deleted, false, "n2 deleted mismatch")
testutils.AssertEqual(t, n2.Dirty, false, "n2 Dirty mismatch")
testutils.AssertEqual(t, n2.USN, 12, "n2 usn mismatch")
testutils.AssertEqual(t, n3.UUID, "3e065d55-6d47-42f2-a6bf-f5844130b2d2", "n3 should have UUID")
testutils.AssertEqual(t, n3.Content, "n3 content", "n3 content mismatch")
testutils.AssertEqual(t, n3.Deleted, false, "n3 deleted mismatch")
testutils.AssertEqual(t, n3.Dirty, false, "n3 Dirty mismatch")
testutils.AssertEqual(t, n3.USN, 13, "n3 usn mismatch")
}
func TestRemoveBook(t *testing.T) {
// Set up
ctx := testutils.InitEnv("../tmp", "./testutils/fixtures/schema.sql")
ctx := testutils.InitEnv(t, "./tmp", "./testutils/fixtures/schema.sql", true)
defer testutils.TeardownEnv(ctx)
testutils.Setup2(t, ctx)
@ -280,35 +263,60 @@ func TestRemoveBook(t *testing.T) {
// Test
db := ctx.DB
var actionCount, noteCount, bookCount, jsNoteCount, linuxNoteCount int
testutils.MustScan(t, "counting actions", db.QueryRow("SELECT count(*) FROM actions"), &actionCount)
var noteCount, bookCount, jsNoteCount, linuxNoteCount int
testutils.MustScan(t, "counting books", db.QueryRow("SELECT count(*) FROM books"), &bookCount)
testutils.MustScan(t, "counting notes", db.QueryRow("SELECT count(*) FROM notes"), &noteCount)
testutils.MustScan(t, "counting js notes", db.QueryRow("SELECT count(*) FROM notes WHERE book_uuid = ?", "js-book-uuid"), &jsNoteCount)
testutils.MustScan(t, "counting linux notes", db.QueryRow("SELECT count(*) FROM notes WHERE book_uuid = ?", "linux-book-uuid"), &linuxNoteCount)
testutils.AssertEqualf(t, actionCount, 1, "action count mismatch")
testutils.AssertEqualf(t, bookCount, 1, "book count mismatch")
testutils.AssertEqualf(t, noteCount, 1, "note count mismatch")
testutils.AssertEqual(t, jsNoteCount, 0, "some notes in book were not deleted")
testutils.AssertEqual(t, linuxNoteCount, 1, "Other book should have one note")
testutils.AssertEqualf(t, bookCount, 2, "book count mismatch")
testutils.AssertEqualf(t, noteCount, 3, "note count mismatch")
testutils.AssertEqual(t, jsNoteCount, 2, "js book should have 2 notes")
testutils.AssertEqual(t, linuxNoteCount, 1, "linux book book should have 1 note")
var b1 infra.Book
var b1, b2 core.Book
var n1, n2, n3 core.Note
testutils.MustScan(t, "getting b1",
db.QueryRow("SELECT label FROM books WHERE uuid = ?", "linux-book-uuid"),
&b1.Name)
db.QueryRow("SELECT label, dirty, deleted, usn FROM books WHERE uuid = ?", "js-book-uuid"),
&b1.Label, &b1.Dirty, &b1.Deleted, &b1.USN)
testutils.MustScan(t, "getting b2",
db.QueryRow("SELECT label, dirty, deleted, usn FROM books WHERE uuid = ?", "linux-book-uuid"),
&b2.Label, &b2.Dirty, &b2.Deleted, &b2.USN)
testutils.MustScan(t, "getting n1",
db.QueryRow("SELECT uuid, content, added_on, dirty, deleted, usn FROM notes WHERE book_uuid = ? AND id = ?", "js-book-uuid", 1),
&n1.UUID, &n1.Content, &n1.AddedOn, &n1.Deleted, &n1.Dirty, &n1.USN)
testutils.MustScan(t, "getting n2",
db.QueryRow("SELECT uuid, content, added_on, dirty, deleted, usn FROM notes WHERE book_uuid = ? AND id = ?", "js-book-uuid", 2),
&n2.UUID, &n2.Content, &n2.AddedOn, &n2.Deleted, &n2.Dirty, &n2.USN)
testutils.MustScan(t, "getting n3",
db.QueryRow("SELECT uuid, content, added_on, dirty, deleted, usn FROM notes WHERE book_uuid = ? AND id = ?", "linux-book-uuid", 3),
&n3.UUID, &n3.Content, &n3.AddedOn, &n3.Deleted, &n3.Dirty, &n3.USN)
var action actions.Action
testutils.MustScan(t, "getting an action",
db.QueryRow("SELECT type, schema, data FROM actions WHERE type = ?", actions.ActionRemoveBook), &action.Type, &action.Schema, &action.Data)
testutils.AssertNotEqual(t, b1.Label, "js", "b1 label mismatch")
testutils.AssertEqual(t, b1.Dirty, true, "b1 Dirty mismatch")
testutils.AssertEqual(t, b1.Deleted, true, "b1 deleted mismatch")
testutils.AssertEqual(t, b1.USN, 111, "b1 usn mismatch")
var actionData actions.RemoveBookDataV1
if err := json.Unmarshal(action.Data, &actionData); err != nil {
log.Fatalf("unmarshalling the action data: %s", err)
}
testutils.AssertEqual(t, b2.Label, "linux", "b2 label mismatch")
testutils.AssertEqual(t, b2.Dirty, false, "b2 Dirty mismatch")
testutils.AssertEqual(t, b2.Deleted, false, "b2 deleted mismatch")
testutils.AssertEqual(t, b2.USN, 122, "b2 usn mismatch")
testutils.AssertEqual(t, action.Type, actions.ActionRemoveBook, "action type mismatch")
testutils.AssertEqual(t, actionData.BookName, "js", "action data name mismatch")
testutils.AssertNotEqual(t, action.Timestamp, 0, "action timestamp mismatch")
testutils.AssertEqual(t, b1.Name, "linux", "Remaining book name mismatch")
testutils.AssertEqual(t, n1.UUID, "f0d0fbb7-31ff-45ae-9f0f-4e429c0c797f", "n1 should have UUID")
testutils.AssertEqual(t, n1.Content, "", "n1 content mismatch")
testutils.AssertEqual(t, n1.Dirty, true, "n1 Dirty mismatch")
testutils.AssertEqual(t, n1.Deleted, true, "n1 deleted mismatch")
testutils.AssertEqual(t, n1.USN, 11, "n1 usn mismatch")
testutils.AssertEqual(t, n2.UUID, "43827b9a-c2b0-4c06-a290-97991c896653", "n2 should have UUID")
testutils.AssertEqual(t, n2.Content, "", "n2 content mismatch")
testutils.AssertEqual(t, n2.Dirty, true, "n2 Dirty mismatch")
testutils.AssertEqual(t, n2.Deleted, true, "n2 deleted mismatch")
testutils.AssertEqual(t, n2.USN, 12, "n2 usn mismatch")
testutils.AssertEqual(t, n3.UUID, "3e065d55-6d47-42f2-a6bf-f5844130b2d2", "n3 should have UUID")
testutils.AssertEqual(t, n3.Content, "n3 content", "n3 content mismatch")
testutils.AssertEqual(t, n3.Dirty, false, "n3 Dirty mismatch")
testutils.AssertEqual(t, n3.Deleted, false, "n3 deleted mismatch")
testutils.AssertEqual(t, n3.USN, 13, "n3 usn mismatch")
}

View file

@ -0,0 +1,35 @@
-- local-1-pre-schema.sql is the schema in which 'dirty' flags do not exist
CREATE TABLE notes
(
id integer PRIMARY KEY AUTOINCREMENT,
uuid text NOT NULL,
book_uuid text NOT NULL,
content text NOT NULL,
added_on integer NOT NULL,
edited_on integer DEFAULT 0,
public bool DEFAULT false
);
CREATE TABLE books
(
uuid text PRIMARY KEY,
label text NOT NULL
);
CREATE TABLE actions
(
uuid text PRIMARY KEY,
schema integer NOT NULL,
type text NOT NULL,
data text NOT NULL,
timestamp integer NOT NULL
);
CREATE TABLE system
(
key string NOT NULL,
value text NOT NULL
);
CREATE UNIQUE INDEX idx_books_label ON books(label);
CREATE UNIQUE INDEX idx_notes_uuid ON notes(uuid);
CREATE UNIQUE INDEX idx_books_uuid ON books(uuid);
CREATE UNIQUE INDEX idx_notes_id ON notes(id);
CREATE INDEX idx_notes_book_uuid ON notes(book_uuid);

View file

@ -0,0 +1,35 @@
-- local-5-pre-schema.sql is the schema in which the actions table is present
CREATE TABLE notes
(
id integer PRIMARY KEY AUTOINCREMENT,
uuid text NOT NULL,
book_uuid text NOT NULL,
content text NOT NULL,
added_on integer NOT NULL,
edited_on integer DEFAULT 0,
public bool DEFAULT false
, dirty bool DEFAULT false, usn int DEFAULT 0 NOT NULL);
CREATE TABLE books
(
uuid text PRIMARY KEY,
label text NOT NULL
, dirty bool DEFAULT false, usn int DEFAULT 0 NOT NULL);
CREATE TABLE actions
(
uuid text PRIMARY KEY,
schema integer NOT NULL,
type text NOT NULL,
data text NOT NULL,
timestamp integer NOT NULL
);
CREATE TABLE system
(
key string NOT NULL,
value text NOT NULL
);
CREATE UNIQUE INDEX idx_books_label ON books(label);
CREATE UNIQUE INDEX idx_notes_uuid ON notes(uuid);
CREATE UNIQUE INDEX idx_books_uuid ON books(uuid);
CREATE UNIQUE INDEX idx_notes_id ON notes(id);
CREATE INDEX idx_notes_book_uuid ON notes(book_uuid);

View file

@ -0,0 +1,25 @@
CREATE TABLE notes
(
id integer PRIMARY KEY AUTOINCREMENT,
uuid text NOT NULL,
book_uuid text NOT NULL,
content text NOT NULL,
added_on integer NOT NULL,
edited_on integer DEFAULT 0,
public bool DEFAULT false
, dirty bool DEFAULT false, usn int DEFAULT 0 NOT NULL, deleted bool DEFAULT false);
CREATE TABLE books
(
uuid text PRIMARY KEY,
label text NOT NULL
, dirty bool DEFAULT false, usn int DEFAULT 0 NOT NULL, deleted bool DEFAULT false);
CREATE TABLE system
(
key string NOT NULL,
value text NOT NULL
);
CREATE UNIQUE INDEX idx_books_label ON books(label);
CREATE UNIQUE INDEX idx_notes_uuid ON notes(uuid);
CREATE UNIQUE INDEX idx_books_uuid ON books(uuid);
CREATE UNIQUE INDEX idx_notes_id ON notes(id);
CREATE INDEX idx_notes_book_uuid ON notes(book_uuid);

View file

@ -0,0 +1,35 @@
-- 5-pre-schema.sql is the schema in which the actions table is present
CREATE TABLE notes
(
id integer PRIMARY KEY AUTOINCREMENT,
uuid text NOT NULL,
book_uuid text NOT NULL,
content text NOT NULL,
added_on integer NOT NULL,
edited_on integer DEFAULT 0,
public bool DEFAULT false
, dirty bool DEFAULT false, usn int DEFAULT 0 NOT NULL);
CREATE TABLE books
(
uuid text PRIMARY KEY,
label text NOT NULL
, dirty bool DEFAULT false, usn int DEFAULT 0 NOT NULL);
CREATE TABLE actions
(
uuid text PRIMARY KEY,
schema integer NOT NULL,
type text NOT NULL,
data text NOT NULL,
timestamp integer NOT NULL
);
CREATE TABLE system
(
key string NOT NULL,
value text NOT NULL
);
CREATE UNIQUE INDEX idx_books_label ON books(label);
CREATE UNIQUE INDEX idx_notes_uuid ON notes(uuid);
CREATE UNIQUE INDEX idx_books_uuid ON books(uuid);
CREATE UNIQUE INDEX idx_notes_id ON notes(id);
CREATE INDEX idx_notes_book_uuid ON notes(book_uuid);

View file

@ -19,7 +19,7 @@ func TestMigrateToV1(t *testing.T) {
t.Run("yaml exists", func(t *testing.T) {
// set up
ctx := testutils.InitEnv("../tmp", "../testutils/fixtures/schema.sql")
ctx := testutils.InitEnv(t, "../tmp", "../testutils/fixtures/schema.sql", true)
defer testutils.TeardownEnv(ctx)
yamlPath, err := filepath.Abs(filepath.Join(ctx.HomeDir, ".dnote-yaml-archived"))
@ -41,7 +41,7 @@ func TestMigrateToV1(t *testing.T) {
t.Run("yaml does not exist", func(t *testing.T) {
// set up
ctx := testutils.InitEnv("../tmp", "../testutils/fixtures/schema.sql")
ctx := testutils.InitEnv(t, "../tmp", "../testutils/fixtures/schema.sql", true)
defer testutils.TeardownEnv(ctx)
yamlPath, err := filepath.Abs(filepath.Join(ctx.HomeDir, ".dnote-yaml-archived"))
@ -62,10 +62,10 @@ func TestMigrateToV1(t *testing.T) {
}
func TestMigrateToV2(t *testing.T) {
ctx := testutils.InitEnv("../tmp", "../testutils/fixtures/schema.sql")
ctx := testutils.InitEnv(t, "../tmp", "../testutils/fixtures/schema.sql", true)
defer testutils.TeardownEnv(ctx)
testutils.CopyFixture(ctx, "./fixtures/2-pre-dnote.json", "dnote")
testutils.CopyFixture(ctx, "./fixtures/legacy-2-pre-dnote.json", "dnote")
// execute
if err := migrateToV2(ctx); err != nil {
@ -96,10 +96,10 @@ func TestMigrateToV2(t *testing.T) {
func TestMigrateToV3(t *testing.T) {
// set up
ctx := testutils.InitEnv("../tmp", "../testutils/fixtures/schema.sql")
ctx := testutils.InitEnv(t, "../tmp", "../testutils/fixtures/schema.sql", true)
defer testutils.TeardownEnv(ctx)
testutils.CopyFixture(ctx, "./fixtures/3-pre-dnote.json", "dnote")
testutils.CopyFixture(ctx, "./fixtures/legacy-3-pre-dnote.json", "dnote")
// execute
if err := migrateToV3(ctx); err != nil {
@ -130,11 +130,11 @@ func TestMigrateToV3(t *testing.T) {
func TestMigrateToV4(t *testing.T) {
// set up
ctx := testutils.InitEnv("../tmp", "../testutils/fixtures/schema.sql")
ctx := testutils.InitEnv(t, "../tmp", "../testutils/fixtures/schema.sql", true)
defer testutils.TeardownEnv(ctx)
defer os.Setenv("EDITOR", "")
testutils.CopyFixture(ctx, "./fixtures/4-pre-dnoterc.yaml", "dnoterc")
testutils.CopyFixture(ctx, "./fixtures/legacy-4-pre-dnoterc.yaml", "dnoterc")
// execute
os.Setenv("EDITOR", "vim")
@ -155,10 +155,10 @@ func TestMigrateToV4(t *testing.T) {
func TestMigrateToV5(t *testing.T) {
// set up
ctx := testutils.InitEnv("../tmp", "../testutils/fixtures/schema.sql")
ctx := testutils.InitEnv(t, "../tmp", "../testutils/fixtures/schema.sql", true)
defer testutils.TeardownEnv(ctx)
testutils.CopyFixture(ctx, "./fixtures/5-pre-actions.json", "actions")
testutils.CopyFixture(ctx, "./fixtures/legacy-5-pre-actions.json", "actions")
// execute
if err := migrateToV5(ctx); err != nil {
@ -167,7 +167,7 @@ func TestMigrateToV5(t *testing.T) {
// test
var oldActions []migrateToV5PreAction
testutils.ReadJSON("./fixtures/5-pre-actions.json", &oldActions)
testutils.ReadJSON("./fixtures/legacy-5-pre-actions.json", &oldActions)
b := testutils.ReadFile(ctx, "actions")
var migratedActions []migrateToV5PostAction
@ -252,10 +252,10 @@ func TestMigrateToV5(t *testing.T) {
func TestMigrateToV6(t *testing.T) {
// set up
ctx := testutils.InitEnv("../tmp", "../testutils/fixtures/schema.sql")
ctx := testutils.InitEnv(t, "../tmp", "../testutils/fixtures/schema.sql", true)
defer testutils.TeardownEnv(ctx)
testutils.CopyFixture(ctx, "./fixtures/6-pre-dnote.json", "dnote")
testutils.CopyFixture(ctx, "./fixtures/legacy-6-pre-dnote.json", "dnote")
// execute
if err := migrateToV6(ctx); err != nil {
@ -269,7 +269,7 @@ func TestMigrateToV6(t *testing.T) {
t.Fatal(errors.Wrap(err, "Failed to unmarshal the result into Dnote").Error())
}
b = testutils.ReadFileAbs("./fixtures/6-post-dnote.json")
b = testutils.ReadFileAbs("./fixtures/legacy-6-post-dnote.json")
var expected migrateToV6PostDnote
if err := json.Unmarshal(b, &expected); err != nil {
t.Fatal(errors.Wrap(err, "Failed to unmarshal the result into Dnote").Error())
@ -282,10 +282,10 @@ func TestMigrateToV6(t *testing.T) {
func TestMigrateToV7(t *testing.T) {
// set up
ctx := testutils.InitEnv("../tmp", "../testutils/fixtures/schema.sql")
ctx := testutils.InitEnv(t, "../tmp", "../testutils/fixtures/schema.sql", true)
defer testutils.TeardownEnv(ctx)
testutils.CopyFixture(ctx, "./fixtures/7-pre-actions.json", "actions")
testutils.CopyFixture(ctx, "./fixtures/legacy-7-pre-actions.json", "actions")
// execute
if err := migrateToV7(ctx); err != nil {
@ -299,7 +299,7 @@ func TestMigrateToV7(t *testing.T) {
t.Fatal(errors.Wrap(err, "unmarshalling the result").Error())
}
b2 := testutils.ReadFileAbs("./fixtures/7-post-actions.json")
b2 := testutils.ReadFileAbs("./fixtures/legacy-7-post-actions.json")
var expected []migrateToV7Action
if err := json.Unmarshal(b, &expected); err != nil {
t.Fatal(errors.Wrap(err, "unmarshalling the result into Dnote").Error())
@ -316,15 +316,15 @@ func TestMigrateToV7(t *testing.T) {
}
func TestMigrateToV8(t *testing.T) {
ctx := testutils.InitEnv("../tmp", "../testutils/fixtures/schema.sql")
ctx := testutils.InitEnv(t, "../tmp", "./fixtures/local-1-pre-schema.sql", false)
defer testutils.TeardownEnv(ctx)
// set up
testutils.CopyFixture(ctx, "./fixtures/8-actions.json", "actions")
testutils.CopyFixture(ctx, "./fixtures/8-dnote.json", "dnote")
testutils.CopyFixture(ctx, "./fixtures/8-dnoterc.yaml", "dnoterc")
testutils.CopyFixture(ctx, "./fixtures/8-schema.yaml", "schema")
testutils.CopyFixture(ctx, "./fixtures/8-timestamps.yaml", "timestamps")
testutils.CopyFixture(ctx, "./fixtures/legacy-8-actions.json", "actions")
testutils.CopyFixture(ctx, "./fixtures/legacy-8-dnote.json", "dnote")
testutils.CopyFixture(ctx, "./fixtures/legacy-8-dnoterc.yaml", "dnoterc")
testutils.CopyFixture(ctx, "./fixtures/legacy-8-schema.yaml", "schema")
testutils.CopyFixture(ctx, "./fixtures/legacy-8-timestamps.yaml", "timestamps")
// execute
if err := migrateToV8(ctx); err != nil {

View file

@ -8,19 +8,34 @@ import (
"github.com/pkg/errors"
)
const (
// LocalMode is a local migration mode
LocalMode = iota
// RemoteMode is a remote migration mode
RemoteMode
)
// LocalSequence is a list of local migrations to be run
var LocalSequence = []migration{
lm1,
lm2,
lm3,
lm4,
lm5,
lm6,
}
func initSchema(ctx infra.DnoteCtx) (int, error) {
// RemoteSequence is a list of remote migrations to be run
var RemoteSequence = []migration{
rm1,
}
func initSchema(ctx infra.DnoteCtx, schemaKey string) (int, error) {
// schemaVersion is the index of the latest run migration in the sequence
schemaVersion := 0
db := ctx.DB
_, err := db.Exec("INSERT INTO system (key, value) VALUES (?, ?)", infra.SystemSchema, schemaVersion)
_, err := db.Exec("INSERT INTO system (key, value) VALUES (?, ?)", schemaKey, schemaVersion)
if err != nil {
return schemaVersion, errors.Wrap(err, "inserting schema")
}
@ -28,13 +43,25 @@ func initSchema(ctx infra.DnoteCtx) (int, error) {
return schemaVersion, nil
}
func getSchema(ctx infra.DnoteCtx) (int, error) {
func getSchemaKey(mode int) (string, error) {
if mode == LocalMode {
return infra.SystemSchema, nil
}
if mode == RemoteMode {
return infra.SystemRemoteSchema, nil
}
return "", errors.Errorf("unsupported migration type '%d'", mode)
}
func getSchema(ctx infra.DnoteCtx, schemaKey string) (int, error) {
var ret int
db := ctx.DB
err := db.QueryRow("SELECT value FROM system where key = ?", infra.SystemSchema).Scan(&ret)
err := db.QueryRow("SELECT value FROM system where key = ?", schemaKey).Scan(&ret)
if err == sql.ErrNoRows {
ret, err = initSchema(ctx)
ret, err = initSchema(ctx, schemaKey)
if err != nil {
return ret, errors.Wrap(err, "initializing schema")
@ -46,7 +73,7 @@ func getSchema(ctx infra.DnoteCtx) (int, error) {
return ret, nil
}
func execute(ctx infra.DnoteCtx, m migration) error {
func execute(ctx infra.DnoteCtx, m migration, schemaKey string) error {
log.Debug("running migration %s\n", m.name)
tx, err := ctx.DB.Begin()
@ -57,17 +84,17 @@ func execute(ctx infra.DnoteCtx, m migration) error {
err = m.run(ctx, tx)
if err != nil {
tx.Rollback()
return errors.Wrapf(err, "running migration '%s'", m.name)
return errors.Wrapf(err, "running '%s'", m.name)
}
var currentSchema int
err = tx.QueryRow("SELECT value FROM system WHERE key = ?", infra.SystemSchema).Scan(&currentSchema)
err = tx.QueryRow("SELECT value FROM system WHERE key = ?", schemaKey).Scan(&currentSchema)
if err != nil {
tx.Rollback()
return errors.Wrap(err, "getting current schema")
}
_, err = tx.Exec("UPDATE system SET value = ? WHERE key = ?", currentSchema+1, infra.SystemSchema)
_, err = tx.Exec("UPDATE system SET value = value + 1 WHERE key = ?", schemaKey)
if err != nil {
tx.Rollback()
return errors.Wrap(err, "incrementing schema")
@ -79,8 +106,13 @@ func execute(ctx infra.DnoteCtx, m migration) error {
}
// Run performs unrun migrations
func Run(ctx infra.DnoteCtx, migrations []migration) error {
schema, err := getSchema(ctx)
func Run(ctx infra.DnoteCtx, migrations []migration, mode int) error {
schemaKey, err := getSchemaKey(mode)
if err != nil {
return errors.Wrap(err, "getting schema key")
}
schema, err := getSchema(ctx, schemaKey)
if err != nil {
return errors.Wrap(err, "getting the current schema")
}
@ -90,8 +122,8 @@ func Run(ctx infra.DnoteCtx, migrations []migration) error {
toRun := migrations[schema:]
for _, m := range toRun {
if err := execute(ctx, m); err != nil {
return errors.Wrapf(err, "running migration %s", m.name)
if err := execute(ctx, m, schemaKey); err != nil {
return errors.Wrap(err, "running migration")
}
}

View file

@ -2,8 +2,12 @@ package migrate
import (
"database/sql"
"encoding/json"
"fmt"
"net/http"
"net/http/httptest"
"testing"
"time"
"github.com/dnote/actions"
"github.com/dnote/cli/infra"
@ -13,212 +17,281 @@ import (
)
func TestExecute_bump_schema(t *testing.T) {
// set up
ctx := testutils.InitEnv("../tmp", "../testutils/fixtures/schema.sql")
defer testutils.TeardownEnv(ctx)
db := ctx.DB
testutils.MustExec(t, "inserting a schema", db, "INSERT INTO system (key, value) VALUES (?, ?)", infra.SystemSchema, 8)
m1 := migration{
name: "noop",
run: func(ctx infra.DnoteCtx, tx *sql.Tx) error {
return nil
testCases := []struct {
schemaKey string
}{
{
schemaKey: infra.SystemSchema,
},
}
m2 := migration{
name: "noop",
run: func(ctx infra.DnoteCtx, tx *sql.Tx) error {
return nil
{
schemaKey: infra.SystemRemoteSchema,
},
}
// execute
err := execute(ctx, m1)
if err != nil {
t.Fatal(errors.Wrap(err, "failed to execute"))
}
err = execute(ctx, m2)
if err != nil {
t.Fatal(errors.Wrap(err, "failed to execute"))
}
for _, tc := range testCases {
func() {
// set up
ctx := testutils.InitEnv(t, "../tmp", "../testutils/fixtures/schema.sql", false)
defer testutils.TeardownEnv(ctx)
// test
var schema int
testutils.MustScan(t, "getting schema", db.QueryRow("SELECT value FROM system WHERE key = ?", infra.SystemSchema), &schema)
testutils.AssertEqual(t, schema, 10, "schema was not incremented properly")
db := ctx.DB
testutils.MustExec(t, "inserting a schema", db, "INSERT INTO system (key, value) VALUES (?, ?)", tc.schemaKey, 8)
m1 := migration{
name: "noop",
run: func(ctx infra.DnoteCtx, tx *sql.Tx) error {
return nil
},
}
m2 := migration{
name: "noop",
run: func(ctx infra.DnoteCtx, tx *sql.Tx) error {
return nil
},
}
// execute
err := execute(ctx, m1, tc.schemaKey)
if err != nil {
t.Fatal(errors.Wrap(err, "failed to execute"))
}
err = execute(ctx, m2, tc.schemaKey)
if err != nil {
t.Fatal(errors.Wrap(err, "failed to execute"))
}
// test
var schema int
testutils.MustScan(t, "getting schema", db.QueryRow("SELECT value FROM system WHERE key = ?", tc.schemaKey), &schema)
testutils.AssertEqual(t, schema, 10, "schema was not incremented properly")
}()
}
}
func TestRun_nonfresh(t *testing.T) {
// set up
ctx := testutils.InitEnv("../tmp", "../testutils/fixtures/schema.sql")
defer testutils.TeardownEnv(ctx)
db := ctx.DB
testutils.MustExec(t, "inserting a schema", db, "INSERT INTO system (key, value) VALUES (?, ?)", infra.SystemSchema, 2)
testutils.MustExec(t, "creating a temporary table for testing", db,
"CREATE TABLE migrate_run_test ( name string )")
sequence := []migration{
migration{
name: "v1",
run: func(ctx infra.DnoteCtx, tx *sql.Tx) error {
testutils.MustExec(t, "marking v1 completed", db, "INSERT INTO migrate_run_test (name) VALUES (?)", "v1")
return nil
},
testCases := []struct {
mode int
schemaKey string
}{
{
mode: LocalMode,
schemaKey: infra.SystemSchema,
},
migration{
name: "v2",
run: func(ctx infra.DnoteCtx, tx *sql.Tx) error {
testutils.MustExec(t, "marking v2 completed", db, "INSERT INTO migrate_run_test (name) VALUES (?)", "v2")
return nil
},
},
migration{
name: "v3",
run: func(ctx infra.DnoteCtx, tx *sql.Tx) error {
testutils.MustExec(t, "marking v3 completed", db, "INSERT INTO migrate_run_test (name) VALUES (?)", "v3")
return nil
},
},
migration{
name: "v4",
run: func(ctx infra.DnoteCtx, tx *sql.Tx) error {
testutils.MustExec(t, "marking v4 completed", db, "INSERT INTO migrate_run_test (name) VALUES (?)", "v4")
return nil
},
{
mode: RemoteMode,
schemaKey: infra.SystemRemoteSchema,
},
}
// execute
err := Run(ctx, sequence)
if err != nil {
t.Fatal(errors.Wrap(err, "failed to run"))
for _, tc := range testCases {
func() {
// set up
ctx := testutils.InitEnv(t, "../tmp", "../testutils/fixtures/schema.sql", false)
defer testutils.TeardownEnv(ctx)
db := ctx.DB
testutils.MustExec(t, "inserting a schema", db, "INSERT INTO system (key, value) VALUES (?, ?)", tc.schemaKey, 2)
testutils.MustExec(t, "creating a temporary table for testing", db,
"CREATE TABLE migrate_run_test ( name string )")
sequence := []migration{
migration{
name: "v1",
run: func(ctx infra.DnoteCtx, tx *sql.Tx) error {
testutils.MustExec(t, "marking v1 completed", db, "INSERT INTO migrate_run_test (name) VALUES (?)", "v1")
return nil
},
},
migration{
name: "v2",
run: func(ctx infra.DnoteCtx, tx *sql.Tx) error {
testutils.MustExec(t, "marking v2 completed", db, "INSERT INTO migrate_run_test (name) VALUES (?)", "v2")
return nil
},
},
migration{
name: "v3",
run: func(ctx infra.DnoteCtx, tx *sql.Tx) error {
testutils.MustExec(t, "marking v3 completed", db, "INSERT INTO migrate_run_test (name) VALUES (?)", "v3")
return nil
},
},
migration{
name: "v4",
run: func(ctx infra.DnoteCtx, tx *sql.Tx) error {
testutils.MustExec(t, "marking v4 completed", db, "INSERT INTO migrate_run_test (name) VALUES (?)", "v4")
return nil
},
},
}
// execute
err := Run(ctx, sequence, tc.mode)
if err != nil {
t.Fatal(errors.Wrap(err, "failed to run"))
}
// test
var schema int
testutils.MustScan(t, fmt.Sprintf("getting schema for %s", tc.schemaKey), db.QueryRow("SELECT value FROM system WHERE key = ?", tc.schemaKey), &schema)
testutils.AssertEqual(t, schema, 4, fmt.Sprintf("schema was not updated for %s", tc.schemaKey))
var testRunCount int
testutils.MustScan(t, "counting test runs", db.QueryRow("SELECT count(*) FROM migrate_run_test"), &testRunCount)
testutils.AssertEqual(t, testRunCount, 2, "test run count mismatch")
var testRun1, testRun2 string
testutils.MustScan(t, "finding test run 1", db.QueryRow("SELECT name FROM migrate_run_test WHERE name = ?", "v3"), &testRun1)
testutils.MustScan(t, "finding test run 2", db.QueryRow("SELECT name FROM migrate_run_test WHERE name = ?", "v4"), &testRun2)
}()
}
// test
var schema int
testutils.MustScan(t, fmt.Sprintf("getting schema for %s", infra.SystemSchema), db.QueryRow("SELECT value FROM system WHERE key = ?", infra.SystemSchema), &schema)
testutils.AssertEqual(t, schema, 4, fmt.Sprintf("schema was not updated for %s", infra.SystemSchema))
var testRunCount int
testutils.MustScan(t, "counting test runs", db.QueryRow("SELECT count(*) FROM migrate_run_test"), &testRunCount)
testutils.AssertEqual(t, testRunCount, 2, "test run count mismatch")
var testRun1, testRun2 string
testutils.MustScan(t, "finding test run 1", db.QueryRow("SELECT name FROM migrate_run_test WHERE name = ?", "v3"), &testRun1)
testutils.MustScan(t, "finding test run 2", db.QueryRow("SELECT name FROM migrate_run_test WHERE name = ?", "v4"), &testRun2)
}
func TestRun_fresh(t *testing.T) {
// set up
ctx := testutils.InitEnv("../tmp", "../testutils/fixtures/schema.sql")
defer testutils.TeardownEnv(ctx)
db := ctx.DB
testutils.MustExec(t, "creating a temporary table for testing", db,
"CREATE TABLE migrate_run_test ( name string )")
sequence := []migration{
migration{
name: "v1",
run: func(ctx infra.DnoteCtx, tx *sql.Tx) error {
testutils.MustExec(t, "marking v1 completed", db, "INSERT INTO migrate_run_test (name) VALUES (?)", "v1")
return nil
},
testCases := []struct {
mode int
schemaKey string
}{
{
mode: LocalMode,
schemaKey: infra.SystemSchema,
},
migration{
name: "v2",
run: func(ctx infra.DnoteCtx, tx *sql.Tx) error {
testutils.MustExec(t, "marking v2 completed", db, "INSERT INTO migrate_run_test (name) VALUES (?)", "v2")
return nil
},
},
migration{
name: "v3",
run: func(ctx infra.DnoteCtx, tx *sql.Tx) error {
testutils.MustExec(t, "marking v3 completed", db, "INSERT INTO migrate_run_test (name) VALUES (?)", "v3")
return nil
},
{
mode: RemoteMode,
schemaKey: infra.SystemRemoteSchema,
},
}
// execute
err := Run(ctx, sequence)
if err != nil {
t.Fatal(errors.Wrap(err, "failed to run"))
for _, tc := range testCases {
func() {
// set up
ctx := testutils.InitEnv(t, "../tmp", "../testutils/fixtures/schema.sql", false)
defer testutils.TeardownEnv(ctx)
db := ctx.DB
testutils.MustExec(t, "creating a temporary table for testing", db,
"CREATE TABLE migrate_run_test ( name string )")
sequence := []migration{
migration{
name: "v1",
run: func(ctx infra.DnoteCtx, tx *sql.Tx) error {
testutils.MustExec(t, "marking v1 completed", db, "INSERT INTO migrate_run_test (name) VALUES (?)", "v1")
return nil
},
},
migration{
name: "v2",
run: func(ctx infra.DnoteCtx, tx *sql.Tx) error {
testutils.MustExec(t, "marking v2 completed", db, "INSERT INTO migrate_run_test (name) VALUES (?)", "v2")
return nil
},
},
migration{
name: "v3",
run: func(ctx infra.DnoteCtx, tx *sql.Tx) error {
testutils.MustExec(t, "marking v3 completed", db, "INSERT INTO migrate_run_test (name) VALUES (?)", "v3")
return nil
},
},
}
// execute
err := Run(ctx, sequence, tc.mode)
if err != nil {
t.Fatal(errors.Wrap(err, "failed to run"))
}
// test
var schema int
testutils.MustScan(t, "getting schema", db.QueryRow("SELECT value FROM system WHERE key = ?", tc.schemaKey), &schema)
testutils.AssertEqual(t, schema, 3, "schema was not updated")
var testRunCount int
testutils.MustScan(t, "counting test runs", db.QueryRow("SELECT count(*) FROM migrate_run_test"), &testRunCount)
testutils.AssertEqual(t, testRunCount, 3, "test run count mismatch")
var testRun1, testRun2, testRun3 string
testutils.MustScan(t, "finding test run 1", db.QueryRow("SELECT name FROM migrate_run_test WHERE name = ?", "v1"), &testRun1)
testutils.MustScan(t, "finding test run 2", db.QueryRow("SELECT name FROM migrate_run_test WHERE name = ?", "v2"), &testRun2)
testutils.MustScan(t, "finding test run 2", db.QueryRow("SELECT name FROM migrate_run_test WHERE name = ?", "v3"), &testRun3)
}()
}
// test
var schema int
testutils.MustScan(t, "getting schema", db.QueryRow("SELECT value FROM system WHERE key = ?", infra.SystemSchema), &schema)
testutils.AssertEqual(t, schema, 3, "schema was not updated")
var testRunCount int
testutils.MustScan(t, "counting test runs", db.QueryRow("SELECT count(*) FROM migrate_run_test"), &testRunCount)
testutils.AssertEqual(t, testRunCount, 3, "test run count mismatch")
var testRun1, testRun2, testRun3 string
testutils.MustScan(t, "finding test run 1", db.QueryRow("SELECT name FROM migrate_run_test WHERE name = ?", "v1"), &testRun1)
testutils.MustScan(t, "finding test run 2", db.QueryRow("SELECT name FROM migrate_run_test WHERE name = ?", "v2"), &testRun2)
testutils.MustScan(t, "finding test run 2", db.QueryRow("SELECT name FROM migrate_run_test WHERE name = ?", "v3"), &testRun3)
}
func TestRun_up_to_date(t *testing.T) {
// set up
ctx := testutils.InitEnv("../tmp", "../testutils/fixtures/schema.sql")
defer testutils.TeardownEnv(ctx)
db := ctx.DB
testutils.MustExec(t, "creating a temporary table for testing", db,
"CREATE TABLE migrate_run_test ( name string )")
testutils.MustExec(t, "inserting a schema", db, "INSERT INTO system (key, value) VALUES (?, ?)", infra.SystemSchema, 3)
sequence := []migration{
migration{
name: "v1",
run: func(ctx infra.DnoteCtx, tx *sql.Tx) error {
testutils.MustExec(t, "marking v1 completed", db, "INSERT INTO migrate_run_test (name) VALUES (?)", "v1")
return nil
},
testCases := []struct {
mode int
schemaKey string
}{
{
mode: LocalMode,
schemaKey: infra.SystemSchema,
},
migration{
name: "v2",
run: func(ctx infra.DnoteCtx, tx *sql.Tx) error {
testutils.MustExec(t, "marking v2 completed", db, "INSERT INTO migrate_run_test (name) VALUES (?)", "v2")
return nil
},
},
migration{
name: "v3",
run: func(ctx infra.DnoteCtx, tx *sql.Tx) error {
testutils.MustExec(t, "marking v3 completed", db, "INSERT INTO migrate_run_test (name) VALUES (?)", "v3")
return nil
},
{
mode: RemoteMode,
schemaKey: infra.SystemRemoteSchema,
},
}
// execute
err := Run(ctx, sequence)
if err != nil {
t.Fatal(errors.Wrap(err, "failed to run"))
for _, tc := range testCases {
func() {
// set up
ctx := testutils.InitEnv(t, "../tmp", "../testutils/fixtures/schema.sql", false)
defer testutils.TeardownEnv(ctx)
db := ctx.DB
testutils.MustExec(t, "creating a temporary table for testing", db,
"CREATE TABLE migrate_run_test ( name string )")
testutils.MustExec(t, "inserting a schema", db, "INSERT INTO system (key, value) VALUES (?, ?)", tc.schemaKey, 3)
sequence := []migration{
migration{
name: "v1",
run: func(ctx infra.DnoteCtx, tx *sql.Tx) error {
testutils.MustExec(t, "marking v1 completed", db, "INSERT INTO migrate_run_test (name) VALUES (?)", "v1")
return nil
},
},
migration{
name: "v2",
run: func(ctx infra.DnoteCtx, tx *sql.Tx) error {
testutils.MustExec(t, "marking v2 completed", db, "INSERT INTO migrate_run_test (name) VALUES (?)", "v2")
return nil
},
},
migration{
name: "v3",
run: func(ctx infra.DnoteCtx, tx *sql.Tx) error {
testutils.MustExec(t, "marking v3 completed", db, "INSERT INTO migrate_run_test (name) VALUES (?)", "v3")
return nil
},
},
}
// execute
err := Run(ctx, sequence, tc.mode)
if err != nil {
t.Fatal(errors.Wrap(err, "failed to run"))
}
// test
var schema int
testutils.MustScan(t, "getting schema", db.QueryRow("SELECT value FROM system WHERE key = ?", tc.schemaKey), &schema)
testutils.AssertEqual(t, schema, 3, "schema was not updated")
var testRunCount int
testutils.MustScan(t, "counting test runs", db.QueryRow("SELECT count(*) FROM migrate_run_test"), &testRunCount)
testutils.AssertEqual(t, testRunCount, 0, "test run count mismatch")
}()
}
// test
var schema int
testutils.MustScan(t, "getting schema", db.QueryRow("SELECT value FROM system WHERE key = ?", infra.SystemSchema), &schema)
testutils.AssertEqual(t, schema, 3, "schema was not updated")
var testRunCount int
testutils.MustScan(t, "counting test runs", db.QueryRow("SELECT count(*) FROM migrate_run_test"), &testRunCount)
testutils.AssertEqual(t, testRunCount, 0, "test run count mismatch")
}
func TestLocalMigration1(t *testing.T) {
// set up
ctx := testutils.InitEnv("../tmp", "../testutils/fixtures/schema.sql")
ctx := testutils.InitEnv(t, "../tmp", "./fixtures/local-1-pre-schema.sql", false)
defer testutils.TeardownEnv(ctx)
db := ctx.DB
@ -295,7 +368,7 @@ func TestLocalMigration1(t *testing.T) {
func TestLocalMigration2(t *testing.T) {
// set up
ctx := testutils.InitEnv("../tmp", "../testutils/fixtures/schema.sql")
ctx := testutils.InitEnv(t, "../tmp", "./fixtures/local-1-pre-schema.sql", false)
defer testutils.TeardownEnv(ctx)
db := ctx.DB
@ -381,7 +454,7 @@ func TestLocalMigration2(t *testing.T) {
func TestLocalMigration3(t *testing.T) {
// set up
ctx := testutils.InitEnv("../tmp", "../testutils/fixtures/schema.sql")
ctx := testutils.InitEnv(t, "../tmp", "./fixtures/local-1-pre-schema.sql", false)
defer testutils.TeardownEnv(ctx)
db := ctx.DB
@ -452,3 +525,314 @@ func TestLocalMigration3(t *testing.T) {
testutils.AssertEqual(t, a3.Timestamp, int64(1537829463), "a3 timestamp mismatch")
testutils.AssertEqual(t, a3Data.NoteUUID, "note-2-uuid", "a3 data note_uuid mismatch")
}
func TestLocalMigration4(t *testing.T) {
// set up
ctx := testutils.InitEnv(t, "../tmp", "./fixtures/local-1-pre-schema.sql", false)
defer testutils.TeardownEnv(ctx)
db := ctx.DB
b1UUID := utils.GenerateUUID()
testutils.MustExec(t, "inserting css book", db, "INSERT INTO books (uuid, label) VALUES (?, ?)", b1UUID, "css")
n1UUID := utils.GenerateUUID()
testutils.MustExec(t, "inserting css note", db, "INSERT INTO notes (uuid, book_uuid, content, added_on) VALUES (?, ?, ?, ?)", n1UUID, b1UUID, "n1 content", time.Now().UnixNano())
// Execute
tx, err := db.Begin()
if err != nil {
t.Fatal(errors.Wrap(err, "beginning a transaction"))
}
err = lm4.run(ctx, tx)
if err != nil {
tx.Rollback()
t.Fatal(errors.Wrap(err, "failed to run"))
}
tx.Commit()
// Test
var n1Dirty, b1Dirty bool
var n1Deleted, b1Deleted bool
var n1USN, b1USN int
testutils.MustScan(t, "scanning the newly added dirty flag of n1", db.QueryRow("SELECT dirty, deleted, usn FROM notes WHERE uuid = ?", n1UUID), &n1Dirty, &n1Deleted, &n1USN)
testutils.MustScan(t, "scanning the newly added dirty flag of b1", db.QueryRow("SELECT dirty, deleted, usn FROM books WHERE uuid = ?", b1UUID), &b1Dirty, &b1Deleted, &b1USN)
testutils.AssertEqual(t, n1Dirty, false, "n1 dirty flag should be false by default")
testutils.AssertEqual(t, b1Dirty, false, "b1 dirty flag should be false by default")
testutils.AssertEqual(t, n1Deleted, false, "n1 deleted flag should be false by default")
testutils.AssertEqual(t, b1Deleted, false, "b1 deleted flag should be false by default")
testutils.AssertEqual(t, n1USN, 0, "n1 usn flag should be 0 by default")
testutils.AssertEqual(t, b1USN, 0, "b1 usn flag should be 0 by default")
}
func TestLocalMigration5(t *testing.T) {
// set up
ctx := testutils.InitEnv(t, "../tmp", "./fixtures/local-5-pre-schema.sql", false)
defer testutils.TeardownEnv(ctx)
db := ctx.DB
b1UUID := utils.GenerateUUID()
testutils.MustExec(t, "inserting css book", db, "INSERT INTO books (uuid, label) VALUES (?, ?)", b1UUID, "css")
b2UUID := utils.GenerateUUID()
testutils.MustExec(t, "inserting js book", db, "INSERT INTO books (uuid, label) VALUES (?, ?)", b2UUID, "js")
n1UUID := utils.GenerateUUID()
testutils.MustExec(t, "inserting css note", db, "INSERT INTO notes (uuid, book_uuid, content, added_on) VALUES (?, ?, ?, ?)", n1UUID, b1UUID, "n1 content", time.Now().UnixNano())
n2UUID := utils.GenerateUUID()
testutils.MustExec(t, "inserting css note", db, "INSERT INTO notes (uuid, book_uuid, content, added_on) VALUES (?, ?, ?, ?)", n2UUID, b1UUID, "n2 content", time.Now().UnixNano())
n3UUID := utils.GenerateUUID()
testutils.MustExec(t, "inserting css note", db, "INSERT INTO notes (uuid, book_uuid, content, added_on) VALUES (?, ?, ?, ?)", n3UUID, b1UUID, "n3 content", time.Now().UnixNano())
data := testutils.MustMarshalJSON(t, actions.AddBookDataV1{BookName: "js"})
testutils.MustExec(t, "inserting a1", db,
"INSERT INTO actions (uuid, schema, type, data, timestamp) VALUES (?, ?, ?, ?, ?)", "a1-uuid", 1, "add_book", string(data), 1537829463)
data = testutils.MustMarshalJSON(t, actions.AddNoteDataV2{NoteUUID: n1UUID, BookName: "css", Content: "n1 content", Public: false})
testutils.MustExec(t, "inserting a2", db,
"INSERT INTO actions (uuid, schema, type, data, timestamp) VALUES (?, ?, ?, ?, ?)", "a2-uuid", 1, "add_note", string(data), 1537829463)
updatedContent := "updated content"
data = testutils.MustMarshalJSON(t, actions.EditNoteDataV3{NoteUUID: n2UUID, BookName: (*string)(nil), Content: &updatedContent, Public: (*bool)(nil)})
testutils.MustExec(t, "inserting a3", db,
"INSERT INTO actions (uuid, schema, type, data, timestamp) VALUES (?, ?, ?, ?, ?)", "a3-uuid", 1, "edit_note", string(data), 1537829463)
// Execute
tx, err := db.Begin()
if err != nil {
t.Fatal(errors.Wrap(err, "beginning a transaction"))
}
err = lm5.run(ctx, tx)
if err != nil {
tx.Rollback()
t.Fatal(errors.Wrap(err, "failed to run"))
}
tx.Commit()
// Test
var b1Dirty, b2Dirty, n1Dirty, n2Dirty, n3Dirty bool
testutils.MustScan(t, "scanning the newly added dirty flag of b1", db.QueryRow("SELECT dirty FROM books WHERE uuid = ?", b1UUID), &b1Dirty)
testutils.MustScan(t, "scanning the newly added dirty flag of b2", db.QueryRow("SELECT dirty FROM books WHERE uuid = ?", b2UUID), &b2Dirty)
testutils.MustScan(t, "scanning the newly added dirty flag of n1", db.QueryRow("SELECT dirty FROM notes WHERE uuid = ?", n1UUID), &n1Dirty)
testutils.MustScan(t, "scanning the newly added dirty flag of n2", db.QueryRow("SELECT dirty FROM notes WHERE uuid = ?", n2UUID), &n2Dirty)
testutils.MustScan(t, "scanning the newly added dirty flag of n3", db.QueryRow("SELECT dirty FROM notes WHERE uuid = ?", n3UUID), &n3Dirty)
testutils.AssertEqual(t, b1Dirty, false, "b1 dirty flag should be false by default")
testutils.AssertEqual(t, b2Dirty, true, "b2 dirty flag should be false by default")
testutils.AssertEqual(t, n1Dirty, true, "n1 dirty flag should be false by default")
testutils.AssertEqual(t, n2Dirty, true, "n2 dirty flag should be false by default")
testutils.AssertEqual(t, n3Dirty, false, "n3 dirty flag should be false by default")
}
func TestLocalMigration6(t *testing.T) {
// set up
ctx := testutils.InitEnv(t, "../tmp", "./fixtures/local-5-pre-schema.sql", false)
defer testutils.TeardownEnv(ctx)
db := ctx.DB
data := testutils.MustMarshalJSON(t, actions.AddBookDataV1{BookName: "js"})
a1UUID := utils.GenerateUUID()
testutils.MustExec(t, "inserting action", db,
"INSERT INTO actions (uuid, schema, type, data, timestamp) VALUES (?, ?, ?, ?, ?)", a1UUID, 1, "add_book", string(data), 1537829463)
// Execute
tx, err := db.Begin()
if err != nil {
t.Fatal(errors.Wrap(err, "beginning a transaction"))
}
err = lm5.run(ctx, tx)
if err != nil {
tx.Rollback()
t.Fatal(errors.Wrap(err, "failed to run"))
}
tx.Commit()
// Test
var count int
err = db.QueryRow("SELECT name FROM sqlite_master WHERE type='table' AND name = ?;", "actions").Scan(&count)
testutils.AssertEqual(t, count, 0, "actions table should have been deleted")
}
func TestLocalMigration7_trash(t *testing.T) {
// set up
ctx := testutils.InitEnv(t, "../tmp", "./fixtures/local-7-pre-schema.sql", false)
defer testutils.TeardownEnv(ctx)
db := ctx.DB
b1UUID := utils.GenerateUUID()
testutils.MustExec(t, "inserting trash book", db, "INSERT INTO books (uuid, label) VALUES (?, ?)", b1UUID, "trash")
// Execute
tx, err := db.Begin()
if err != nil {
t.Fatal(errors.Wrap(err, "beginning a transaction"))
}
err = lm7.run(ctx, tx)
if err != nil {
tx.Rollback()
t.Fatal(errors.Wrap(err, "failed to run"))
}
tx.Commit()
// Test
var b1Label string
var b1Dirty bool
testutils.MustScan(t, "scanning b1 label", db.QueryRow("SELECT label, dirty FROM books WHERE uuid = ?", b1UUID), &b1Label, &b1Dirty)
testutils.AssertEqual(t, b1Label, "trash (2)", "b1 label was not migrated")
testutils.AssertEqual(t, b1Dirty, true, "b1 was not marked dirty")
}
func TestLocalMigration7_conflicts(t *testing.T) {
// set up
ctx := testutils.InitEnv(t, "../tmp", "./fixtures/local-7-pre-schema.sql", false)
defer testutils.TeardownEnv(ctx)
db := ctx.DB
b1UUID := utils.GenerateUUID()
testutils.MustExec(t, "inserting book", db, "INSERT INTO books (uuid, label) VALUES (?, ?)", b1UUID, "conflicts")
// Execute
tx, err := db.Begin()
if err != nil {
t.Fatal(errors.Wrap(err, "beginning a transaction"))
}
err = lm7.run(ctx, tx)
if err != nil {
tx.Rollback()
t.Fatal(errors.Wrap(err, "failed to run"))
}
tx.Commit()
// Test
var b1Label string
var b1Dirty bool
testutils.MustScan(t, "scanning b1 label", db.QueryRow("SELECT label, dirty FROM books WHERE uuid = ?", b1UUID), &b1Label, &b1Dirty)
testutils.AssertEqual(t, b1Label, "conflicts (2)", "b1 label was not migrated")
testutils.AssertEqual(t, b1Dirty, true, "b1 was not marked dirty")
}
func TestLocalMigration7_conflicts_dup(t *testing.T) {
// set up
ctx := testutils.InitEnv(t, "../tmp", "./fixtures/local-7-pre-schema.sql", false)
defer testutils.TeardownEnv(ctx)
db := ctx.DB
b1UUID := utils.GenerateUUID()
testutils.MustExec(t, "inserting book", db, "INSERT INTO books (uuid, label) VALUES (?, ?)", b1UUID, "conflicts")
b2UUID := utils.GenerateUUID()
testutils.MustExec(t, "inserting book", db, "INSERT INTO books (uuid, label) VALUES (?, ?)", b2UUID, "conflicts (2)")
// Execute
tx, err := db.Begin()
if err != nil {
t.Fatal(errors.Wrap(err, "beginning a transaction"))
}
err = lm7.run(ctx, tx)
if err != nil {
tx.Rollback()
t.Fatal(errors.Wrap(err, "failed to run"))
}
tx.Commit()
// Test
var b1Label, b2Label string
var b1Dirty, b2Dirty bool
testutils.MustScan(t, "scanning b1 label", db.QueryRow("SELECT label, dirty FROM books WHERE uuid = ?", b1UUID), &b1Label, &b1Dirty)
testutils.MustScan(t, "scanning b2 label", db.QueryRow("SELECT label, dirty FROM books WHERE uuid = ?", b2UUID), &b2Label, &b2Dirty)
testutils.AssertEqual(t, b1Label, "conflicts (3)", "b1 label was not migrated")
testutils.AssertEqual(t, b2Label, "conflicts (2)", "b1 label was not migrated")
testutils.AssertEqual(t, b1Dirty, true, "b1 was not marked dirty")
testutils.AssertEqual(t, b2Dirty, false, "b2 should not have been marked dirty")
}
func TestRemoteMigration1(t *testing.T) {
// set up
ctx := testutils.InitEnv(t, "../tmp", "./fixtures/remote-1-pre-schema.sql", false)
defer testutils.TeardownEnv(ctx)
JSBookUUID := "existing-js-book-uuid"
CSSBookUUID := "existing-css-book-uuid"
linuxBookUUID := "existing-linux-book-uuid"
newJSBookUUID := "new-js-book-uuid"
newCSSBookUUID := "new-css-book-uuid"
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if r.URL.String() == "/v1/books" {
res := []struct {
UUID string `json:"uuid"`
Label string `json:"label"`
}{
{
UUID: newJSBookUUID,
Label: "js",
},
{
UUID: newCSSBookUUID,
Label: "css",
},
// book that only exists on the server. client must ignore.
{
UUID: "golang-book-uuid",
Label: "golang",
},
}
if err := json.NewEncoder(w).Encode(res); err != nil {
t.Fatal(errors.Wrap(err, "encoding response"))
}
}
}))
defer server.Close()
ctx.APIEndpoint = server.URL
confStr := fmt.Sprintf("apikey: mock_api_key")
testutils.WriteFile(ctx, []byte(confStr), "dnoterc")
db := ctx.DB
testutils.MustExec(t, "inserting js book", db, "INSERT INTO books (uuid, label) VALUES (?, ?)", JSBookUUID, "js")
testutils.MustExec(t, "inserting css book", db, "INSERT INTO books (uuid, label) VALUES (?, ?)", CSSBookUUID, "css")
testutils.MustExec(t, "inserting linux book", db, "INSERT INTO books (uuid, label) VALUES (?, ?)", linuxBookUUID, "linux")
tx, err := db.Begin()
if err != nil {
t.Fatal(errors.Wrap(err, "beginning a transaction"))
}
err = rm1.run(ctx, tx)
if err != nil {
tx.Rollback()
t.Fatal(errors.Wrap(err, "failed to run"))
}
tx.Commit()
// test
var postJSBookUUID, postCSSBookUUID, postLinuxBookUUID string
testutils.MustScan(t, "getting js book uuid", db.QueryRow("SELECT uuid FROM books WHERE label = ?", "js"), &postJSBookUUID)
testutils.MustScan(t, "getting css book uuid", db.QueryRow("SELECT uuid FROM books WHERE label = ?", "css"), &postCSSBookUUID)
testutils.MustScan(t, "getting linux book uuid", db.QueryRow("SELECT uuid FROM books WHERE label = ?", "linux"), &postLinuxBookUUID)
testutils.AssertEqual(t, postJSBookUUID, newJSBookUUID, "js book uuid was not updated correctly")
testutils.AssertEqual(t, postCSSBookUUID, newCSSBookUUID, "css book uuid was not updated correctly")
testutils.AssertEqual(t, postLinuxBookUUID, linuxBookUUID, "linux book uuid changed")
}

View file

@ -3,9 +3,13 @@ package migrate
import (
"database/sql"
"encoding/json"
"fmt"
"github.com/dnote/actions"
"github.com/dnote/cli/client"
"github.com/dnote/cli/core"
"github.com/dnote/cli/infra"
"github.com/dnote/cli/log"
"github.com/pkg/errors"
)
@ -148,3 +152,223 @@ var lm3 = migration{
return nil
},
}
var lm4 = migration{
name: "add-dirty-usn-deleted-to-notes-and-books",
run: func(ctx infra.DnoteCtx, tx *sql.Tx) error {
_, err := tx.Exec("ALTER TABLE books ADD COLUMN dirty bool DEFAULT false")
if err != nil {
return errors.Wrap(err, "adding dirty column to books")
}
_, err = tx.Exec("ALTER TABLE books ADD COLUMN usn int DEFAULT 0 NOT NULL")
if err != nil {
return errors.Wrap(err, "adding usn column to books")
}
_, err = tx.Exec("ALTER TABLE books ADD COLUMN deleted bool DEFAULT false")
if err != nil {
return errors.Wrap(err, "adding deleted column to books")
}
_, err = tx.Exec("ALTER TABLE notes ADD COLUMN dirty bool DEFAULT false")
if err != nil {
return errors.Wrap(err, "adding dirty column to notes")
}
_, err = tx.Exec("ALTER TABLE notes ADD COLUMN usn int DEFAULT 0 NOT NULL")
if err != nil {
return errors.Wrap(err, "adding usn column to notes")
}
_, err = tx.Exec("ALTER TABLE notes ADD COLUMN deleted bool DEFAULT false")
if err != nil {
return errors.Wrap(err, "adding deleted column to notes")
}
return nil
},
}
var lm5 = migration{
name: "mark-action-targets-dirty",
run: func(ctx infra.DnoteCtx, tx *sql.Tx) error {
rows, err := tx.Query("SELECT uuid, data, type FROM actions")
if err != nil {
return errors.Wrap(err, "querying rows")
}
defer rows.Close()
for rows.Next() {
var uuid, dat, actionType string
err = rows.Scan(&uuid, &dat, &actionType)
if err != nil {
return errors.Wrap(err, "scanning a row")
}
// removed notes and removed books cannot be reliably derived retrospectively
// because books did not use to have uuid. Users will find locally deleted
// notes and books coming back to existence if they have not synced the change.
// But there will be no data loss.
switch actionType {
case "add_note":
var data actions.AddNoteDataV2
err = json.Unmarshal([]byte(dat), &data)
if err != nil {
return errors.Wrap(err, "unmarshalling existing data")
}
_, err := tx.Exec("UPDATE notes SET dirty = true WHERE uuid = ?", data.NoteUUID)
if err != nil {
return errors.Wrapf(err, "markig note dirty '%s'", data.NoteUUID)
}
case "edit_note":
var data actions.EditNoteDataV3
err = json.Unmarshal([]byte(dat), &data)
if err != nil {
return errors.Wrap(err, "unmarshalling existing data")
}
_, err := tx.Exec("UPDATE notes SET dirty = true WHERE uuid = ?", data.NoteUUID)
if err != nil {
return errors.Wrapf(err, "markig note dirty '%s'", data.NoteUUID)
}
case "add_book":
var data actions.AddBookDataV1
err = json.Unmarshal([]byte(dat), &data)
if err != nil {
return errors.Wrap(err, "unmarshalling existing data")
}
_, err := tx.Exec("UPDATE books SET dirty = true WHERE label = ?", data.BookName)
if err != nil {
return errors.Wrapf(err, "markig note dirty '%s'", data.BookName)
}
}
}
return nil
},
}
var lm6 = migration{
name: "drop-actions",
run: func(ctx infra.DnoteCtx, tx *sql.Tx) error {
_, err := tx.Exec("DROP TABLE actions;")
if err != nil {
return errors.Wrap(err, "dropping the actions table")
}
return nil
},
}
var lm7 = migration{
name: "resolve-conflicts-with-reserved-book-names",
run: func(ctx infra.DnoteCtx, tx *sql.Tx) error {
migrateBook := func(name string) error {
var uuid string
err := tx.QueryRow("SELECT uuid FROM books WHERE label = ?", name).Scan(&uuid)
if err == sql.ErrNoRows {
// if not found, noop
return nil
} else if err != nil {
return errors.Wrap(err, "finding trash book")
}
for i := 2; ; i++ {
candidate := fmt.Sprintf("%s (%d)", name, i)
var count int
err := tx.QueryRow("SELECT count(*) FROM books WHERE label = ?", candidate).Scan(&count)
if err != nil {
return errors.Wrap(err, "counting candidate")
}
if count == 0 {
_, err := tx.Exec("UPDATE books SET label = ?, dirty = ? WHERE uuid = ?", candidate, true, uuid)
if err != nil {
return errors.Wrapf(err, "updating book '%s'", name)
}
break
}
}
return nil
}
if err := migrateBook("trash"); err != nil {
return errors.Wrap(err, "migrating trash book")
}
if err := migrateBook("conflicts"); err != nil {
return errors.Wrap(err, "migrating conflicts book")
}
return nil
},
}
var rm1 = migration{
name: "sync-book-uuids-from-server",
run: func(ctx infra.DnoteCtx, tx *sql.Tx) error {
config, err := core.ReadConfig(ctx)
if err != nil {
return errors.Wrap(err, "reading the config")
}
if config.APIKey == "" {
return errors.New("login required")
}
resp, err := client.GetBooks(ctx, config.APIKey)
if err != nil {
return errors.Wrap(err, "getting books from the server")
}
log.Debug("book details from the server: %+v\n", resp)
UUIDMap := map[string]string{}
for _, book := range resp {
// Build a map from uuid to label
UUIDMap[book.Label] = book.UUID
}
for _, book := range resp {
// update uuid in the books table
log.Debug("Updating book %s\n", book.Label)
//todo if does not exist, then continue loop
var count int
if err := tx.
QueryRow("SELECT count(*) FROM books WHERE label = ?", book.Label).
Scan(&count); err != nil {
return errors.Wrapf(err, "checking if book exists: %s", book.Label)
}
if count == 0 {
continue
}
var originalUUID string
if err := tx.
QueryRow("SELECT uuid FROM books WHERE label = ?", book.Label).
Scan(&originalUUID); err != nil {
return errors.Wrapf(err, "scanning the orignal uuid of the book %s", book.Label)
}
log.Debug("original uuid: %s. new_uuid %s\n", originalUUID, book.UUID)
_, err := tx.Exec("UPDATE books SET uuid = ? WHERE label = ?", book.UUID, book.Label)
if err != nil {
return errors.Wrapf(err, "updating book '%s'", book.Label)
}
_, err = tx.Exec("UPDATE notes SET book_uuid = ? WHERE book_uuid = ?", book.UUID, originalUUID)
if err != nil {
return errors.Wrapf(err, "updating book_uuids of notes")
}
}
return nil
},
}

View file

@ -2,6 +2,9 @@
# dev.sh builds a new binary and replaces the old one in the PATH with it
rm "$(which dnote)" $GOPATH/bin/cli
go install -ldflags "-X main.apiEndpoint=http://127.0.0.1:5000" --tags "darwin" .
ln -s $GOPATH/bin/cli /usr/local/bin/dnote
sudo rm "$(which dnote)" $GOPATH/bin/cli
# change tags to darwin if on macos
go install -ldflags "-X main.apiEndpoint=http://127.0.0.1:5000" --tags "linux" .
sudo ln -s $GOPATH/bin/cli /usr/local/bin/dnote

View file

@ -3,5 +3,9 @@
# run_server_test.sh runs server test files sequentially
# https://stackoverflow.com/questions/23715302/go-how-to-run-tests-for-multiple-packages
# clear tmp dir in case not properly torn down
rm -rf ./tmp
# run test
go test ./... -p 1

View file

@ -1,13 +0,0 @@
{
"js": {
"name": "js",
"notes": [
{
"uuid": "43827b9a-c2b0-4c06-a290-97991c896653",
"content": "Booleans have toString()",
"added_on": 1515199943,
"edited_on": 0
}
]
}
}

View file

@ -1,19 +0,0 @@
{
"js": {
"name": "js",
"notes": [
{
"uuid": "43827b9a-c2b0-4c06-a290-97991c896653",
"content": "Booleans have toString()",
"added_on": 1515199943,
"edited_on": 0
},
{
"uuid": "f0d0fbb7-31ff-45ae-9f0f-4e429c0c797f",
"content": "Date object implements mathematical comparisons",
"added_on": 1515199951,
"edited_on": 0
}
]
}
}

View file

@ -1,30 +0,0 @@
{
"js": {
"name": "js",
"notes": [
{
"uuid": "43827b9a-c2b0-4c06-a290-97991c896653",
"content": "Booleans have toString()",
"added_on": 1515199943,
"edited_on": 0
},
{
"uuid": "f0d0fbb7-31ff-45ae-9f0f-4e429c0c797f",
"content": "Date object implements mathematical comparisons",
"added_on": 1515199951,
"edited_on": 0
}
]
},
"linux": {
"name": "linux",
"notes": [
{
"uuid": "3e065d55-6d47-42f2-a6bf-f5844130b2d2",
"content": "wc -l to count words",
"added_on": 1515199961,
"edited_on": 0
}
]
}
}

View file

@ -1,17 +0,0 @@
{
"js": {
"name": "js",
"notes": [
{
"uuid": "43827b9a-c2b0-4c06-a290-97991c896653",
"content": "Booleans have toString()",
"added_on": 1515199943,
"edited_on": 0
}
]
},
"linux": {
"name": "linux",
"notes": []
}
}

View file

@ -7,20 +7,12 @@ CREATE TABLE notes
added_on integer NOT NULL,
edited_on integer DEFAULT 0,
public bool DEFAULT false
);
, dirty bool DEFAULT false, usn int DEFAULT 0 NOT NULL, deleted bool DEFAULT false);
CREATE TABLE books
(
uuid text PRIMARY KEY,
label text NOT NULL
);
CREATE TABLE actions
(
uuid text PRIMARY KEY,
schema integer NOT NULL,
type text NOT NULL,
data text NOT NULL,
timestamp integer NOT NULL
);
, dirty bool DEFAULT false, usn int DEFAULT 0 NOT NULL, deleted bool DEFAULT false);
CREATE TABLE system
(
key string NOT NULL,

View file

@ -21,30 +21,41 @@ import (
)
// InitEnv sets up a test env and returns a new dnote context
func InitEnv(relPath string, relFixturePath string) infra.DnoteCtx {
func InitEnv(t *testing.T, relPath string, relFixturePath string, migrated bool) infra.DnoteCtx {
path, err := filepath.Abs(relPath)
if err != nil {
panic(errors.Wrap(err, "pasrsing path").Error())
t.Fatal(errors.Wrap(err, "pasrsing path"))
}
os.Setenv("DNOTE_HOME_DIR", path)
ctx, err := infra.NewCtx("", "")
if err != nil {
panic(errors.Wrap(err, "getting new ctx").Error())
t.Fatal(errors.Wrap(err, "getting new ctx"))
}
// set up directory and db
// set up directory
if err := os.MkdirAll(ctx.DnoteDir, 0755); err != nil {
panic(err)
t.Fatal(err)
}
// set up db
b := ReadFileAbs(relFixturePath)
setupSQL := string(b)
db := ctx.DB
_, err = db.Exec(setupSQL)
if err != nil {
panic(errors.Wrap(err, "running schema sql").Error())
if _, err := db.Exec(setupSQL); err != nil {
t.Fatal(errors.Wrap(err, "running schema sql"))
}
if migrated {
// mark migrations as done. When adding new migrations, bump the numbers here.
if _, err := db.Exec("INSERT INTO system (key, value) VALUES (? , ?);", infra.SystemSchema, 6); err != nil {
t.Fatal(errors.Wrap(err, "inserting schema"))
}
if _, err := db.Exec("INSERT INTO system (key, value) VALUES (? , ?);", infra.SystemRemoteSchema, 1); err != nil {
t.Fatal(errors.Wrap(err, "inserting remote schema"))
}
}
return ctx

View file

@ -27,12 +27,12 @@ func Setup2(t *testing.T, ctx infra.DnoteCtx) {
b1UUID := "js-book-uuid"
b2UUID := "linux-book-uuid"
MustExec(t, "setting up book 1", db, "INSERT INTO books (uuid, label) VALUES (?, ?)", b1UUID, "js")
MustExec(t, "setting up book 2", db, "INSERT INTO books (uuid, label) VALUES (?, ?)", b2UUID, "linux")
MustExec(t, "setting up book 1", db, "INSERT INTO books (uuid, label, usn) VALUES (?, ?, ?)", b1UUID, "js", 111)
MustExec(t, "setting up book 2", db, "INSERT INTO books (uuid, label, usn) VALUES (?, ?, ?)", b2UUID, "linux", 122)
MustExec(t, "setting up note 2", db, "INSERT INTO notes (id, uuid, book_uuid, content, added_on) VALUES (?, ?, ?, ?, ?)", 1, "f0d0fbb7-31ff-45ae-9f0f-4e429c0c797f", b1UUID, "Date object implements mathematical comparisons", 1515199951)
MustExec(t, "setting up note 1", db, "INSERT INTO notes (id, uuid, book_uuid, content, added_on) VALUES (?, ?, ?, ?, ?)", 2, "43827b9a-c2b0-4c06-a290-97991c896653", b1UUID, "Booleans have toString()", 1515199943)
MustExec(t, "setting up note 3", db, "INSERT INTO notes (id, uuid, book_uuid, content, added_on) VALUES (?, ?, ?, ?, ?)", 3, "3e065d55-6d47-42f2-a6bf-f5844130b2d2", b2UUID, "wc -l to count words", 1515199961)
MustExec(t, "setting up note 1", db, "INSERT INTO notes (id, uuid, book_uuid, content, added_on, usn) VALUES (?, ?, ?, ?, ?, ?)", 1, "f0d0fbb7-31ff-45ae-9f0f-4e429c0c797f", b1UUID, "n1 content", 1515199951, 11)
MustExec(t, "setting up note 2", db, "INSERT INTO notes (id, uuid, book_uuid, content, added_on, usn) VALUES (?, ?, ?, ?, ?, ?)", 2, "43827b9a-c2b0-4c06-a290-97991c896653", b1UUID, "n2 content", 1515199943, 12)
MustExec(t, "setting up note 3", db, "INSERT INTO notes (id, uuid, book_uuid, content, added_on, usn) VALUES (?, ?, ?, ?, ?, ?)", 3, "3e065d55-6d47-42f2-a6bf-f5844130b2d2", b2UUID, "n3 content", 1515199961, 13)
}
// Setup3 sets up a dnote env #1

View file

@ -2,11 +2,15 @@ package utils
import (
"bufio"
"fmt"
"io"
"io/ioutil"
"net/http"
"os"
"path/filepath"
"strings"
"github.com/dnote/cli/infra"
"github.com/dnote/cli/log"
"github.com/pkg/errors"
"github.com/satori/go.uuid"
@ -142,3 +146,24 @@ func CopyDir(src, dest string) error {
return nil
}
// DoAuthorizedReq does a http request to the given path in the api endpoint as a user,
// with the appropriate headers. The given path should include the preceding slash.
func DoAuthorizedReq(ctx infra.DnoteCtx, apiKey, method, path, body string) (*http.Response, error) {
endpoint := fmt.Sprintf("%s%s", ctx.APIEndpoint, path)
req, err := http.NewRequest(method, endpoint, strings.NewReader(body))
if err != nil {
return nil, errors.Wrap(err, "constructing http request")
}
req.Header.Set("Authorization", apiKey)
req.Header.Set("CLI-Version", ctx.Version)
hc := http.Client{}
res, err := hc.Do(req)
if err != nil {
return res, errors.Wrap(err, "making http request")
}
return res, nil
}