Simplify view and edit command (#184)

* Simplify the view command

* Simplify the edit command

* Migrate number-only book names

* Run migration

* Simplify remove

* Print note info when adding, editing, or removing

* Disallow users from editing or removing already removed notes
This commit is contained in:
Sung Won Cho 2019-05-18 16:52:12 +10:00 committed by GitHub
commit f526124243
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
16 changed files with 447 additions and 113 deletions

View file

@ -76,14 +76,28 @@ func isReservedName(name string) bool {
return false
}
// ErrBookNameReserved is an error incidating that the specified book name is reserved
var ErrBookNameReserved = errors.New("The book name is reserved")
// ErrNumericBookName is an error for book names that only contain numbers
var ErrNumericBookName = errors.New("The book name cannot contain only numbers")
func validateBookName(name string) error {
if isReservedName(name) {
return ErrBookNameReserved
}
if utils.IsNumber(name) {
return ErrNumericBookName
}
return nil
}
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)
@ -97,13 +111,19 @@ func newRun(ctx infra.DnoteCtx) core.RunEFunc {
}
ts := time.Now().UnixNano()
err := writeNote(ctx, bookName, content, ts)
noteRowID, err := writeNote(ctx, bookName, content, ts)
if err != nil {
return errors.Wrap(err, "Failed to write note")
}
log.Successf("added to %s\n", bookName)
log.PrintContent(content)
info, err := core.GetNoteInfo(ctx, noteRowID)
if err != nil {
return err
}
core.PrintNoteInfo(info)
if err := core.CheckUpdate(ctx); err != nil {
log.Error(errors.Wrap(err, "automatically checking updates").Error())
@ -113,10 +133,10 @@ func newRun(ctx infra.DnoteCtx) core.RunEFunc {
}
}
func writeNote(ctx infra.DnoteCtx, bookLabel string, content string, ts int64) error {
func writeNote(ctx infra.DnoteCtx, bookLabel string, content string, ts int64) (string, error) {
tx, err := ctx.DB.Begin()
if err != nil {
return errors.Wrap(err, "beginning a transaction")
return "", errors.Wrap(err, "beginning a transaction")
}
var bookUUID string
@ -128,10 +148,10 @@ func writeNote(ctx infra.DnoteCtx, bookLabel string, content string, ts int64) e
err = b.Insert(tx)
if err != nil {
tx.Rollback()
return errors.Wrap(err, "creating the book")
return "", errors.Wrap(err, "creating the book")
}
} else if err != nil {
return errors.Wrap(err, "finding the book")
return "", errors.Wrap(err, "finding the book")
}
noteUUID := utils.GenerateUUID()
@ -140,10 +160,19 @@ func writeNote(ctx infra.DnoteCtx, bookLabel string, content string, ts int64) e
err = n.Insert(tx)
if err != nil {
tx.Rollback()
return errors.Wrap(err, "creating the note")
return "", errors.Wrap(err, "creating the note")
}
var noteRowID string
err = tx.QueryRow(`SELECT notes.rowid
FROM notes
WHERE notes.uuid = ?`, noteUUID).
Scan(&noteRowID)
if err != nil {
return noteRowID, errors.Wrap(err, "getting the note rowid")
}
tx.Commit()
return nil
return noteRowID, nil
}

85
cli/cmd/add/add_test.go Normal file
View file

@ -0,0 +1,85 @@
/* Copyright (C) 2019 Monomax Software Pty Ltd
*
* This file is part of Dnote CLI.
*
* Dnote CLI is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* Dnote CLI is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with Dnote CLI. If not, see <https://www.gnu.org/licenses/>.
*/
package add
import (
"testing"
"github.com/dnote/dnote/cli/testutils"
)
func TestValidateBookName(t *testing.T) {
testCases := []struct {
input string
expected error
}{
{
input: "javascript",
expected: nil,
},
{
input: "node.js",
expected: nil,
},
{
input: "foo bar",
expected: nil,
},
{
input: "123",
expected: ErrNumericBookName,
},
{
input: "+123",
expected: nil,
},
{
input: "-123",
expected: nil,
},
{
input: "+javascript",
expected: nil,
},
{
input: "0",
expected: ErrNumericBookName,
},
{
input: "0333",
expected: ErrNumericBookName,
},
// reserved book names
{
input: "trash",
expected: ErrBookNameReserved,
},
{
input: "conflicts",
expected: ErrBookNameReserved,
},
}
for _, tc := range testCases {
actual := validateBookName(tc.input)
testutils.AssertEqual(t, actual, tc.expected, "result does not match")
}
}

View file

@ -19,10 +19,6 @@
package cat
import (
"database/sql"
"fmt"
"time"
"github.com/dnote/dnote/cli/core"
"github.com/dnote/dnote/cli/infra"
"github.com/dnote/dnote/cli/log"
@ -63,50 +59,25 @@ func NewCmd(ctx infra.DnoteCtx) *cobra.Command {
return cmd
}
type noteInfo struct {
BookLabel string
UUID string
Content string
AddedOn int64
EditedOn int64
}
// NewRun returns a new run function
func NewRun(ctx infra.DnoteCtx) core.RunEFunc {
return func(cmd *cobra.Command, args []string) error {
db := ctx.DB
bookLabel := args[0]
noteRowID := args[1]
var noteRowID string
var bookUUID string
err := db.QueryRow("SELECT uuid FROM books WHERE label = ?", bookLabel).Scan(&bookUUID)
if err == sql.ErrNoRows {
return errors.Errorf("book '%s' not found", bookLabel)
} else if err != nil {
return errors.Wrap(err, "querying the book")
if len(args) == 2 {
log.Plain(log.ColorYellow.Sprintf("DEPRECATED: you no longer need to pass book name to the view command. e.g. `dnote view 123`.\n\n"))
noteRowID = args[1]
} else {
noteRowID = args[0]
}
var info noteInfo
err = db.QueryRow(`SELECT books.label, notes.uuid, notes.body, notes.added_on, notes.edited_on
FROM notes
INNER JOIN books ON books.uuid = notes.book_uuid
WHERE notes.rowid = ? AND books.uuid = ?`, noteRowID, bookUUID).
Scan(&info.BookLabel, &info.UUID, &info.Content, &info.AddedOn, &info.EditedOn)
if err == sql.ErrNoRows {
return errors.Errorf("note %s not found in the book '%s'", noteRowID, bookLabel)
} else if err != nil {
return errors.Wrap(err, "querying the note")
info, err := core.GetNoteInfo(ctx, noteRowID)
if err != nil {
return err
}
log.Infof("book name: %s\n", info.BookLabel)
log.Infof("note uuid: %s\n", info.UUID)
log.Infof("created at: %s\n", time.Unix(0, info.AddedOn).Format("Jan 2, 2006 3:04pm (MST)"))
if info.EditedOn != 0 {
log.Infof("updated at: %s\n", time.Unix(0, info.EditedOn).Format("Jan 2, 2006 3:04pm (MST)"))
}
fmt.Printf("\n------------------------content------------------------\n")
fmt.Printf("%s", info.Content)
fmt.Printf("\n-------------------------------------------------------\n")
core.PrintNoteInfo(info)
return nil
}

View file

@ -34,11 +34,11 @@ import (
var newContent string
var example = `
* Edit the note by index in a book
dnote edit js 3
* Edit the note by its id
dnote edit 3
* Skip the prompt by providing new content directly
dnote edit js 3 -c "new content"`
dnote edit 3 -c "new content"`
// NewCmd returns a new edit command
func NewCmd(ctx infra.DnoteCtx) *cobra.Command {
@ -58,7 +58,7 @@ func NewCmd(ctx infra.DnoteCtx) *cobra.Command {
}
func preRun(cmd *cobra.Command, args []string) error {
if len(args) != 2 {
if len(args) > 2 {
return errors.New("Incorrect number of argument")
}
@ -68,18 +68,21 @@ func preRun(cmd *cobra.Command, args []string) error {
func newRun(ctx infra.DnoteCtx) core.RunEFunc {
return func(cmd *cobra.Command, args []string) error {
db := ctx.DB
bookLabel := args[0]
noteRowID := args[1]
bookUUID, err := core.GetBookUUID(ctx, bookLabel)
if err != nil {
return errors.Wrap(err, "finding book uuid")
var noteRowID string
if len(args) == 2 {
log.Plain(log.ColorYellow.Sprintf("DEPRECATED: you no longer need to pass book name to the view command. e.g. `dnote view 123`.\n\n"))
noteRowID = args[1]
} else {
noteRowID = args[0]
}
var noteUUID, oldContent string
err = db.QueryRow("SELECT uuid, body FROM notes WHERE rowid = ? AND book_uuid = ?", noteRowID, bookUUID).Scan(&noteUUID, &oldContent)
err := db.QueryRow("SELECT uuid, body FROM notes WHERE rowid = ? AND deleted = false", noteRowID).Scan(&noteUUID, &oldContent)
if err == sql.ErrNoRows {
return errors.Errorf("note %s not found in the book '%s'", noteRowID, bookLabel)
return errors.Errorf("note %s not found", noteRowID)
} else if err != nil {
return errors.Wrap(err, "querying the book")
}
@ -112,7 +115,7 @@ func newRun(ctx infra.DnoteCtx) core.RunEFunc {
_, err = tx.Exec(`UPDATE notes
SET body = ?, edited_on = ?, dirty = ?
WHERE rowid = ? AND book_uuid = ?`, newContent, ts, true, noteRowID, bookUUID)
WHERE rowid = ?`, newContent, ts, true, noteRowID)
if err != nil {
tx.Rollback()
return errors.Wrap(err, "updating the note")

View file

@ -19,7 +19,6 @@
package remove
import (
"database/sql"
"fmt"
"github.com/dnote/dnote/cli/core"
@ -33,8 +32,8 @@ import (
var targetBookName string
var example = `
* Delete a note by its index from a book
dnote delete js 2
* Delete a note by its id
dnote delete 2
* Delete a book
dnote delete -b js`
@ -65,14 +64,18 @@ func newRun(ctx infra.DnoteCtx) core.RunEFunc {
return nil
}
if len(args) < 2 {
var noteRowID string
if len(args) == 2 {
log.Plain(log.ColorYellow.Sprintf("DEPRECATED: you no longer need to pass book name to the view command. e.g. `dnote view 123`.\n\n"))
noteRowID = args[1]
} else if len(args) == 1 {
noteRowID = args[0]
} else {
return errors.New("Missing argument")
}
targetBook := args[0]
noteRowID := args[1]
if err := removeNote(ctx, noteRowID, targetBook); err != nil {
if err := removeNote(ctx, noteRowID); err != nil {
return errors.Wrap(err, "removing the note")
}
@ -80,24 +83,15 @@ func newRun(ctx infra.DnoteCtx) core.RunEFunc {
}
}
func removeNote(ctx infra.DnoteCtx, noteRowID, bookLabel string) error {
func removeNote(ctx infra.DnoteCtx, noteRowID string) error {
db := ctx.DB
bookUUID, err := core.GetBookUUID(ctx, bookLabel)
noteInfo, err := core.GetNoteInfo(ctx, noteRowID)
if err != nil {
return errors.Wrap(err, "finding book uuid")
return err
}
var noteUUID, noteContent string
err = db.QueryRow("SELECT uuid, body FROM notes WHERE rowid = ? AND book_uuid = ?", noteRowID, bookUUID).Scan(&noteUUID, &noteContent)
if err == sql.ErrNoRows {
return errors.Errorf("note %s not found in the book '%s'", noteRowID, bookLabel)
} else if err != nil {
return errors.Wrap(err, "querying the book")
}
// todo: multiline
log.Printf("body: \"%s\"\n", noteContent)
core.PrintNoteInfo(noteInfo)
ok, err := utils.AskConfirmation("remove this note?", false)
if err != nil {
@ -113,12 +107,12 @@ func removeNote(ctx infra.DnoteCtx, noteRowID, bookLabel string) error {
return errors.Wrap(err, "beginning a transaction")
}
if _, err = tx.Exec("UPDATE notes SET deleted = ?, dirty = ?, body = ? WHERE uuid = ? AND book_uuid = ?", true, true, "", noteUUID, bookUUID); err != nil {
if _, err = tx.Exec("UPDATE notes SET deleted = ?, dirty = ?, body = ? WHERE uuid = ?", true, true, "", noteInfo.UUID); err != nil {
return errors.Wrap(err, "removing the note")
}
tx.Commit()
log.Successf("removed from %s\n", bookLabel)
log.Successf("removed from %s\n", noteInfo.BookLabel)
return nil
}

View file

@ -26,6 +26,7 @@ import (
"github.com/dnote/dnote/cli/cmd/cat"
"github.com/dnote/dnote/cli/cmd/ls"
"github.com/dnote/dnote/cli/utils"
)
var example = `
@ -65,9 +66,16 @@ func newRun(ctx infra.DnoteCtx) core.RunEFunc {
return func(cmd *cobra.Command, args []string) error {
var run core.RunEFunc
if len(args) <= 1 {
if len(args) == 0 {
run = ls.NewRun(ctx)
} else if len(args) == 1 {
if utils.IsNumber(args[0]) {
run = cat.NewRun(ctx)
} else {
run = ls.NewRun(ctx)
}
} else if len(args) == 2 {
// DEPRECATED: passing book name to view command is deprecated
run = cat.NewRun(ctx)
} else {
return errors.New("Incorrect number of arguments")